From 4fa797596383d325b9698a135bb1479b256c783f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 25 Sep 2026 09:13:41 -0400 Subject: [PATCH] fix(ci): every publishing job builds the commit that fired the run, not the branch tip (#4427) BUILD_REF resolved to the branch name on ordinary runs, and each job's checkout re-resolves a branch when that job starts. A push that lands mid-run therefore moved the later jobs onto the new tip: - run 7499 signed 423275a's extension; - its build-web then checked out 83e1382, derived a version nobody had signed, and got a 404 on the download. The guard failed closed. A job without such a guard would have published a commit the run's lanes never tested. The lanes already check out github.sha by default. BUILD_REF is now github.sha on every trigger except the base refresh, which keeps `main` and its branch guard. The publish therefore builds exactly what the lanes passed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR --- .forgejo/workflows/build.yml | 24 +++++++++++++++++------- ci-requirements.md | 5 ++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a255774..1640059 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -142,6 +142,16 @@ concurrency: # Deriving it per job invites the two halves to disagree: sign-extension would # derive dev's extension version while build-web bundled main's, and the # release download would 404 on a version that exists perfectly well. +# +# On every other trigger it is the COMMIT that fired (`github.sha`), never the +# branch name. A branch is re-resolved by each job's checkout when that job +# starts, so a push landing mid-run moved the later jobs onto the new tip: run +# 7499 signed 423275a's extension, then build-web checked out 83e1382 (pushed +# while 7499 ran), derived a version nobody had signed, and 404'd on the +# download (#4427). The guard failed closed that time; a job without one would +# have published a commit the run's own lanes never tested — the lanes check +# out `github.sha` by default, so pinning here makes the publish build exactly +# what they passed. # IS THIS A BASE REFRESH? Asked in five places and previously spelled five # ways — `github.event_name == 'schedule'` in an `if:`, `$GITHUB_EVENT_NAME` in # one shell, an `EVENT:` env passed into another, and a bare expression on @@ -178,7 +188,7 @@ concurrency: # where a step-level `if:` needs the answer before any shell runs. env: IS_REFRESH: ${{ (github.event_name == 'schedule' || format('{0}', github.event.inputs.refresh) == 'true') && 'true' || 'false' }} - BUILD_REF: ${{ (github.event_name == 'schedule' || format('{0}', github.event.inputs.refresh) == 'true') && 'main' || github.ref }} + BUILD_REF: ${{ (github.event_name == 'schedule' || format('{0}', github.event.inputs.refresh) == 'true') && 'main' || github.sha }} # Requires repo secret RELEASE_TOKEN — a Forgejo PAT with scopes: # - write:package, read:package (for docker push to git.fabledsword.com) @@ -605,8 +615,8 @@ jobs: - uses: actions/checkout@v4 with: # Not the triggering ref — see the `env:` block at the top. On a - # scheduled refresh this is `main`; on everything else it is the ref - # that fired, so this is a no-op on every ordinary path. + # scheduled refresh this is `main`; on everything else it is the + # commit that fired, the same one the lanes above tested. ref: ${{ env.BUILD_REF }} # Full history is load-bearing, not a convenience: the version this # job signs is derived from the commit TIME of the newest packaged @@ -945,8 +955,8 @@ jobs: - uses: actions/checkout@v4 with: # Not the triggering ref — see the `env:` block at the top. On a - # scheduled refresh this is `main`; on everything else it is the ref - # that fired, so this is a no-op on every ordinary path. + # scheduled refresh this is `main`; on everything else it is the + # commit that fired, the same one the lanes above tested. ref: ${{ env.BUILD_REF }} # Full history: this job RE-DERIVES the extension version rather than # being handed it, and a depth-1 clone derives a wrong, too-low value @@ -2174,8 +2184,8 @@ jobs: - uses: actions/checkout@v4 with: # Not the triggering ref — see the `env:` block at the top. On a - # scheduled refresh this is `main`; on everything else it is the ref - # that fired, so this is a no-op on every ordinary path. + # scheduled refresh this is `main`; on everything else it is the + # commit that fired, the same one the lanes above tested. ref: ${{ env.BUILD_REF }} # Full history: this job derives its artifact's version from the # commit its shipped files last changed in (milestone 313). A diff --git a/ci-requirements.md b/ci-requirements.md index 963be1f..aba5b30 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -180,7 +180,10 @@ per `docs/process.md`'s "add deps to the image when used by >1 project". BUILD_REF` that every checkout in the file takes, rather than per job — otherwise `sign-extension` would derive dev's extension version while `build-web` bundled main's, and the release download would 404 on a version - that exists perfectly well. Every job then ASSERTS its checkout is `main` + that exists perfectly well. On every other trigger `BUILD_REF` is the + triggering COMMIT (`github.sha`), not the branch: a branch is re-resolved + per job, so a push landing mid-run used to move the publishing jobs onto a + commit the run's lanes never tested (run 7499, #4427). Every job then ASSERTS its checkout is `main` before doing anything, because `env` inside `with:` is not a context this runner is known to evaluate — if it silently resolved to empty, checkout would fall back to the triggering ref and the refresh would publish dev's