chore(ci): close three validate.yml coverage gaps #38
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "chore/validate-coverage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Each of these three would have caught a defect already found in this review round.
1.
escape-hatches-are-plumbedA repo variable only reaches a script if the step running it lists the name in
env:. Nothing enforced that, and the result was a documented override that did nothing:check-hardening-drift.shhonouredALLOW_HARDENING_REGRESSION, bothPOLICY.md:296andconfig-architecture.md:138told the operator to set it, and the drift step'senv:listed onlyWORKDIRandSERIES. The operator's only route past a drift failure was a commit tomain— which the stale-tag guard then turns into "delete and re-cut the tag", mid-CVE-SLA.This job asserts every
ALLOW_*/REQUIRE_*name a script reads is both passed viaenv: vars.<NAME>by some workflow and documented underdocs/operators/or inPOLICY.md.Worth being precise: a workflow-shell linter would not have caught the original — a missing YAML
env:key involves no shell at all. That's why this is a separate check rather than folded into (3).It earned its place immediately. Run against the tree, it flagged
ALLOW_CROSS_SERIES_SEEDas plumbed but undocumented — a gap I'd left in #31. Fixed there, in the branch that introduced the hatch, rather than papered over here.2.
container-digests-pinned, replacingno-placeholder-digestsThe old job grepped
containers/for the placeholder token. That token now appears only inshim/Dockerfile, which the job deliberately excludes — so it could not fail. A check that cannot fail is worse than no check, because it reads as coverage.Inverted to a positive assertion that cannot go dormant: every
FROMundercontainers/must carry a concrete@sha256digest. Verified that a placeholder there now does fail, so the new form covers the old form's stated purpose.Folded in the adjacent free win:
build-kernel.yml'scontainer.image(line 34) and theCONTAINER_IMAGEthat goes into every manifest (line 424) were coupled only by a "Keep this digest in sync" comment. If they diverge, every manifest records a build environment the build did not use, andrepro-checkfaithfully rebuilds in an image that never produced the release — mismatches that look like tampering.3.
workflow-shell+tools/lint-workflow-shell.pyThe
shellcheckjob coversbuild/andtools/only. The largest and least-tested shell in this repo is in workflowrun:blocks —cve-watch.ymlalone is ~150 lines withjq,comm, anapi_callhelper and pipefail interactions — and a parse error there surfaces only when that job actually runs, which forcve-watchorprunecould be weeks later.Gated at
severity=error: 43 inline scripts, 0 findings, so it lands green and stays signal rather than a wall of style noise. The backlog above that is small and stated in the job comment rather than hidden: 1 finding atwarning, 3 atinfo.The linter lives in
tools/rather than as an inline heredoc precisely because the premise is that inline workflow shell goes unlinted — so the linter itself belongs whereshellcheckandpy_compilealready cover it.It substitutes a placeholder for each expression template (a raw template isn't valid shell), which produces one deliberate interaction: comparing a template in a
run:body shows up asSC2050"expression is constant". That isn't noise — it means a template is being used whereworkflow-expressions(#29) now requires anenv:variable. The two checks reinforce each other.Verification
Exit codes checked without pipeline masking (my first pass read
head's status, not the script's):FROMde-pinnedcontainers/CONTAINER_IMAGEcontainers/with noFROMlines (vacuity guard)if, bad array)The linter's positive control needed a real parse error — my first attempt used warning-level issues against an error-level gate and passed, which is exactly why the gate level is now stated explicitly in the job comment rather than left implicit.
shellcheck,py_compile,yamllint -d relaxedclean.Note
prune.py's doctests and thelines.pysmoke test — the fourth gap from the plan — are in #37 instead, since they belong with the prune changes.🤖 Generated with Claude Code
The two checks that stand between a seed bump and a silently weakened kernel could both report success while checking nothing. Verified empirically, not inferred. tools/check-hardening-drift.sh: run against an EMPTY baseline, and against a 50-line truncation, it printed hardening-drift: OK (no enabled hardening symbol regressed vs baseline) and exited 0 both times. The shape is `while ... done < <(enabled_hardening "$baseline")`: no matches means the loop never iterates, `regressed` stays 0, and the success path runs. build-kernel.yml only tests that the baseline file EXISTS, so a truncated or zero-length releases/<tag>/final.config would retire the only guard covering the 31 inherited hardening symbols -- with a green log and no output distinguishable from a real pass. build/configure-kernel.sh's fidelity assertion: same shape, same outcome. An empty intent.config yields "fidelity assertion: OK (0 intent lines, all landed)". `set -e` cannot catch it, because the inline `$(grep -c ...)` in that message is a command substitution used as an ARGUMENT, which never fails the shell. Fixes, following the anti-vacuous-pass pattern validate.yml already uses in its intent-matches-policy and dep-list-parity jobs: - drift: a >=1000 CONFIG_-line floor on BOTH inputs (real configs carry ~6,800, so this only catches truncation); the baseline symbol set is materialized before the loop and floored at 15 (31 today) -- a second, distinct check, because exclude_re is built FROM intent.config and a mangled intent can collapse the comparison set against a perfectly good baseline; the success line now reports the count. - fidelity: parse once into a variable, then three guards -- nothing parsed, a >=60 line floor (92 today), and a candidates-vs-parsed equality check. The third catches what a floor cannot: CRLF endings, trailing whitespace after "is not set", "#CONFIG_X" without the space. Each of those silently drops that pin instead of failing. The CRLF case is not hypothetical -- it parses all 69 `=y` pins and drops all 23 off-pins, and the guard reports exactly which lines fell through. Separately: ALLOW_HARDENING_REGRESSION was inert. check-hardening-drift.sh has honoured it since it was written, and both configs/POLICY.md and docs/operators/config-architecture.md tell the operator to set it as a repo variable -- but the drift step's env: block listed only WORKDIR and SERIES, so `vars.ALLOW_HARDENING_REGRESSION` never reached the script. Without it the only route past a drift failure is a commit to main, which the stale-tag guard then turns into "delete and re-cut the tag", mid-CVE-SLA. Plumb it the same way ALLOW_STALE_TAG and REQUIRE_NET_ISOLATION already are, hard-fail an empty SERIES (which would silently skip the guard), and make the no-baseline branch a :⚠️: so a SKIPPED guard is visible in the run summary. Document all three hatches in a repo-variables table in runner-setup.md, which previously mentioned only REQUIRE_NET_ISOLATION, in prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>