Skip to content

📬 Newsletter · 🔄 Sync from Buttondown #9

📬 Newsletter · 🔄 Sync from Buttondown

📬 Newsletter · 🔄 Sync from Buttondown #9

name: '📬 Newsletter · 🔄 Sync & Deploy'
# =============================================================================
# Newsletter — Sync from Buttondown, Build & Deploy
# =============================================================================
#
# Fetches published emails from the Buttondown API, converts them to
# Quarto-compatible markdown, and deploys the newsletter to both sites.
# Posts are committed to dev only — main is never touched.
#
# Flow:
# 1. SYNC — Fetch emails from Buttondown API → write .md posts
# 2. COMMIT — Stage new/updated posts → push to dev
# 3. BUILD — Quarto render (only if new posts found)
# 4. DEPLOY — Newsletter slice to dev preview + live gh-pages
#
# Triggers:
# - Schedule: Daily at 6 AM UTC (1 AM ET)
# - Manual: Choose deploy target (both / dev / live)
#
# Deploys to:
# - Dev preview: SSH push to DEV_REPO_URL/newsletter/
# - Live site: gh-pages branch /newsletter/ (no full publish needed)
#
# Secrets: BUTTONDOWN_API_KEY, SSH_DEPLOY_KEY, GITHUB_TOKEN
# Vars: DEV_REPO_URL
#
# Related:
# - site/newsletter/cli/ — news CLI (pull subcommand powers this)
# - site-publish-live.yml — Also syncs before full builds
# - site-preview-dev.yml — Also syncs before dev builds
#
# =============================================================================
on:
schedule:
- cron: '0 6 * * *' # Daily at 6am UTC (1am ET) — syncs before morning traffic
workflow_dispatch:
inputs:
deploy_target:
description: 'Where to deploy'
type: choice
options:
- both
- dev
- live
default: both
permissions:
contents: write
actions: read
concurrency:
group: newsletter-sync
cancel-in-progress: true
jobs:
sync-and-deploy:
name: '📬 Sync, Build & Deploy Newsletter'
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
with:
ref: dev
- name: 🐍 Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: 📦 Install news CLI
run: pip install -r site/newsletter/requirements.txt
- name: 📬 Sync newsletter from Buttondown
env:
BUTTONDOWN_API_KEY: ${{ secrets.BUTTONDOWN_API_KEY }}
run: python3 site/newsletter/bin/news pull
- name: 📦 Commit new posts to dev
id: commit
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add site/newsletter/posts/ site/newsletter/_stats.yml
if git diff --staged --quiet; then
echo "ℹ️ No new newsletter posts"
echo "has_new_posts=false" >> "$GITHUB_OUTPUT"
else
COUNT=$(git diff --staged --name-only | wc -l | tr -d ' ')
git commit -m "chore: sync $COUNT newsletter post(s) from Buttondown [automated]"
git push origin dev
echo "✅ Committed $COUNT new post(s) to dev"
echo "has_new_posts=true" >> "$GITHUB_OUTPUT"
fi
# ── Build ─────────────────────────────────────────────────────────────
- name: 🔧 Setup Quarto
if: steps.commit.outputs.has_new_posts == 'true'
uses: quarto-dev/quarto-actions/setup@v2
- name: 🔨 Build newsletter
if: steps.commit.outputs.has_new_posts == 'true'
working-directory: site
run: |
quarto render
touch _build/.nojekyll
echo "✅ Built: $(find _build/newsletter -name '*.html' | wc -l) newsletter pages"
- name: 🔍 Validate build
if: steps.commit.outputs.has_new_posts == 'true'
run: |
if [ ! -f "site/_build/newsletter/index.html" ]; then
echo "❌ CRITICAL: newsletter/index.html missing. Aborting."
exit 1
fi
# ── Deploy to Dev Preview ─────────────────────────────────────────────
- name: 🚀 Deploy to Dev Site
if: >-
steps.commit.outputs.has_new_posts == 'true' &&
(inputs.deploy_target == 'dev' || inputs.deploy_target == 'both' || inputs.deploy_target == '')
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
run: |
# Rewrite URLs for dev site prefix
bash .github/scripts/rewrite-dev-urls.sh newsletter site/_build/newsletter
# Modify announcement banner for dev preview
COMMIT_HASH="${{ github.sha }}"
COMMIT_SHORT="${COMMIT_HASH:0:8}"
python3 ${{ vars.BOOK_TOOLS }}/scripts/publish/modify_dev_announcement.py \
site/_build/newsletter \
--verbose \
--commit-hash "$COMMIT_HASH" \
--commit-short "$COMMIT_SHORT"
eval "$(ssh-agent -s)"
echo "$SSH_DEPLOY_KEY" | tr -d '\r' | ssh-add - > /dev/null
mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
git config --global user.email "actions@github.com"
git config --global user.name "GitHub Actions"
git clone --depth=1 ${{ vars.DEV_REPO_URL }} target-repo
cd target-repo
rm -rf newsletter && mkdir -p newsletter
cp -r "${{ github.workspace }}/site/_build/newsletter/." newsletter/
cp -r "${{ github.workspace }}/site/_build/site_libs" . 2>/dev/null || true
[ -f "newsletter/index.html" ] || { echo "❌ newsletter/index.html missing"; exit 1; }
git add .
git commit -m "📬 Deploy Newsletter dev from ${{ github.sha }}" --allow-empty || true
for i in 1 2 3; do
git push origin main 2>/dev/null && break
git pull --rebase origin main || true
done
echo "✅ Newsletter deployed to dev preview"
# ── Deploy to Live Site ───────────────────────────────────────────────
# Only touches the newsletter/ directory on gh-pages.
# All other live content (book, kits, etc.) is untouched.
- name: 🔨 Rebuild for live (clean URLs)
if: >-
steps.commit.outputs.has_new_posts == 'true' &&
(inputs.deploy_target == 'live' || inputs.deploy_target == 'both' || inputs.deploy_target == '')
working-directory: site
run: |
# Dev deploy rewrites URLs in-place, so re-render for clean live URLs
quarto render
touch _build/.nojekyll
- name: 🚀 Deploy to Live Site (gh-pages)
if: >-
steps.commit.outputs.has_new_posts == 'true' &&
(inputs.deploy_target == 'live' || inputs.deploy_target == 'both' || inputs.deploy_target == '')
run: |
[ -f "site/_build/newsletter/index.html" ] || { echo "❌ newsletter/index.html missing"; exit 1; }
rm -rf gh-pages-repo
git clone --depth=1 --branch=gh-pages \
https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git \
gh-pages-repo
cd gh-pages-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Only replace the newsletter directory — leave everything else alone
rm -rf newsletter
cp -r "${{ github.workspace }}/site/_build/newsletter" newsletter
cp -r "${{ github.workspace }}/site/_build/site_libs" . 2>/dev/null || true
touch .nojekyll
git add .
git commit -m "📬 Auto-sync newsletter to live from ${{ github.sha }}" --allow-empty || true
for i in 1 2 3; do
git push origin gh-pages 2>/dev/null && break
git pull --rebase origin gh-pages || true
done
echo "✅ Newsletter deployed to mlsysbook.ai/newsletter/"