📬 Newsletter · 🔄 Sync from Buttondown #11
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: '📬 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/" |