bench: a checksum that cannot be computed must not count as a pass #73
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: CI | |
| on: | |
| push: | |
| branches: [master] | |
| pull_request: | |
| workflow_dispatch: | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| cache: yarn | |
| - name: Install packages | |
| run: yarn install --frozen-lockfile | |
| - name: Build | |
| run: yarn build | |
| - name: Check the static files survived the build | |
| # CNAME keeps the custom domain, 404.html is the SPA shell that serves | |
| # unprerendered deep paths, and api/ is the gpu.js API reference served | |
| # at https://gpu.rocks/api/ | |
| run: | | |
| for file in index.html CNAME 404.html api/index.html manifest.json service-worker.js; do | |
| if [ ! -f "dist/$file" ]; then | |
| echo "::error::dist/$file is missing from the build" | |
| exit 1 | |
| fi | |
| echo "ok dist/$file" | |
| done | |
| - name: Serve the build | |
| run: | | |
| yarn preview --port 4173 & | |
| for _ in $(seq 1 30); do | |
| curl -sf http://localhost:4173/ >/dev/null && exit 0 | |
| sleep 1 | |
| done | |
| echo "::error::preview server never came up" | |
| exit 1 | |
| - name: Smoke test every route in a browser | |
| run: yarn test:smoke http://localhost:4173 | |
| - name: Keep the tested build for the deploy job | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist | |
| retention-days: 7 | |
| deploy: | |
| # publishes the exact build the smoke test passed against, rather than | |
| # rebuilding, so what ships is what was verified | |
| needs: build | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/master' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: deploy-gh-pages | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist | |
| - name: Publish to the gh-pages branch | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name 'github-actions[bot]' | |
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | |
| # gh-pages publishes from its own clone, which does not inherit the | |
| # credentials actions/checkout writes into this working copy | |
| npx --yes gh-pages@6 --dist dist --dotfiles \ | |
| --repo "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ | |
| --message "Deploy ${GITHUB_SHA::7}: $(git log -1 --pretty=%s)" | |
| - name: Wait for the deploy to go live | |
| # compares the served page against what was just published rather than | |
| # looking for an asset name, so an HTML-only change is verified too | |
| run: | | |
| for _ in $(seq 1 30); do | |
| if curl -sf "https://gpu.rocks/?cachebust=$RANDOM" -o /tmp/live.html \ | |
| && diff -q dist/index.html /tmp/live.html >/dev/null; then | |
| curl -sf -o /dev/null "https://gpu.rocks/api/" \ | |
| && echo "live, and /api/ is still served" && exit 0 | |
| echo "::error::the site is live but /api/ is not being served" | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| echo "::error::gpu.rocks is not serving the page that was just published" | |
| diff dist/index.html /tmp/live.html || true | |
| exit 1 | |
| - name: Purge the Cloudflare cache (HTML only) | |
| # HTML must be purged: it names the new content-hashed chunks, and a | |
| # stale page would ask for assets this deploy replaced. The assets | |
| # themselves must NOT be — their names change whenever their bytes do, | |
| # so they are immutable, and purge_everything just forces every visitor | |
| # to re-pull ~2 MB from GitHub Pages cold. Doing that on back-to-back | |
| # deploys is what turned this site into 8-17 s origin fetches and edge | |
| # 520s. Cloudflare takes at most 30 URLs per call, hence the batching. | |
| env: | |
| CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: | | |
| if [ -z "$CLOUDFLARE_ZONE_ID" ] || [ -z "$CLOUDFLARE_API_TOKEN" ]; then | |
| echo "Cloudflare secrets not configured — skipping purge (stale HTML expires within ~10 min)" | |
| exit 0 | |
| fi | |
| # Every prerendered page, in both the slashless and trailing-slash | |
| # forms GitHub Pages serves — Cloudflare keys the cache on the exact | |
| # URL, so both need purging. Only index.html files name real URLs: | |
| # prerender also writes a <route>.html twin so GitHub Pages serves the | |
| # slashless path without a redirect, but nobody requests that name — | |
| # purging it would be 95 wasted origin fetches. dist/api is the | |
| # vendored gpu.js reference: it changes only on a version bump, is not | |
| # coupled to our hashed chunks, and can expire on its own max-age. | |
| find dist -name 'index.html' -not -path 'dist/api/*' | while read -r file; do | |
| rel=${file#dist/} | |
| if [ "$rel" = "index.html" ]; then | |
| echo "https://gpu.rocks/" | |
| else | |
| dir=${rel%/index.html} | |
| echo "https://gpu.rocks/$dir" | |
| echo "https://gpu.rocks/$dir/" | |
| fi | |
| done | sort -u > /tmp/purge-urls.txt | |
| # The Markdown twins (<route>.md) and the llms.txt pair carry the same | |
| # prose as the HTML and are just as un-hashed, so they go stale the | |
| # same way. dist/api is excluded for the reason above (it ships its | |
| # own README.md). | |
| find dist -name '*.md' -not -path 'dist/api/*' | while read -r file; do | |
| echo "https://gpu.rocks/${file#dist/}" | |
| done | sort -u >> /tmp/purge-urls.txt | |
| printf '%s\n' \ | |
| "https://gpu.rocks/404.html" \ | |
| "https://gpu.rocks/sitemap.xml" \ | |
| "https://gpu.rocks/robots.txt" \ | |
| "https://gpu.rocks/llms.txt" \ | |
| "https://gpu.rocks/llms-full.txt" \ | |
| "https://gpu.rocks/version.json" >> /tmp/purge-urls.txt | |
| # version.json is the one file that MUST be purged. It is what an open | |
| # tab polls to notice it has gone stale, it is not content-hashed, and | |
| # it is cached (cf-cache-status HIT, max-age=14400) — so leaving it out | |
| # meant a deploy could go unannounced for four hours, which is exactly | |
| # the failure the update banner exists to prevent. Caught by a served | |
| # build id disagreeing with the one just built. | |
| echo "purging $(wc -l < /tmp/purge-urls.txt) HTML + Markdown URLs (assets left warm)" | |
| split -l 30 /tmp/purge-urls.txt /tmp/purge-batch- | |
| failed=0 | |
| for batch in /tmp/purge-batch-*; do | |
| payload=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | {files: .}' < "$batch") | |
| response=$(curl -s -X POST \ | |
| "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \ | |
| -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| --data "$payload") | |
| if echo "$response" | grep -q '"success": *true'; then | |
| echo " purged $(wc -l < "$batch") URLs" | |
| else | |
| echo "::error::Cloudflare purge failed: $response" | |
| failed=1 | |
| fi | |
| done | |
| [ "$failed" -eq 0 ] || exit 1 | |
| echo "Cloudflare HTML cache purged" |