Skip to content

🌐 Landing Site · 🚀 Publish (Live) #24

🌐 Landing Site · 🚀 Publish (Live)

🌐 Landing Site · 🚀 Publish (Live) #24

name: '🌐 Landing Site · 🚀 Publish (Live)'
# =============================================================================
# Unified Site — Publish to mlsysbook.ai
# =============================================================================
#
# Builds the unified site (landing + about + community + newsletter) and
# deploys to the live site via gh-pages. Syncs newsletter from Buttondown
# before building so posts are always current.
#
# Flow:
# 1. SYNC — Fetch latest newsletter posts from Buttondown API
# 2. BUILD — Quarto render entire site
# 3. VALIDATE — Verify all expected pages exist
# 4. DEPLOY — Push to gh-pages branch (only site subsites, preserves others)
#
# Triggers:
# - Manual: workflow_dispatch
# - Called by: publish-all-live.yml (orchestrator)
#
# Deploys to: gh-pages → mlsysbook.ai/{about,community,newsletter}/
# Secrets: BUTTONDOWN_API_KEY, GITHUB_TOKEN
#
# Related:
# - site-preview-dev.yml — Same site, deployed to dev preview
# - sync-newsletter.yml — Dedicated daily newsletter sync
# - publish-all-live.yml — Orchestrates all live deploys
#
# =============================================================================
on:
workflow_dispatch: {}
workflow_call: {}
permissions:
contents: write
actions: read
# NB: gh-pages-deploy concurrency is declared on the build-and-deploy job
# only. The guard runs without holding the lock.
jobs:
# Block publish unless the latest dev validate run for this site is green.
# See .github/workflows/infra-publish-guard.yml for guard semantics.
guard:
name: '🛡️ Validate-Dev Green Gate'
uses: ./.github/workflows/infra-publish-guard.yml
with:
validate_workflow: site-validate-dev.yml
build-and-deploy:
name: '🌐 Deploy Landing Site'
needs: [guard]
runs-on: ubuntu-latest
concurrency:
group: gh-pages-deploy
cancel-in-progress: false
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: 🐍 Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ vars.PYTHON_VERSION || '3.12' }}
- name: 📬 Sync newsletter from Buttondown
env:
BUTTONDOWN_API_KEY: ${{ secrets.BUTTONDOWN_API_KEY }}
run: |
pip install -r site/newsletter/requirements.txt
python3 site/newsletter/bin/news pull
echo "✅ Newsletter posts synced"
- name: 🔧 Setup Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: 📊 Install stats dependencies
run: pip install -r site/scripts/requirements.txt
- name: 🔨 Build unified site
working-directory: site
env:
# See site-validate-dev.yml for why the token matters here: the
# pre-render stats step calls the GitHub search API, which is
# rate-limited to 10 req/min per IP when unauthenticated.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUTTONDOWN_API_KEY: ${{ secrets.BUTTONDOWN_API_KEY }}
# Readership from the GA4 property behind the Looker Studio report.
# Absent the secret the build keeps the committed figures rather than
# failing, so a credential lapse degrades quietly instead of blocking
# a deploy. Deliberately not set in site-validate-dev.yml, which runs
# on pull_request and would expose it to forks.
GA4_SERVICE_ACCOUNT_JSON: ${{ secrets.GA4_SERVICE_ACCOUNT_JSON }}
run: |
quarto render
touch _build/.nojekyll
echo "✅ Site rendered: $(find _build -name '*.html' | wc -l) pages"
- name: 🔍 Validate build
run: |
for page in index.html about/index.html community/index.html newsletter/index.html; do
if [ ! -f "site/_build/$page" ]; then
echo "❌ CRITICAL: $page missing from build. Aborting."
exit 1
fi
done
echo "✅ All site pages validated"
- name: 🚀 Deploy to gh-pages
run: |
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"
BUILD_DIR="${{ github.workspace }}/site/_build"
# Deploy subsites to their directories
for subsite in about community newsletter; do
rm -rf "$subsite"
cp -r "$BUILD_DIR/$subsite" .
done
# Deploy landing page to root (preserve directories managed by other workflows).
# site_libs MUST be deployed at root: the about/community/newsletter HTML
# references ../site_libs/... which resolves to /site_libs/ at runtime.
# Skipping it (as we did before 2026-04-28) leaves those subsites without
# Bootstrap/Quarto CSS — the navbar collapses to a raw <ul>, the hero
# disappears into 3000 px of empty whitespace, and dark mode is unstyled.
for item in "$BUILD_DIR"/*; do
basename="$(basename "$item")"
if [[ -d "$basename" ]] && [[ "$basename" =~ ^(book|kits|tinytorch|labs|mlsysim|slides|instructors|interviews|staffml|about|community|newsletter)$ ]]; then
continue
fi
rm -rf "./$basename"
cp -r "$item" .
done
touch .nojekyll
[ -f "index.html" ] || { echo "❌ CRITICAL: Landing index.html missing."; exit 1; }
git add .
git commit -m "🌐 Deploy site live - ${{ github.sha }}" --allow-empty || echo "🟡 Nothing to commit"
PUSHED=false
for i in 1 2 3; do
if git push origin gh-pages 2>/dev/null; then
echo "✅ Push succeeded on attempt $i"
PUSHED=true
break
fi
echo "⚠️ Push failed (attempt $i/3), pulling with rebase..."
git pull --rebase origin gh-pages || true
done
if [ "$PUSHED" != "true" ]; then
echo "❌ Push failed after 3 attempts."
exit 1
fi
echo "✅ Site deployed to mlsysbook.ai"
- name: 📊 Summary
run: |
echo "## 🌐 Landing Site Publish Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Page | URL |" >> $GITHUB_STEP_SUMMARY
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| 🏠 Landing | https://mlsysbook.ai/ |" >> $GITHUB_STEP_SUMMARY
echo "| 📋 About | https://mlsysbook.ai/about/ |" >> $GITHUB_STEP_SUMMARY
echo "| 🌍 Community | https://mlsysbook.ai/community/ |" >> $GITHUB_STEP_SUMMARY
echo "| 📬 Newsletter | https://mlsysbook.ai/newsletter/ |" >> $GITHUB_STEP_SUMMARY