ci: purge the Cloudflare cache after each deploy goes live #17
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@v4 | |
| - uses: actions/setup-node@v4 | |
| 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@v4 | |
| 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@v4 | |
| - uses: actions/download-artifact@v4 | |
| 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 | |
| # the origin is now serving the new build (previous step), but Cloudflare | |
| # caches HTML for max-age=600 — purge so visitors see the deploy at once. | |
| # Assets are content-hashed, so purge_everything is safe. | |
| 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 | |
| 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 '{"purge_everything":true}') | |
| if echo "$response" | grep -q '"success": *true'; then | |
| echo "Cloudflare cache purged" | |
| else | |
| echo "::error::Cloudflare purge failed: $response" | |
| exit 1 | |
| fi |