Skip to content

Post Gallery

Post Gallery #214

Workflow file for this run

name: Post Gallery
on:
schedule:
# Run daily at 10:00 AM UTC (adjustable)
- cron: '0 10 * * *'
workflow_dispatch: # Allow manual trigger
jobs:
post-gallery:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Find and post gallery entry
id: gallery
run: |
GALLERY_FILE="src/lib/fixtures/gallery.json"
POSTED_FILE=".github/gallery-posted.json"
# Get list of already posted IDs
POSTED_IDS=$(jq -r '.posted[].id' "$POSTED_FILE" 2>/dev/null || echo "")
# Find first unposted gallery entry that has social explicitly enabled (social == true)
ENTRY=$(jq -c --argjson posted "$(jq '.posted | map(.id)' "$POSTED_FILE" 2>/dev/null || echo '[]')" \
'[.[] | select(.social == true) | select(.id as $id | $posted | index($id) | not)] | first' "$GALLERY_FILE")
if [ "$ENTRY" = "null" ] || [ -z "$ENTRY" ]; then
echo "All gallery entries have been posted!"
echo "has_entry=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_entry=true" >> $GITHUB_OUTPUT
# Extract fields
ID=$(echo "$ENTRY" | jq -r '.id')
TITLE=$(echo "$ENTRY" | jq -r '.title')
DESCRIPTION=$(echo "$ENTRY" | jq -r '.description')
AUTHOR_NAME=$(echo "$ENTRY" | jq -r '.author.name')
AUTHOR_URL=$(echo "$ENTRY" | jq -r '.author.url')
TAGS=$(echo "$ENTRY" | jq -r '.tags | map("#" + .) | join(" ")')
IMAGE=$(echo "$ENTRY" | jq -r '.image')
FEATURED=$(echo "$ENTRY" | jq -r '.featured')
echo "id=$ID" >> $GITHUB_OUTPUT
echo "title=$TITLE" >> $GITHUB_OUTPUT
echo "description=$DESCRIPTION" >> $GITHUB_OUTPUT
echo "author_name=$AUTHOR_NAME" >> $GITHUB_OUTPUT
echo "author_url=$AUTHOR_URL" >> $GITHUB_OUTPUT
echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "image=https://scanopy.net$IMAGE" >> $GITHUB_OUTPUT
echo "featured=$FEATURED" >> $GITHUB_OUTPUT
echo "Processing gallery entry: $TITLE by $AUTHOR_NAME"
- name: Download gallery image
if: steps.gallery.outputs.has_entry == 'true'
run: |
curl -L -o gallery-image.png "${{ steps.gallery.outputs.image }}"
echo "Downloaded image from ${{ steps.gallery.outputs.image }}"
- name: Update posted tracking file
if: steps.gallery.outputs.has_entry == 'true'
run: |
POSTED_FILE=".github/gallery-posted.json"
jq --arg id "${{ steps.gallery.outputs.id }}" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'.posted += [{"id": $id, "posted_at": $date}]' "$POSTED_FILE" > tmp.json
mv tmp.json "$POSTED_FILE"
- name: Setup Node.js
if: steps.gallery.outputs.has_entry == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Post to Twitter/X with media
if: steps.gallery.outputs.has_entry == 'true'
env:
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
TWEET_TITLE: ${{ steps.gallery.outputs.title }}
TWEET_DESCRIPTION: ${{ steps.gallery.outputs.description }}
TWEET_AUTHOR: ${{ steps.gallery.outputs.author_name }}
TWEET_TAGS: ${{ steps.gallery.outputs.tags }}
run: |
npm install twitter-api-v2 sharp
node << 'SCRIPT'
const { TwitterApi } = require('twitter-api-v2');
const fs = require('fs');
const sharp = require('sharp');
const client = new TwitterApi({
appKey: process.env.TWITTER_CONSUMER_KEY,
appSecret: process.env.TWITTER_CONSUMER_SECRET,
accessToken: process.env.TWITTER_ACCESS_TOKEN,
accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET,
});
async function postTweet() {
try {
// Check original image size
const stats = fs.statSync('./gallery-image.png');
console.log('Original image size:', stats.size, 'bytes', '(' + (stats.size / 1024 / 1024).toFixed(2) + ' MB)');
// Compress image if larger than 1MB (Twitter limit is 5MB but smaller is safer)
let imagePath = './gallery-image.png';
if (stats.size > 1024 * 1024) {
console.log('Image too large, compressing...');
await sharp('./gallery-image.png')
.resize(1920, 1920, { fit: 'inside', withoutEnlargement: true })
.png({ quality: 80, compressionLevel: 9 })
.toFile('./gallery-image-compressed.png');
const compressedStats = fs.statSync('./gallery-image-compressed.png');
console.log('Compressed image size:', compressedStats.size, 'bytes', '(' + (compressedStats.size / 1024 / 1024).toFixed(2) + ' MB)');
imagePath = './gallery-image-compressed.png';
}
// Upload media using v1.1 API (the original working method)
console.log('Uploading media...');
const mediaId = await client.v1.uploadMedia(imagePath);
console.log('Media uploaded, ID:', mediaId);
// Compose tweet text
const text = [
`Community Showcase: ${process.env.TWEET_TITLE}`,
'',
process.env.TWEET_DESCRIPTION,
'',
`Submitted by ${process.env.TWEET_AUTHOR}`,
'',
'Check out more at https://scanopy.net/showcase?utm_source=twitter&utm_medium=social&utm_campaign=gallery',
'',
`${process.env.TWEET_TAGS} #Scanopy #NetworkDiagram #Homelab`
].join('\n');
// Post tweet using v2 API with media
const tweet = await client.v2.tweet({
text: text,
media: { media_ids: [mediaId] }
});
console.log('Tweet posted successfully:', tweet.data.id);
} catch (error) {
console.error('Error posting tweet:', error);
process.exit(1);
}
}
postTweet();
SCRIPT
- name: Compress image for Bluesky
if: steps.gallery.outputs.has_entry == 'true'
run: |
node << 'SCRIPT'
const sharp = require('sharp');
const fs = require('fs');
async function compress() {
// Bluesky has a 976KB limit, compress aggressively to JPEG
let quality = 70;
let size = 1200;
while (size >= 600) {
await sharp('./gallery-image.png')
.resize(size, size, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality })
.toFile('./gallery-image-bsky.jpg');
const stats = fs.statSync('./gallery-image-bsky.jpg');
console.log(`Size ${size}px, quality ${quality}: ${(stats.size / 1024).toFixed(0)}KB`);
if (stats.size < 950000) {
console.log('Compression successful!');
return;
}
// Try lower quality first, then smaller size
if (quality > 50) {
quality -= 10;
} else {
size -= 200;
quality = 70;
}
}
console.log('Warning: Could not compress below 950KB');
}
compress().catch(console.error);
SCRIPT
- name: Post to Bluesky
if: steps.gallery.outputs.has_entry == 'true'
uses: myConsciousness/bluesky-post@v5
with:
text: |
Community Showcase: ${{ steps.gallery.outputs.title }}
${{ steps.gallery.outputs.description }}
Submitted by ${{ steps.gallery.outputs.author_name }}
Check out more at https://scanopy.net/showcase?utm_source=bluesky&utm_medium=social&utm_campaign=gallery
${{ steps.gallery.outputs.tags }} #Scanopy #NetworkDiagram #Homelab
media: gallery-image-bsky.jpg
media-alt: "Network diagram: ${{ steps.gallery.outputs.title }} by ${{ steps.gallery.outputs.author_name }}"
identifier: ${{ secrets.BLUESKY_IDENTIFIER }}
password: ${{ secrets.BLUESKY_PASSWORD }}
# - name: Post to Reddit
# if: steps.gallery.outputs.has_entry == 'true'
# uses: bluwy/release-for-reddit-action@v2
# with:
# username: ${{ secrets.REDDIT_USERNAME }}
# password: ${{ secrets.REDDIT_PASSWORD }}
# app-id: ${{ secrets.REDDIT_APP_ID }}
# app-secret: ${{ secrets.REDDIT_APP_SECRET }}
# subreddit: scanopy
# title: "Community Showcase: ${{ steps.gallery.outputs.title }}"
# text: |
# ${{ steps.gallery.outputs.description }}
#
# Submitted by ${{ steps.gallery.outputs.author_name }}
#
# ---
#
# [View in showcase](https://scanopy.net/showcase?utm_source=reddit&utm_medium=social&utm_campaign=gallery)
#
# Submit your own network diagram: https://scanopy.net/showcase
- name: Commit tracking update
if: steps.gallery.outputs.has_entry == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: mark gallery entry '${{ steps.gallery.outputs.id }}' as posted"
file_pattern: .github/gallery-posted.json