Skip to content

Merge feat/site-stats-countup: count impact figures up on scroll into… #214

Merge feat/site-stats-countup: count impact figures up on scroll into…

Merge feat/site-stats-countup: count impact figures up on scroll into… #214

name: '🎯 StaffML · 👁️ Preview (Dev)'
# =============================================================================
# StaffML — Dev Preview Deploy
# =============================================================================
#
# Builds the StaffML Next.js interview-prep app and deploys to the dev
# preview site via SSH. Validation is delegated to the reusable workflows
# staffml-validate-dev.yml + staffml-validate-vault.yml — they run as
# `uses:` jobs in this workflow, and the deploy job blocks on `needs:`
# both passing. This closes the race window where Preview could deploy
# on a SHA the parallel Validate workflows had already failed on.
#
# Job graph (parallel where possible):
# ┌── validate-dev (uses: staffml-validate-dev.yml)
# ├── validate-vault (uses: staffml-validate-vault.yml)
# ├── build (Preview-specific Next.js static export)
# └── deploy (needs: validate-dev + validate-vault + build)
#
# The validate jobs and build run in parallel — wall-clock for the typical
# push is max(validate, build) + ~1 min for SSH deploy, basically
# unchanged from the previous single-job design.
#
# Triggers:
# - push: dev branch, interviews/staffml/** + vault questions/chains/schema
# - workflow_dispatch: manual
#
# Deploys to: harvard-edge.github.io/{DEV_REPO}/staffml/
# Secrets: SSH_DEPLOY_KEY (deploy job only)
# Vars: DEV_REPO_URL
#
# Related:
# - staffml-validate-dev.yml — Reusable site validate (called above)
# - staffml-validate-vault.yml — Reusable vault validate (called above)
# - staffml-publish-live.yml — Production deploy to mlsysbook.ai/staffml/
# - staffml-auto-pr.yml — Auto-PR from community question issues
# - staffml-welcome.yml — Welcome comment on contributor PRs
#
# =============================================================================
on:
workflow_dispatch:
push:
branches: [dev]
paths:
- 'interviews/staffml/**'
# Also redeploy when YAMLs or chains change — the build job
# regenerates corpus.json + corpus-summary.json from YAMLs before
# building, so the dev site always reflects current vault state.
- 'interviews/vault/questions/**'
- 'interviews/vault/chains.json'
- 'interviews/vault/schema/**'
# Re-run on edits to this workflow or the reusable validate
# workflows it calls — without these paths, a CI fix to any
# staffml workflow doesn't re-run the badge-driving job, so
# the README badge stays red until the next unrelated push to
# interviews/staffml/**.
- '.github/workflows/staffml-preview-dev.yml'
- '.github/workflows/staffml-validate-dev.yml'
- '.github/workflows/staffml-validate-vault.yml'
# vault-cli is the CLI that the validate-vault job (called here) lints.
# A fix to it — e.g. a ruff import-sort — must also re-run this
# badge-driving workflow, or the README badge stays red after the fix
# lands. That happened: PR #1879 sorted vault-cli imports and validate-vault
# went green, but preview-dev never re-ran, so the badge stayed red.
- 'interviews/vault-cli/**'
permissions:
contents: read
actions: read
concurrency:
group: staffml-dev-deploy
cancel-in-progress: true
env:
# Single source for paths and versions — see docs/CI-VARIABLES.md.
# The `paths:` trigger filter at the top of this file is intentionally
# NOT vars-ified (GitHub Actions evaluates triggers at workflow-load
# time, before vars are resolved). If STAFFML_ROOT moves, update the
# paths filter manually.
STAFFML_ROOT: ${{ vars.STAFFML_ROOT || 'interviews/staffml' }}
VAULT_DIR: ${{ vars.VAULT_DIR || 'interviews/vault' }}
VAULT_CLI_DIR: ${{ vars.VAULT_CLI_DIR || 'interviews/vault-cli' }}
DEV_STAFFML_PATH: ${{ vars.DEV_STAFFML_PATH || 'staffml' }}
NODE_VERSION: ${{ vars.NODE_VERSION || '20' }}
PYTHON_VERSION: ${{ vars.PYTHON_VERSION || '3.12' }}
jobs:
# ===========================================================================
# Validate — reusable workflows, run in parallel with build.
# ===========================================================================
# Site validate: tsc + tests + build + Playwright E2E + vault smoke + links.
validate-dev:
name: '✅ Validate (Dev)'
uses: ./.github/workflows/staffml-validate-dev.yml
# Vault validate: vault-cli ruff/mypy/pytest + vault check --strict +
# codegen drift + registry append-only + exemplar coverage + worker vitest.
validate-vault:
name: '✅ Validate (Vault)'
uses: ./.github/workflows/staffml-validate-vault.yml
# ===========================================================================
# Build — Preview-specific Next.js static export, runs in parallel.
# ===========================================================================
# Uses Preview-specific env (BASE_PATH for the dev subdirectory, hosted
# interviewer endpoint, hosted vault API) so the artifact is exactly
# what gets shipped. The validate-dev job builds with its own validation
# env vars and throws away the artifact — that build proves the code
# compiles, this build produces the deployable bytes.
build:
name: '🔨 Build StaffML'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: 🔧 Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: ${{ env.STAFFML_ROOT }}/package-lock.json
- name: 📦 Install dependencies
working-directory: ${{ env.STAFFML_ROOT }}
run: npm ci
- name: 🐍 Setup Python (for vault-cli)
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: 🛠️ Install vault-cli
run: pip install -e "$VAULT_CLI_DIR/"
- name: 🔄 Regenerate corpus from YAMLs (vault build --local-json)
# Emits $STAFFML_ROOT/src/data/corpus.json (full) +
# corpus-summary.json (site bundle) from the committed YAMLs.
# Guarantees the built site reflects current vault state even if
# the committed JSON artifacts drift from YAMLs.
run: vault build --vault-dir "$VAULT_DIR" --release-id preview-dev --local-json
- name: 🔨 Build StaffML
working-directory: ${{ env.STAFFML_ROOT }}
env:
NEXT_PUBLIC_BASE_PATH: /cs249r_book_dev/${{ env.DEV_STAFFML_PATH }}
NEXT_PUBLIC_ECOSYSTEM_BASE: https://harvard-edge.github.io/cs249r_book_dev
NEXT_PUBLIC_ANALYTICS_URL: ${{ vars.PRODUCTION_DOMAIN || 'https://mlsysbook.ai' }}/api/staffml-analytics
# Preview builds share the production interviewer worker. This
# means preview PRs can exercise the real hosted AskInterviewer
# panel, and any rate-limit hits on preview count against the
# same global budget as production. The CORS allowlist on the
# worker already includes localhost plus the production origins.
NEXT_PUBLIC_INTERVIEWER_ENDPOINT: ${{ vars.PRODUCTION_DOMAIN || 'https://mlsysbook.ai' }}/api/staffml-interviewer
# Preview shares the production worker (same D1 data).
NEXT_PUBLIC_VAULT_API: ${{ vars.STAFFML_VAULT_WORKER_URL || 'https://staffml-vault.mlsysbook-ai-account.workers.dev' }}
NEXT_PUBLIC_VAULT_RELEASE: "1.0.2"
run: npm run build
- name: 📁 Inject paper and figures into build
run: |
echo "📁 Injecting paper assets into built site..."
mkdir -p "$STAFFML_ROOT/out/downloads"
if [ -f interviews/paper/paper.pdf ]; then
cp interviews/paper/paper.pdf "$STAFFML_ROOT/out/downloads/StaffML-Paper.pdf"
echo "✅ Injected StaffML-Paper.pdf"
else
echo "⚠️ Paper PDF not found"
fi
echo ""
echo "📦 Downloads folder contents:"
ls -la "$STAFFML_ROOT/out/downloads/" || echo "No downloads folder"
- name: 🔗 Rewrite URLs for dev site
run: bash .github/scripts/rewrite-dev-urls.sh "$DEV_STAFFML_PATH" "$STAFFML_ROOT/out"
- name: 🔍 Validate build output
# Light sanity check on the artifact about to be uploaded. Heavier
# checks (corpus invariants, vault integrity, E2E smoke, link
# check, schema drift) live in the called validate workflows;
# this just confirms the critical pages were emitted by the
# build before we hand it to the deploy job.
run: |
if [ ! -f "$STAFFML_ROOT/out/index.html" ]; then
echo "❌ CRITICAL: index.html missing. Aborting deployment."
exit 1
fi
PAGE_COUNT=$(find "$STAFFML_ROOT/out" -name '*.html' | wc -l)
echo "✅ StaffML built: $PAGE_COUNT pages"
echo "📊 Build size: $(du -sh "$STAFFML_ROOT/out" | cut -f1)"
# Verify critical pages exist.
# next.config.mjs sets `trailingSlash: true`, so /<page> emits
# out/<page>/index.html (not out/<page>.html). The 404 page is the
# one exception — Next.js still ships it flat as out/404.html so
# GitHub Pages can serve it as the not-found template.
MISSING=0
for page in practice gauntlet progress about; do
if [ ! -f "$STAFFML_ROOT/out/${page}/index.html" ]; then
echo "❌ MISSING: ${page}/index.html"
MISSING=$((MISSING + 1))
fi
done
if [ ! -f "$STAFFML_ROOT/out/404.html" ]; then
echo "❌ MISSING: 404.html"
MISSING=$((MISSING + 1))
fi
if [ "$MISSING" -gt 0 ]; then
echo "❌ $MISSING critical pages missing. Aborting."
exit 1
fi
# Show vault release identity
MANIFEST="$STAFFML_ROOT/src/data/vault-manifest.json"
if [ -f "$MANIFEST" ]; then
echo "📋 Vault release: v$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['releaseId'])" "$MANIFEST")"
fi
- name: 📤 Upload build artifact
uses: actions/upload-artifact@v7
with:
name: staffml-dev-build
path: ${{ env.STAFFML_ROOT }}/out
retention-days: 1
if-no-files-found: error
# ===========================================================================
# Deploy — gated on validates + build all succeeding.
# ===========================================================================
# The job graph means this never starts unless every validate workflow
# and the Preview-specific build all reached `success`. The screenshot
# case "Preview ✅ but Validate ❌ on the same SHA" can no longer occur:
# if either validate fails, this job is skipped and Preview as a whole
# reports failure.
deploy:
name: '🚀 Deploy StaffML to Dev Site'
runs-on: ubuntu-latest
needs: [validate-dev, validate-vault, build]
timeout-minutes: 10
steps:
- name: 📥 Download build artifact
uses: actions/download-artifact@v8
with:
name: staffml-dev-build
path: staffml-out
- name: 🚀 Deploy to Dev Site via SSH
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
run: |
echo "🔐 Starting ssh-agent..."
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"
echo "🔄 Cloning dev preview repository..."
git clone --depth=1 ${{ vars.DEV_REPO_URL }} target-repo
cd target-repo
echo "🧹 Cleaning staffml directory..."
rm -rf staffml
mkdir -p staffml
echo "🚚 Copying StaffML build..."
cp -r "${{ github.workspace }}/staffml-out/." staffml/
if [ ! -f "staffml/index.html" ]; then
echo "❌ CRITICAL: staffml/index.html missing. Aborting."
exit 1
fi
echo "📦 Committing and pushing..."
git add .
git commit -m "🎯 Deploy StaffML dev to /staffml/ from ${{ github.sha }}" --allow-empty || echo "🟡 Nothing to commit"
# Retry push with rebase to handle concurrent deploys
for i in 1 2 3; do
if git push origin main 2>/dev/null; then
echo "✅ Push succeeded on attempt $i"
break
fi
echo "⚠️ Push failed (attempt $i/3), pulling with rebase..."
git pull --rebase origin main || true
done
echo "✅ StaffML deployed to dev preview"