Skip to content

Syndicate Content to Social Media #788

Syndicate Content to Social Media

Syndicate Content to Social Media #788

name: Syndicate Content to Social Media
on:
# Webhook trigger from Netlify after successful deploy
repository_dispatch:
types: [netlify-deploy-succeeded]
# Manual trigger for testing
workflow_dispatch:
inputs:
content_type:
description: "Content type to syndicate"
required: false
default: "both"
type: choice
options:
- posts
- links
- both
test_mode:
description: "Run in test mode (no actual posts)"
required: false
default: false
type: boolean
bootstrap_cache_before:
description: "Seed cache for items published before this date (YYYY-MM-DD)"
required: false
default: ""
type: string
bootstrap_cache_only:
description: "Only seed the cache and skip syndication"
required: false
default: false
type: boolean
siteName:
description: "Netlify site name (auto-populated)"
required: false
default: ""
type: string
deployPrimeUrl:
description: "Netlify deploy preview URL"
required: false
default: ""
type: string
commit:
description: "Commit SHA for the deploy"
required: false
default: ""
type: string
max_items_per_run:
description: "Max feed items to syndicate per content type per run (0 = no limit)"
required: false
default: "1"
type: string
# Scheduled weekday check at 18:30 UTC (11:30am PDT / 10:30am PST)
schedule:
- cron: "30 18 * * 1-5"
# Only allow one syndication run at a time per branch to prevent concurrent runs
# from reading the same stale cache and sending duplicate posts to social platforms.
# cancel-in-progress: false ensures queued runs wait for the current run to finish
# (rather than being cancelled) so they pick up the updated cache and skip
# already-syndicated items.
concurrency:
group: syndication-${{ github.ref_name }}
cancel-in-progress: false
jobs:
syndicate:
if: github.ref_name == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Install dependencies
run: |
# Only install the minimal dependencies needed for syndication
npm install node-fetch@3 html-to-text
- name: Resolve latest processed items cache key
id: syndication-cache-latest
env:
GH_TOKEN: ${{ github.token }}
run: |
key_prefix="syndication-cache-${{ github.ref_name }}-"
latest_key=$(curl -fsSL -G \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
--data-urlencode "per_page=100" \
--data-urlencode "key=${key_prefix}" \
--data-urlencode "ref=${{ github.ref }}" \
"${{ github.api_url }}/repos/${{ github.repository }}/actions/caches" | jq -r '.actions_caches | sort_by(.created_at) | last | .key // empty')
if [ -n "$latest_key" ]; then
echo "latest_key=$latest_key" >> "$GITHUB_OUTPUT"
echo "Resolved latest cache key: $latest_key"
else
echo "latest_key=${key_prefix}" >> "$GITHUB_OUTPUT"
echo "No existing cache key found for prefix: $key_prefix"
fi
- name: Setup environment
run: |
echo "SITE_URL=https://www.aaron-gustafson.com" >> $GITHUB_ENV
echo "POSTS_FEED_URL=https://www.aaron-gustafson.com/feeds/latest-posts.json" >> $GITHUB_ENV
echo "LINKS_FEED_URL=https://www.aaron-gustafson.com/feeds/latest-links.json" >> $GITHUB_ENV
echo "TEST_MODE=${{ github.event.inputs.test_mode || 'false' }}" >> $GITHUB_ENV
echo "BOOTSTRAP_CACHE_BEFORE=${{ github.event.inputs.bootstrap_cache_before || '' }}" >> $GITHUB_ENV
echo "BOOTSTRAP_CACHE_CONTENT_TYPE=${{ github.event.inputs.content_type || 'both' }}" >> $GITHUB_ENV
# Safety guard: prevent accidental real posts when running on a non-main branch.
# This can happen when workflow_dispatch is triggered on a feature or Copilot branch
# that has no syndication cache — every feed item would appear "new" and get posted.
- name: Enforce test mode on non-main branches
if: github.ref_name != 'main'
run: |
echo "⚠️ Running on non-main branch ('${{ github.ref_name }}'). Forcing TEST_MODE=true to prevent real posts."
echo "TEST_MODE=true" >> $GITHUB_ENV
# Restore cache before processing to avoid duplicates
- name: Restore processed items cache
id: syndication-cache
uses: actions/cache/restore@v5
with:
path: .github/cache
key: ${{ steps.syndication-cache-latest.outputs.latest_key }}
restore-keys: |
syndication-cache-${{ github.ref_name }}-
- name: Inspect restored cache
if: always()
run: node .github/scripts/inspect-syndication-cache.js
- name: Seed processed items cache
if: github.event_name == 'workflow_dispatch' && github.event.inputs.bootstrap_cache_before != ''
run: node .github/scripts/bootstrap-syndication-cache.js
- name: Inspect cache after seeding
if: always() && github.event_name == 'workflow_dispatch' && github.event.inputs.bootstrap_cache_before != ''
run: node .github/scripts/inspect-syndication-cache.js
# Check for new posts
- name: Syndicate Posts
if: github.event.inputs.bootstrap_cache_only != 'true' && github.event.inputs.content_type != 'links'
run: node .github/scripts/syndicate-posts.js
env:
# Mastodon API
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
MASTODON_SERVER_URL: ${{ secrets.MASTODON_SERVER_URL }}
# Buffer API for Twitter/Bluesky
BUFFER_ACCESS_TOKEN: ${{ secrets.BUFFER_ACCESS_TOKEN }}
BUFFER_TWITTER_PROFILE_ID: ${{ secrets.BUFFER_TWITTER_PROFILE_ID }}
BUFFER_BLUESKY_PROFILE_ID: ${{ secrets.BUFFER_BLUESKY_PROFILE_ID }}
# IFTTT Webhook (fallback)
IFTTT_KEY: ${{ secrets.IFTTT_KEY }}
# Stagger: max items to post per platform per run (0 = no limit)
MAX_ITEMS_PER_RUN: ${{ github.event.inputs.max_items_per_run || '1' }}
# Check for new links
- name: Syndicate Links
if: github.event.inputs.bootstrap_cache_only != 'true' && github.event.inputs.content_type != 'posts'
run: node .github/scripts/syndicate-links.js
env:
# Mastodon API
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
MASTODON_SERVER_URL: ${{ secrets.MASTODON_SERVER_URL }}
# Buffer API for Twitter/Bluesky
BUFFER_ACCESS_TOKEN: ${{ secrets.BUFFER_ACCESS_TOKEN }}
BUFFER_TWITTER_PROFILE_ID: ${{ secrets.BUFFER_TWITTER_PROFILE_ID }}
BUFFER_BLUESKY_PROFILE_ID: ${{ secrets.BUFFER_BLUESKY_PROFILE_ID }}
# IFTTT Webhook (fallback)
IFTTT_KEY: ${{ secrets.IFTTT_KEY }}
# Stagger: max items to post per platform per run (0 = no limit)
MAX_ITEMS_PER_RUN: ${{ github.event.inputs.max_items_per_run || '1' }}
# Save updated cache after processing
- name: Compute processed items cache key
id: syndication-cache-key
if: always()
run: |
if [ -f .github/cache/syndication-status.json ]; then
cache_hash=$(sha256sum .github/cache/syndication-status.json | cut -d' ' -f1)
else
cache_hash=empty
fi
echo "key=syndication-cache-${{ github.ref_name }}-${cache_hash}" >> "$GITHUB_OUTPUT"
- name: Inspect cache before save
if: always()
run: |
echo "restored_key=${{ steps.syndication-cache.outputs.cache-matched-key }}"
echo "computed_key=${{ steps.syndication-cache-key.outputs.key }}"
node .github/scripts/inspect-syndication-cache.js
- name: Save processed items cache
uses: actions/cache/save@v5
if: always() && steps.syndication-cache.outputs.cache-matched-key != steps.syndication-cache-key.outputs.key
with:
path: .github/cache
key: ${{ steps.syndication-cache-key.outputs.key }}