Post Gallery #214
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |