fix(ci): key cve-watch off published releases, not bare tags #32

Merged
zach merged 1 commit from fix/cve-watch-published-oracle into fix/workflow-injection-and-http-status 2026-07-25 21:46:11 +00:00
Owner

Stacked on #29 (fix/workflow-injection-and-http-status). Both branches edit this file's issue body, so this targets that branch rather than handing you a conflict on the same lines. Merge #29 first; this PR's base then becomes main automatically, or retarget it.

The defect

cve-watch.yml compared the newest upstream tag against the newest local git tag, so the mere existence of a tag asserted "we already have this". But a tag can exist with nothing published:

  • the stale-tag guard from 3cd8e59 hard-fails — and that guard made this state common; it is exactly what v7.1.5-hardened1 is in right now
  • the fidelity assertion aborts
  • the drift guard fires
  • publish.yml was simply never dispatched

In every case upstream == local, so new=false, and the only automated "you are behind upstream on security patches" signal switched itself off — indefinitely, and precisely when a release had gone wrong. The failure mode silences the alarm it should be raising.

The fix

Key off releases/<tag>/manifest.json. publish.yml is what commits that file, so its presence is evidence that build-kernel.yml completed and publish ran. New tools/newest-published-release.sh does the resolution — in tools/ rather than inline so shellcheck build/*.sh tools/*.sh covers it and it's testable against fixture directories.

The newest git tag is still computed, now purely as a diagnostic: when a tag exists with no matching published release, the run emits a ::warning:: and the filed issue carries a "Stuck release" note. The watcher becomes a stuck-release detector, which is the natural place for that signal.

One deliberate behaviour change: the [[ -z $local ]] branch used to suppress filing, because a shallow checkout could yield zero git tags and make every upstream tag read as new — that was issue #4's false positive. Reading releases/ from the working tree removes that failure mode entirely, so an empty local now legitimately means "nothing has ever been published" and filing is correct. Reworded rather than left in place with an evaporated rationale.

Also relabelled the body's Local latest:Local newest PUBLISHED:, since that is now what the value means.

Verification

The oracle script, against the real repo and fixtures:

input result
real repo, no args v7.1.4-hardened1
--series 7.1 / --series 7.0 v7.1.4-hardened1 / v7.0.12-hardened1
fixture with a v7.2.0 dir and no manifest v7.1.4-hardened1 — correctly ignored
same after adding a manifest v7.2.0-hardened1
empty dir / nonexistent dir empty output, exit 0
--series 7 (malformed) exit 2
--series 7.0 with a v7.10.1 present empty — dot escaped, no false match

The rewired comparison, run offline with a stubbed curl against the real repo state (newest published v7.1.4, tag v7.1.5 exists unpublished):

simulated upstream new local stuck
v7.1.6-hardened1 true v7.1.4-hardened1 v7.1.5-hardened1
v7.1.5-hardened1 true v7.1.4-hardened1 v7.1.5-hardened1
v7.1.4-hardened1 false v7.1.4-hardened1 v7.1.5-hardened1

The middle row is the whole point: that is today's state, and before this change it produced new=false — silent forever.

Issue body rendered both ways (real step body extracted, templates substituted): with a stuck tag the note lands as its own paragraph; without one there is no stray whitespace.

Validators: shellcheck clean (including the new script), yamllint -d relaxed exit 0, both workflow-expressions greps clean, and the new run:-body template rule from #29 still passes.

🤖 Generated with Claude Code

> **Stacked on #29** (`fix/workflow-injection-and-http-status`). Both branches edit this file's issue body, so this targets that branch rather than handing you a conflict on the same lines. Merge #29 first; this PR's base then becomes `main` automatically, or retarget it. ## The defect `cve-watch.yml` compared the newest upstream tag against the newest **local git tag**, so the mere existence of a tag asserted "we already have this". But a tag can exist with nothing published: - the stale-tag guard from `3cd8e59` hard-fails — and that guard made this state *common*; it is exactly what `v7.1.5-hardened1` is in right now - the fidelity assertion aborts - the drift guard fires - `publish.yml` was simply never dispatched In every case `upstream == local`, so `new=false`, and the only automated "you are behind upstream on security patches" signal switched itself off — indefinitely, and precisely when a release had gone wrong. The failure mode silences the alarm it should be raising. ## The fix Key off `releases/<tag>/manifest.json`. `publish.yml` is what commits that file, so its presence is evidence that `build-kernel.yml` completed **and** publish ran. New `tools/newest-published-release.sh` does the resolution — in `tools/` rather than inline so `shellcheck build/*.sh tools/*.sh` covers it and it's testable against fixture directories. The newest git tag is still computed, now purely as a **diagnostic**: when a tag exists with no matching published release, the run emits a `::warning::` and the filed issue carries a "Stuck release" note. The watcher becomes a stuck-release detector, which is the natural place for that signal. **One deliberate behaviour change:** the `[[ -z $local ]]` branch used to suppress filing, because a shallow checkout could yield zero git tags and make every upstream tag read as new — that was issue #4's false positive. Reading `releases/` from the working tree removes that failure mode entirely, so an empty `local` now legitimately means "nothing has ever been published" and filing *is* correct. Reworded rather than left in place with an evaporated rationale. Also relabelled the body's `Local latest:` → `Local newest PUBLISHED:`, since that is now what the value means. ## Verification **The oracle script**, against the real repo and fixtures: | input | result | |---|---| | real repo, no args | `v7.1.4-hardened1` | | `--series 7.1` / `--series 7.0` | `v7.1.4-hardened1` / `v7.0.12-hardened1` | | fixture with a `v7.2.0` dir **and no manifest** | `v7.1.4-hardened1` — correctly ignored | | same after adding a manifest | `v7.2.0-hardened1` | | empty dir / nonexistent dir | empty output, exit 0 | | `--series 7` (malformed) | exit 2 | | `--series 7.0` with a `v7.10.1` present | empty — dot escaped, no false match | **The rewired comparison**, run offline with a stubbed `curl` against the *real* repo state (newest published `v7.1.4`, tag `v7.1.5` exists unpublished): | simulated upstream | `new` | `local` | `stuck` | |---|---|---|---| | `v7.1.6-hardened1` | `true` | `v7.1.4-hardened1` | `v7.1.5-hardened1` | | **`v7.1.5-hardened1`** | **`true`** | `v7.1.4-hardened1` | `v7.1.5-hardened1` | | `v7.1.4-hardened1` | `false` | `v7.1.4-hardened1` | `v7.1.5-hardened1` | The middle row is the whole point: that is today's state, and before this change it produced `new=false` — silent forever. **Issue body** rendered both ways (real step body extracted, templates substituted): with a stuck tag the note lands as its own paragraph; without one there is no stray whitespace. Validators: `shellcheck` clean (including the new script), `yamllint -d relaxed` exit 0, both `workflow-expressions` greps clean, and the new run:-body template rule from #29 still passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ci): key cve-watch off published releases, not bare tags
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
c33dfa41ab
cve-watch compared the newest upstream tag against the newest LOCAL GIT TAG, so
the mere existence of a tag asserted "we already have this". A tag can exist
with nothing published: the stale-tag guard added in build-kernel.yml hard-fails
(and that guard made this state common -- it is exactly what v7.1.5-hardened1
is in right now), the fidelity assertion aborts, the drift guard fires, or
publish.yml was simply never dispatched.

In every one of those cases upstream == local, so new=false, and the only
automated "you are behind upstream on security patches" signal switched itself
off. Indefinitely, and precisely when a release had gone wrong -- the failure
mode silences the alarm it should be raising.

Key off releases/<tag>/manifest.json instead. publish.yml is what commits that
file, so its presence is evidence build-kernel.yml completed AND publish ran.
New tools/newest-published-release.sh does the resolution; putting it in tools/
rather than inline in the workflow means shellcheck covers it and it can be
tested against fixture directories.

The newest git tag is still computed, now purely as a diagnostic: when a tag
exists with no matching published release, the run emits a :⚠️: and the
filed issue carries a "Stuck release" note. The watcher becomes a stuck-release
detector, which is the natural place for that signal to surface.

One deliberate behaviour change: the `[[ -z $local ]]` branch used to suppress
filing, because a shallow checkout could yield zero git tags and make every
upstream tag read as new (issue #4's false positive). Reading releases/ from the
working tree removes that failure mode -- the directory is either checked out or
the checkout is broken in a way that fails earlier -- so an empty `local` now
legitimately means "nothing has ever been published", and filing is correct.
Reworded rather than left in place with an evaporated rationale.

Also relabel the body's "Local latest:" to "Local newest PUBLISHED:", since
that is now what the value means.

Based on fix/workflow-injection-and-http-status: both branches edit this file's
issue body, so stacking avoids a conflict on the same lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zach merged commit 71fffda74e into fix/workflow-injection-and-http-status 2026-07-25 21:46:11 +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!32
No description provided.