fix(ci): reject kernel tags cut from a stale checkout #27

Merged
zach merged 1 commit from fix/stale-tag-guard into main 2026-07-25 19:04:50 +00:00
Owner

Problem

Tagging without pulling first produces a valid tag on an old commit, and build-kernel.yml then goes green while silently losing two properties:

  1. Wrong seed pin. The build uses configs/seeds/<X.Y>.toml as of the tagged commit. Seed bumps land on main as merged seed-bump.yml PRs between builds, so a stale tag can build the previous baseline — or a cross-series fallback seed after main has already advanced to a real same-line seed. The seed sets every un-pinned hardening default, so configs/intent.config stays satisfied and the fidelity assertion catches nothing.
  2. Drift check skipped. build-kernel.yml resolves the hardening-drift baseline from releases/ in the tag's own tree, and each release dir arrives via the publish: commit that follows its build. A tag predating the first publish of its line finds no baseline and logs skipping — so the one check that would have caught (1) is disabled by the same mistake.

v7.1.5-hardened1 hit exactly this. Cut from 9bffa1b, it is missing 8 commits including both seed bumps: still pinned to 7.0.12.hardened1-1 (the cross-series fallback) while main carries the real 7.1.4.hardened1-1 seed anthraxx finally shipped. Its tree has no releases/v7.1.* dir, so drift checking is skipped.

Change

  • build-kernel.yml — new assert tag is current with main step after checkout, asserting origin/main is an ancestor of the tagged commit. On failure it prints the missing commits and the exact delete-and-re-cut recipe.

    Hard failure rather than a warning: both symptoms are invisible in a green log, and a ::warning:: scrolls past in a 40-minute build. Repo variable ALLOW_STALE_TAG=1 downgrades it to a warning for a deliberate rebuild of an old commit, mirroring the existing REQUIRE_NET_ISOLATION escape hatch.

    The main ref is fetched with an explicit destination refspec (+refs/heads/main:refs/remotes/origin/main) rather than the bare git fetch origin main, because actions/checkout running against a tag may configure a tag-only fetch refspec, which would leave refs/remotes/origin/main absent.

  • cve-watch.yml — the tracking-issue body said "tag and push" with no pull, which is what caused this. It now leads with git pull --ff-only in a fenced block, explains why the pull is load-bearing, and carries the recovery steps.

  • docs/operators/config-architecture.md — same fix to the routine-bump recipe.

Verification

  • Ancestry logic self-tested both directions against real refs: v7.1.5-hardened1 → stale, guard fires; a temp tag at origin/main → current, guard passes.
  • Confirmed the explicit refspec populates origin/main.
  • yamllint -d relaxed .forgejo/workflows/ exits 0; both workflow-expressions greps from validate.yml pass (no expression templates in the new comments — the failure mode that once broke cve-watch.yml); shellcheck build/*.sh tools/*.sh clean.
  • Simulated the cve-watch issue body end-to-end with template expressions substituted, confirming literal backticks and correct markdown.

Follow-up (not in this PR)

configs/seeds/7.1.toml on main is now pinned to 7.1.4.hardened1-1, but its header comment still claims anthraxx "has not published a 7.1 config.x86_64 yet" and that the line is seeded from 7.0.12. bump-seed.sh rewrites the pin values but not the surrounding prose (it also collapsed the commit/tag key alignment). Worth a separate fix.

🤖 Generated with Claude Code

## Problem Tagging without pulling first produces a valid tag on an old commit, and `build-kernel.yml` then goes **green** while silently losing two properties: 1. **Wrong seed pin.** The build uses `configs/seeds/<X.Y>.toml` as of the tagged commit. Seed bumps land on `main` as merged `seed-bump.yml` PRs between builds, so a stale tag can build the previous baseline — or a cross-series fallback seed after `main` has already advanced to a real same-line seed. The seed sets every *un-pinned* hardening default, so `configs/intent.config` stays satisfied and the fidelity assertion catches nothing. 2. **Drift check skipped.** [build-kernel.yml](.forgejo/workflows/build-kernel.yml) resolves the hardening-drift baseline from `releases/` *in the tag's own tree*, and each release dir arrives via the `publish:` commit that follows its build. A tag predating the first publish of its line finds no baseline and logs `skipping` — so the one check that would have caught (1) is disabled by the same mistake. `v7.1.5-hardened1` hit exactly this. Cut from `9bffa1b`, it is missing 8 commits including both seed bumps: still pinned to `7.0.12.hardened1-1` (the cross-series fallback) while `main` carries the real `7.1.4.hardened1-1` seed anthraxx finally shipped. Its tree has no `releases/v7.1.*` dir, so drift checking is skipped. ## Change - **`build-kernel.yml`** — new `assert tag is current with main` step after checkout, asserting `origin/main` is an ancestor of the tagged commit. On failure it prints the missing commits and the exact delete-and-re-cut recipe. Hard failure rather than a warning: both symptoms are invisible in a green log, and a `::warning::` scrolls past in a 40-minute build. Repo variable `ALLOW_STALE_TAG=1` downgrades it to a warning for a deliberate rebuild of an old commit, mirroring the existing `REQUIRE_NET_ISOLATION` escape hatch. The `main` ref is fetched with an explicit destination refspec (`+refs/heads/main:refs/remotes/origin/main`) rather than the bare `git fetch origin main`, because `actions/checkout` running against a tag may configure a tag-only fetch refspec, which would leave `refs/remotes/origin/main` absent. - **`cve-watch.yml`** — the tracking-issue body said "tag and push" with no pull, which is what caused this. It now leads with `git pull --ff-only` in a fenced block, explains why the pull is load-bearing, and carries the recovery steps. - **`docs/operators/config-architecture.md`** — same fix to the routine-bump recipe. ## Verification - Ancestry logic self-tested both directions against real refs: `v7.1.5-hardened1` → stale, guard fires; a temp tag at `origin/main` → current, guard passes. - Confirmed the explicit refspec populates `origin/main`. - `yamllint -d relaxed .forgejo/workflows/` exits 0; both `workflow-expressions` greps from `validate.yml` pass (no expression templates in the new comments — the failure mode that once broke `cve-watch.yml`); `shellcheck build/*.sh tools/*.sh` clean. - Simulated the `cve-watch` issue body end-to-end with template expressions substituted, confirming literal backticks and correct markdown. ## Follow-up (not in this PR) `configs/seeds/7.1.toml` on `main` is now pinned to `7.1.4.hardened1-1`, but its header comment still claims anthraxx "has not published a 7.1 config.x86_64 yet" and that the line is seeded from 7.0.12. `bump-seed.sh` rewrites the pin values but not the surrounding prose (it also collapsed the `commit`/`tag` key alignment). Worth a separate fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ci): reject kernel tags cut from a stale checkout
All checks were successful
validate / shellcheck (pull_request) Successful in 12s
validate / yamllint (pull_request) Successful in 12s
validate / workflow-expressions (pull_request) Successful in 4s
validate / pycompile (pull_request) Successful in 4s
validate / intent-matches-policy (pull_request) Successful in 5s
validate / dep-list-parity (pull_request) Successful in 4s
validate / no-placeholder-digests (pull_request) Has been skipped
3cd8e59789
Tagging without pulling first produces a valid tag on an old commit, and
build-kernel.yml then goes GREEN while silently losing two properties:

  1. It uses the configs/seeds/<X.Y>.toml pin as of the tagged commit, not
     the current one. Seed bumps land on main as merged seed-bump.yml PRs
     between builds, so a stale tag can build the previous baseline -- or a
     cross-series fallback seed after main has already advanced to a real
     same-line seed. The seed sets every UN-pinned hardening default, so the
     fidelity assertion stays satisfied and catches nothing.
  2. It skips the hardening-drift guard. That step resolves its baseline
     from releases/ in the tag's own tree, and each release dir arrives via
     the `publish:` commit following its build -- so a tag predating the
     first publish of its line finds no baseline and logs "skipping". The
     one check that would have caught (1) is disabled by the same mistake.

v7.1.5-hardened1 hit exactly this: cut from 9bffa1b, it is missing both
seed bumps (still pinned to the 7.0.12 fallback while main carries the real
7.1.4 seed) and has no 7.1 release dir, so drift checking is skipped.

Add an ancestry assertion right after checkout. Hard failure, not a warning:
both symptoms are invisible in a green log and a WARNING scrolls past in a
40-minute build. Repo variable ALLOW_STALE_TAG=1 downgrades it to a warning
for a deliberate rebuild of an old commit, mirroring REQUIRE_NET_ISOLATION.

Also close the loop on the instructions that caused it -- the cve-watch
tracking issue and the operator bump recipe both said "tag and push" with
no pull. Both now lead with `git pull --ff-only` and carry the delete-and-
re-cut recovery steps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zach merged commit 1d530c28b0 into main 2026-07-25 19:04:50 +00:00
zach deleted branch fix/stale-tag-guard 2026-07-25 19:04:50 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
unredacted/linux-hardened-unredacted!27
No description provided.