name: Build and Deploy to Cloudflare Pages on: push: branches: - main jobs: build: runs-on: ubuntu-latest container: # Same digest the Dockerfile pins: one pinned base image and the # same dependency list (script/bootstrap) for both the check build # and the deploy build. The one extra thing this job needs on top # of the Dockerfile is the Actions runner's own prerequisites -- # see the first step. # alpine 3.21, 2026-02-28 image: alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 defaults: run: # The default step shell is bash; this image has only busybox # sh, so say so explicitly rather than rely on a fallback. shell: sh steps: # This image is bare busybox+musl. act_runner executes JavaScript # actions (checkout, upload-artifact) with `node` *inside* the job # container and does not inject one, so node has to exist before # the first `uses:` step -- script/bootstrap runs too late. git is # needed for checkout's `submodules: recursive` (without it # checkout degrades to a tarball download that cannot do # submodules). An inline `run:` needs only a shell, so this step # works on the bare image. These apk packages resolve at run time # and are not hash-pinned; that gap is repo-wide (script/bootstrap # has it too) and is tracked in #19. - name: Install runner prerequisites run: apk add --no-cache nodejs git tar - name: Checkout # actions/checkout v4.2.2, 2026-02-28 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: submodules: recursive - name: Install build dependencies run: script/bootstrap - name: Build site run: script/test - name: Archive site run: tar -czf site.tar.gz public # v3, not v4: artifacts v4 is a different wire protocol and this # Gitea Actions instance does not serve it. That is what broke the # deploy in run 25 -- measured by running two otherwise identical # jobs on a branch, one ending in upload-artifact v4 (failed) and # one without that step (passed). Tracked in #20. This SHA is the # exact commit the mutable `@v3` used to resolve to, i.e. the code # that was already deploying this site, now pinned rather than # floating. - name: Upload artifact # actions/upload-artifact v3.2.1, 2026-08-09 uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 with: name: site path: site.tar.gz deploy: runs-on: ubuntu-latest needs: build # Publishing guard. This job spends CLOUDFLARE_API_TOKEN and creates a # real Cloudflare Pages deployment, so it must never run off main -- # not even if a branch is added to the push trigger above, deliberately # or by accident. Costs one line; the build job stays exercisable from # a branch without this job touching anything external. if: github.ref_name == 'main' container: # node 20.20.2-bookworm, 2026-08-09 image: node@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5 steps: # Must match the upload-artifact major above -- v4 artifacts and # v3 artifacts are different protocols and do not interoperate. # Like the upload above, this is the exact commit `@v3` used to # resolve to. - name: Download artifact # actions/download-artifact v3.0.2, 2026-08-09 uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a with: name: site - name: Extract site run: tar -xzf site.tar.gz # 4.86.0, not the 4.120.0 that `latest` points at: wrangler # 4.120.0 requires node >= 22 and refuses to start on this # container's node 20. Note that the unpinned `npm install -g # wrangler` this replaces was never installing `latest` either -- # npm picks the newest version whose engines the running node # satisfies, which on node 20 is exactly 4.86.0. So this pins the # version that has actually been deploying this site, rather than # silently changing it. Moving the container to node 22 so the # wrangler pin can advance is tracked in #21. - name: Install Wrangler # wrangler 4.86.0, 2026-08-09 run: npm install -g wrangler@4.86.0 - name: Deploy to Cloudflare Pages run: wrangler pages deploy public --project-name=lora-vegas --branch=${{ github.ref_name }} env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} notify: runs-on: ubuntu-latest needs: [build, deploy] # Run whether the build and deploy passed or failed. The Notify step # carries continue-on-error: true so a notification that cannot be sent # leaves the deploy's result exactly as it was. if: always() container: # node 20.20.2-bookworm, 2026-08-09. Same pinned image as the deploy # job, chosen because it ships curl; no extra packages to install. image: node@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5 steps: # needs.deploy.result is "success" only when the deploy job actually # published; anything else -- a failed build or a failed deploy -- is # reported as a failure with a link to the run. - name: Notify # curl failing (ntfy unreachable or erroring, or NTFY_TOPIC # absent) must not fail the run, so continue-on-error lives on # this step and not on the job: on this Gitea a job-level flag # still marks the job failed and reddens the commit. continue-on-error: true env: RESULT: ${{ needs.deploy.result }} NTFY_TOPIC: ${{ secrets.NTFY_TOPIC }} run: | short="$(printf '%.7s' "$GITHUB_SHA")" run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" if [ "$RESULT" = success ]; then curl -fsS --max-time 10 \ -H "Title: lora.vegas deployed from $GITHUB_REF_NAME" \ -d "$short deployed to https://lora.vegas/" \ "https://ntfy.sh/$NTFY_TOPIC" else curl -fsS --max-time 10 \ -H "Title: lora.vegas deploy from $GITHUB_REF_NAME failed" \ -H "Priority: high" \ -d "Run: $run_url" \ "https://ntfy.sh/$NTFY_TOPIC" fi