diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 3c6d666..7498f3e 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -128,6 +128,33 @@ jobs: # wrong, too-low value rather than failing (ci-requirements.md). fetch-depth: 0 + # BUILD_REF is what makes a scheduled run build `main` rather than the + # branch its cron fired from — and it is read through the `env` context + # inside `with:`, which this runner is NOT known to evaluate. If it does + # not, checkout silently falls back to the triggering ref and the weekly + # refresh publishes DEV's source to `:latest`, which is production. + # Every lane would stay green; the first sign of it would be production + # running code that was never merged. + # + # So assert the checkout instead of trusting the expression. A red + # weekly job is a fine outcome. Shipping dev to production is not. + # + # `if:` reads the `github` context, which the runner demonstrably does + # evaluate — this file already gates steps on it — so the guard cannot + # be disabled by the same uncertainty it exists to cover. + - name: Guard — a scheduled run must have checked out main + if: github.event_name == 'schedule' + run: | + set -eu + BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "schedule: HEAD is $BRANCH ($(git rev-parse --short HEAD))" + if [ "$BRANCH" != "main" ]; then + echo "schedule: expected main, got '$BRANCH'." >&2 + echo "schedule: BUILD_REF was not honoured by the runner." >&2 + echo "schedule: refusing to publish a channel tag from it." >&2 + exit 1 + fi + # The version is DERIVED, not read from the repo (milestone 271 step 4, # cut over 2026-08-27). `packaging.sh version` returns `YYYY.M.D.HHMM` # UTC — the commit TIME of the newest change to a PACKAGED extension @@ -408,6 +435,20 @@ jobs: # that exists perfectly well under its real name. fetch-depth: 0 + # See sign-extension's copy for why this guard exists. + - name: Guard — a scheduled run must have checked out main + if: github.event_name == 'schedule' + run: | + set -eu + BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "schedule: HEAD is $BRANCH ($(git rev-parse --short HEAD))" + if [ "$BRANCH" != "main" ]; then + echo "schedule: expected main, got '$BRANCH'." >&2 + echo "schedule: BUILD_REF was not honoured by the runner." >&2 + echo "schedule: refusing to publish a channel tag from it." >&2 + exit 1 + fi + # --- derived values, one line (milestone 313) ------------------------ # These stopped being shadow output at step 3. `revision` decides # whether the build below runs at all and `version` is what the image @@ -907,6 +948,20 @@ jobs: # the build would otherwise notice. fetch-depth: 0 + # See sign-extension's copy for why this guard exists. + - name: Guard — a scheduled run must have checked out main + if: github.event_name == 'schedule' + run: | + set -eu + BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "schedule: HEAD is $BRANCH ($(git rev-parse --short HEAD))" + if [ "$BRANCH" != "main" ]; then + echo "schedule: expected main, got '$BRANCH'." >&2 + echo "schedule: BUILD_REF was not honoured by the runner." >&2 + echo "schedule: refusing to publish a channel tag from it." >&2 + exit 1 + fi + # --- derived values, one line (milestone 313) ------------------------ # These stopped being shadow output at step 3. `revision` decides # whether the build below runs at all and `version` is what the image @@ -1260,6 +1315,20 @@ jobs: # the build would otherwise notice. fetch-depth: 0 + # See sign-extension's copy for why this guard exists. + - name: Guard — a scheduled run must have checked out main + if: github.event_name == 'schedule' + run: | + set -eu + BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "schedule: HEAD is $BRANCH ($(git rev-parse --short HEAD))" + if [ "$BRANCH" != "main" ]; then + echo "schedule: expected main, got '$BRANCH'." >&2 + echo "schedule: BUILD_REF was not honoured by the runner." >&2 + echo "schedule: refusing to publish a channel tag from it." >&2 + exit 1 + fi + # --- derived values, one line (milestone 313) ------------------------ # These stopped being shadow output at step 3. `revision` decides # whether the build below runs at all and `version` is what the image diff --git a/ci-requirements.md b/ci-requirements.md index 330471c..016fdd3 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -178,7 +178,11 @@ 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. + that exists perfectly well. 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 + source to `:latest` with every lane green. - It **publishes only `:latest`.** `:c-` for main's HEAD already names the bytes that commit built; re-pushing it over refreshed layers would break the one tag rule 145 makes immutable, and it is the rollback unit.