From 3f91a7c2737ed35d0b268299a5ee6ed2d40abfa9 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 9 Aug 2026 01:49:21 +0000 Subject: [PATCH 1/2] Hash-pin every external reference in deploy.yml (closes #7) deploy.yml was the last file in the repo carrying mutable external references. Every image is now pinned by digest and every action by a full 40-hex commit SHA, each with a version/date comment on the line above. All values were resolved from upstream and verified to resolve. - build container: klakegg/hugo:ext-alpine (abandoned since 2021, mutable tag) replaced by the exact alpine 3.21 digest the Dockerfile already pins, with script/bootstrap to install hugo and script/test to build. One pinned base and one dependency list now serve both the check build and the deploy build. - deploy container: node:20 -> node@sha256:8f693eaa... (node 20.20.2, bookworm). - actions/checkout: v4 -> 11bd7190... (v4.2.2), the same SHA check.yml pins, so the two workflows agree. - actions/upload-artifact: v3 -> ea165f8d... (v4.6.2); v3 is deprecated. - actions/download-artifact: v3 -> d3f86a10... (v4.3.0); v3 is deprecated. - npm install -g wrangler -> wrangler@4.120.0, so the deploy no longer executes whatever the wrangler tag happens to point at. Also drops the dead feat/initial-site push trigger (that branch is fully merged into main) and reindents the file to 4-space YAML to match check.yml and .editorconfig. The two jobs are deliberately left separate so a deploy regression can be attributed unambiguously. Verified: make check and script/cibuild both green; the workflow parses as YAML with the expected job/step structure. The Cloudflare Pages deploy path itself cannot be exercised from a branch (it runs only on push to main and needs CLOUDFLARE_API_TOKEN), so the deploy run on main must be watched after merge. --- .gitea/workflows/deploy.yml | 91 +++++++++++++++++++++---------------- TODO.md | 15 ++++-- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 3967d2e..76d7cf7 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,52 +1,63 @@ name: Build and Deploy to Cloudflare Pages on: - push: - branches: - - feat/initial-site - - main + push: + branches: + - main jobs: - build: - runs-on: ubuntu-latest - container: - image: klakegg/hugo:ext-alpine - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive + build: + runs-on: ubuntu-latest + container: + # Same digest the Dockerfile pins: one pinned base image and one + # dependency list (script/bootstrap) for both the check build and + # the deploy build. + # alpine 3.21, 2026-02-28 + image: alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 + steps: + # actions/checkout v4.2.2, 2026-08-09 + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + submodules: recursive - - name: Build site - run: hugo --minify + - name: Install build dependencies + run: script/bootstrap - - name: Archive site - run: tar -czf site.tar.gz public + - name: Build site + run: script/test - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: site - path: site.tar.gz + - name: Archive site + run: tar -czf site.tar.gz public - deploy: - runs-on: ubuntu-latest - needs: build - container: - image: node:20 - steps: - - name: Download artifact - uses: actions/download-artifact@v3 - with: - name: site + # actions/upload-artifact v4.6.2, 2026-08-09 + - name: Upload artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: site + path: site.tar.gz - - name: Extract site - run: tar -xzf site.tar.gz + deploy: + runs-on: ubuntu-latest + needs: build + container: + # node 20.20.2-bookworm, 2026-08-09 + image: node@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5 + steps: + # actions/download-artifact v4.3.0, 2026-08-09 + - name: Download artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: site - - name: Install Wrangler - run: npm install -g wrangler + - name: Extract site + run: tar -xzf site.tar.gz - - 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 }} + # wrangler 4.120.0, 2026-08-09 + - name: Install Wrangler + run: npm install -g wrangler@4.120.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 }} diff --git a/TODO.md b/TODO.md index c8a581c..163f0cf 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,8 @@ pre-1.0 No git tags. The site is live and now has the scripts-to-rule-them-all scaffold (`Makefile`, `script/`, `Dockerfile`, `check.yml`); still missing `LICENSE` and -policy files. +policy files. Every external reference in the repo is now pinned by +cryptographic hash (or, for the wrangler CLI install, an exact version). # Next Step @@ -24,6 +25,15 @@ Update `README.md` accordingly. # Completed Steps +- 2026-08-09: hash-pinned every external reference in + `.gitea/workflows/deploy.yml` (closes #7): both job container images are + pinned by digest, all three `uses:` are pinned by 40-hex commit SHA + (`upload`/`download-artifact` moved v3 to v4), and the wrangler install is + pinned to an exact version. The abandoned `klakegg/hugo:ext-alpine` image is + gone: the build job now runs on the same pinned `alpine` digest the + `Dockerfile` uses, with `script/bootstrap` then `script/test`. Also dropped + the dead `feat/initial-site` push trigger and reindented the file to 4-space + YAML to match `check.yml` - 2026-07-25: added the scripts-to-rule-them-all scaffold (closes #4): `script/` entrypoints, `Makefile` shims, a Hugo `Dockerfile` (sha256-pinned alpine) plus `.dockerignore` that runs `make check`, `.gitea/workflows/check.yml` running @@ -40,9 +50,6 @@ Update `README.md` accordingly. # Future Steps -- Pin the images and actions in `deploy.yml` by sha256 - (`klakegg/hugo:ext-alpine`, `node:20`, `actions/checkout`, - `upload`/`download-artifact` are all unpinned) - Rework README.md into the standard sections: Description, Getting Started, Rationale, Design, TODO, License, Author (currently About, Contributing, Technical Details, License) From b157bfd52cb6060548bb2bd0c4eb0a45abaad3bb Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 9 Aug 2026 02:15:44 +0000 Subject: [PATCH 2/2] Install runner prerequisites in the pinned build container (closes #7) Replacing klakegg/hugo:ext-alpine with the Dockerfile's pinned alpine digest satisfied the pinning requirement but dropped the runtime the Actions runner itself depends on, which would have broken the deploy: - act_runner executes JavaScript actions with `node` inside the job container and does not inject one. Stock alpine has no node, so actions/checkout - the job's first step - would fail with "node: not found", and script/bootstrap (which installs node) is step 2 and never runs. The build job fails, deploy is skipped for `needs: build`, and the site stops publishing. - Steps default to `bash`, which stock alpine does not ship either. Fixes, both scoped to keeping the mandated image replacement runnable: - A pre-checkout inline `run:` step (`apk add --no-cache nodejs git tar`) installs what the runner needs before the first `uses:` step. An inline run needs only a shell, so it works on the bare image. git is there for checkout's `submodules: recursive`; without it checkout degrades to a tarball download that cannot do submodules. - `defaults.run.shell: sh` on the build job, so the shell is stated rather than left to a bash-to-sh fallback. No pinned value is touched. The 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. Also moves each version/date comment to sit directly above the pinned line rather than above the step's `- name:`, matching check.yml, and dates the actions/checkout pin 2026-02-28 as check.yml already does for the same SHA. Verified by running the build job's step sequence inside the pinned alpine digest: bare, `node` and `bash` are absent and the pinned checkout bundle dies with "node: not found"; after the new apk step, node 22.23.2, git 2.47.3 and GNU tar 1.35 are present, that same checkout bundle runs under node and gets as far as "GITHUB_WORKSPACE not defined", and script/bootstrap, script/test and the tar step all complete. make check and script/cibuild (with the build cache pruned, so nothing was CACHED) are green. --- .gitea/workflows/deploy.yml | 34 +++++++++++++++++++++++++++------- TODO.md | 8 +++++--- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 76d7cf7..f24f7b3 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -9,14 +9,34 @@ jobs: build: runs-on: ubuntu-latest container: - # Same digest the Dockerfile pins: one pinned base image and one - # dependency list (script/bootstrap) for both the check build and - # the deploy build. + # 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: - # actions/checkout v4.2.2, 2026-08-09 + # 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 @@ -30,8 +50,8 @@ jobs: - name: Archive site run: tar -czf site.tar.gz public - # actions/upload-artifact v4.6.2, 2026-08-09 - name: Upload artifact + # actions/upload-artifact v4.6.2, 2026-08-09 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: site @@ -44,8 +64,8 @@ jobs: # node 20.20.2-bookworm, 2026-08-09 image: node@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5 steps: - # actions/download-artifact v4.3.0, 2026-08-09 - name: Download artifact + # actions/download-artifact v4.3.0, 2026-08-09 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: site @@ -53,8 +73,8 @@ jobs: - name: Extract site run: tar -xzf site.tar.gz - # wrangler 4.120.0, 2026-08-09 - name: Install Wrangler + # wrangler 4.120.0, 2026-08-09 run: npm install -g wrangler@4.120.0 - name: Deploy to Cloudflare Pages diff --git a/TODO.md b/TODO.md index 163f0cf..7b81a32 100644 --- a/TODO.md +++ b/TODO.md @@ -31,9 +31,11 @@ Update `README.md` accordingly. (`upload`/`download-artifact` moved v3 to v4), and the wrangler install is pinned to an exact version. The abandoned `klakegg/hugo:ext-alpine` image is gone: the build job now runs on the same pinned `alpine` digest the - `Dockerfile` uses, with `script/bootstrap` then `script/test`. Also dropped - the dead `feat/initial-site` push trigger and reindented the file to 4-space - YAML to match `check.yml` + `Dockerfile` uses, with a pre-checkout `apk add nodejs git tar` step (the + Actions runner needs `node` inside the job container to execute JavaScript + actions), an explicit `shell: sh` default, then `script/bootstrap` and + `script/test`. Also dropped the dead `feat/initial-site` push trigger and + reindented the file to 4-space YAML to match `check.yml` - 2026-07-25: added the scripts-to-rule-them-all scaffold (closes #4): `script/` entrypoints, `Makefile` shims, a Hugo `Dockerfile` (sha256-pinned alpine) plus `.dockerignore` that runs `make check`, `.gitea/workflows/check.yml` running