Merge feat/site-stats-countup: count impact figures up on scroll into… #214
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: '🎯 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" |