Skip to content

ci(release): per-leg rust-cache keys in build-napi; suite-keyed Tier-B allowance in verify-pr-checks; re-scope #345 (#352, #341, #345) - #369

Merged
dean0x merged 8 commits into
mainfrom
ci/b2-cache-keys-suite-keying
Sep 7, 2026
Merged

ci(release): per-leg rust-cache keys in build-napi; suite-keyed Tier-B allowance in verify-pr-checks; re-scope #345 (#352, #341, #345)#369
dean0x merged 8 commits into
mainfrom
ci/b2-cache-keys-suite-keying

Conversation

@dean0x

@dean0x dean0x commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

Step B2 of the v0.4.3 action plan: three CI-hardening items landed as one CI-gated PR.

Also folds in the release-pipeline knowledge base (.devflow/features/release-pipeline/KNOWLEDGE.md, commit 78cb095) and updates it to the post-B2 state.

#352 — per-leg rust-cache keys (PF-041)

Swatinem/rust-cache@v2 (v2.9.2) builds its key as v0-rust[-<key>]-<job>-<runner.os>-<runner.arch>-<envhash>-<lockhash>. The env hash covers rustc -vV (the HOST triple) plus CARGO*/CC*/CFLAGS/CXX*/CMAKE*/RUST* env; the cross TARGET is not in it, and build-napi exports its CARGO_TARGET_*_LINKER vars after the cache step. Live proof in PR run 34065573775: every Linux leg restored v0-rust-build-napi-Linux-x64-6ff13d87-4c33221b; both macOS legs restored v0-rust-build-napi-Darwin-arm64-….

  • Fix: key: ${{ matrix.settings.target }} on build-napi's step. The key input precedes the job id, so v0-rust-aarch64-apple-darwin-build-napi-… cannot collide with build-python's v0-rust-aarch64-apple-darwin--build-python-…. Host is already encoded (runner os/arch + rustc host hash), so only the target is added; no matrix.settings.host (runner labels float).
  • build-python untouched: PR feat(ci): Python wheel matrix + PyPI publishing (markdown-script, #132) #347 already gave all seven wheel legs key: ${{ matrix.target }}-${{ matrix.manylinux }}; its four cache lines are byte-identical here (comment added above). The ticket's "macOS pair share a key" claim was stale.
  • Single-leg jobs stay bare: publish-crates and publish-npm get a one-line comment; the automatic job-id key is already unique.
  • The only non-comment change to release.yml is the with:/key: pair (verified by diff filter). No job-level if:, needs:, on: or permissions: change; no ${{ inside comments (PF-045); no new uses: (PF-046).
  • Cache scope rule: pull_request runs save under refs/pull/N/merge, invisible to a workflow_dispatch on the branch; main holds no build-napi caches (release.yml never runs on pushes to main), so tag runs were always cold and this change is behaviour-neutral on the tag path. Warm evidence therefore needs TWO runs in the SAME scope (two dispatches), never a dispatch after a PR run.
  • Spec S20 (release-auth-probe.spec.mjs): every rust-cache step in every matrix job of release.yml must carry a key containing matrix.; non-vacuity asserts build-napi and build-python are checked and publish-crates is exempt; seven positive controls (bare step, static key, commented-out key, matrix key, commented-out strategy:, strategy: with trailing comment, uses: not the first key, no key-borrowing from the next step). Proven red on the real file with the key removed.

#341 — suite-keyed allowance and D-PR7 attribution (D-PR8)

  • Three keys, not one: a Tier B skipped is allowed iff the name is in TIER_B_EXPECTED_SKIPPED (set unchanged; M10c/S13 still pin it to release.yml's guarded jobs) AND check_suite.id is an integer AND that id belongs to a workflow run whose path is .github/workflows/release.yml. Any event qualifies — the RELEASING.md dry-run is a workflow_dispatch, and PR chore(ci): bump checkout v7, setup-node v7, setup-python v7; normalize maturin-action pin (PF-040) #365's head carries a dispatch-only release suite.
  • D-PR7 (Version gate, Stage + verify platform packages, Rehearse PyPI publish (no upload) on release-surface PRs): attributed the same way; same-name runs in foreign suites are ignored (and still judged by Tier B); every attributed run must be completed+success (all-must-pass).
  • One extra bounded call: GET /actions/runs?head_sha=<sha>&per_page=100, at most 5 pages, total_count asserted, projected to {id,path,event,check_suite_id,conclusion}; it is the LAST fetch in main() so earlier exit-2 paths are unchanged. Call budget 58 worst case (was 53).
  • Fail-closed matrix: runs-API error / >5 pages / count mismatch → exit 2; zero release suites → the five skipped names fail and D-PR7 contexts read as absent; missing check_suite → fail; foreign suite → fail naming both ids; releaseSuiteIds undefined → allowance DISABLED and D-PR7 cannot attribute (a head with no skipped allow-listed names and an untouched surface still passes); several release suites → each run attributed on its own.
  • app.slug is not consulted: the path filter is what excludes the ci.yml run, the dynamic CodeQL analysis run (which DOES appear in /actions/runs as dynamic/github-code-scanning/codeql) and the github-advanced-security umbrella suite (which does not appear there at all).
  • Fixtures: checks-pr366-e02bcf2.json (44 check-runs, 4 suites), runs-pr366-e02bcf2.json (3 runs, one release.yml suite 92290758559), protection-main-2026-09.json (15 contexts). protection-main.json (2026-08, 6 contexts) is byte-identical (md5 cd875dabeb4f65bdb31dde15cfc71579).
  • Specs: 187 → 210 gate tests. New D-PR5g/g2/h/h2/i/i2/j/k, D-PR7k/l, releaseSuiteIdsFrom unit + RELEASE_SURFACE.includes(RELEASE_WORKFLOW_PATH) drift guard, and a current fixtures (2026-09) describe (CF-1..CF-5), each with a PF-013 flipped-condition control.
  • Live results (retroactive, read-only):
    • PR 366 → release.yml suites: 92290758559 (pull_request), five allowed (D-PR8) lines, PASS, exit 0.
    • PR 367 → exit 1: "Rehearse PyPI publish (no upload)" — status=completed, conclusion=failure (suite 92287905855).
    • PR 353 → release.yml suites: none, release surface not touched, PASS, exit 0.
    • PR 365 → release.yml suites: 92246852351 (workflow_dispatch), four allowed lines (dispatch-only suite attributed — event-agnostic), D-PR7 absent for the rehearsal, exit 1.

#345 — re-scoped to documentation

Verified live 2026-09-07 against rust-lang/crates.io main:

  • GET /api/v1/me is AuthCheck::only_cookie() (src/controllers/user/me.rs:38-41); a valid API token, scoped or not, gets HTTP 403 this action can only be performed on the crates.io website (src/auth.rs:136-144).
  • A token failing crates.io's format check gets HTTP 401 (src/auth.rs:295, InsecurelyGeneratedTokenRevoked); a well-formed token not in the database gets HTTP 403 authentication failed (src/auth.rs:297-303). The live probe with a bogus header returns 401, not the 403 the ticket assumed.
  • The only token-accepting read route, GET /api/v1/me/tokens/{id} (src/controllers/token.rs:269-282), accepts legacy unscoped tokens only; a scoped token gets 403 this token does not have the required permissions. There is no read-only probe for a scoped publish token. Every request needs a User-Agent (src/middleware/require_user_agent.rs:35-47).
  • So the non-empty guard is the strongest general check; a dead token is first detected at the first cargo publish (fail-before-write, after the build matrix). v0.4.0 run 33569514359 attempt 1 is the precedent; gh run rerun --failed completed it.

Landed: the two-line guard comment in version-gate (no ${{, no cargo publish -p substring), spec S3 now asserts the executable if [ -z "$CARGO_REG_TOKEN" ]; then line on comment-stripped text (planted control: guard-in-comment-only is rejected; proven red on the real file), RELEASING.md's credential-probe section rewritten as three bullets stating what each registry check proves, prerequisite 3 amended (PF-023), and the fork/Dependabot paragraph corrected: a fail-closed Version gate on a release-surface PR blocks the merge (D-PR7) by design. #345 will be closed as not planned after this merges; the durable fix (crates.io Trusted Publishing via rust-lang/crates-io-auth-action) is #368.

#351

Untouched. The containerised build-python Linux legs remain protected only by the #347 per-leg keys; this PR does not change that.

Verification

Local (all on HEAD 183fc84):

  • npm run test:gates 210 pass / 0 fail (baseline 187); node scripts/verify-no-control-bytes.mjs and node scripts/verify-versions.mjs pass.
  • js-yaml: 10 jobs; build-napi {"key":"${{ matrix.settings.target }}"}; build-python {"key":"${{ matrix.target }}-${{ matrix.manylinux }}"}; publish-crates/publish-npm no with.
  • PF-013 real-file controls: S20 red with the build-napi key removed; S3 red with the guard commented out; both green after restore.
  • Independent QA: 13 scenarios (four live verifier runs, ten pure-function fail-closed cases including the page cap observed at exactly 5 requests, YAML shape, docs) all PASS.

CI:

  • PR-triggered release run (cold for the new keys): run 34127009143, success. Every build-napi leg printed No cache found. then ... Saving cache ...; per-leg Cache Key: values carry the target, e.g. v0-rust-aarch64-apple-darwin-build-napi-Darwin-arm64-2eab217e-4c33221b, v0-rust-x86_64-apple-darwin-build-napi-Darwin-arm64-2eab217e-4c33221b, v0-rust-x86_64-unknown-linux-musl-build-napi-Linux-x64-6ff13d87-4c33221b, v0-rust-x86_64-pc-windows-msvc-build-napi-Windows_NT-x64-2113753f-4c33221b (all seven distinct); no leg printed Restored from cache key "v0-rust-build-napi- (the old shared blob); refs/pull/369/merge holds exactly 7 v0-rust-*-build-napi-* keys and none of the old shape. musl readelf gates on both matrices: Shared library: [libc.so], no glibc, no ::error::. Version gate: # pass 210 / # fail 0, PyPI trusted publisher OK, CI-history gate step-skipped with the notice. Five guarded jobs skipped; everything else success.
  • ci.yml on this head: run 34127009027, completed success.
  • Dispatch refactor: tech debt — EvalContext, Lexer struct, Arc sharing, IndexSet #1 (branch scope): run 34127666131, success. Cold again as predicted by the cache-scope rule (all seven legs No cache found.Saving cache); the CI-history gate executed (not skipped); # pass 210 / # fail 0; five guarded jobs skipped.
  • Dispatch refactor: Arc<FunctionDef> for closure capture (eliminate quadratic cloning) #2 (warm evidence, PF-041 rule): run 34128014799, success. Every build-napi leg printed Restored from cache key "v0-rust-<its own target>-build-napi-…" full match: true. and post-step Cache up-to-date. — e.g. Restored from cache key "v0-rust-aarch64-unknown-linux-musl-build-napi-Linux-x64-6ff13d87-4c33221b" full match: true.; env hash 4c33221b unchanged between dispatches; the branch scope holds exactly 7 v0-rust-*-build-napi-* keys; readelf gates green.
  • node scripts/verify-pr-checks.mjs 369: PASS on head 183fc84release.yml suites: 92452493617 (workflow_dispatch), 92451554644 (workflow_dispatch), 92449765759 (pull_request); fifteen allowed (D-PR8) lines (five per suite); release surface touched (1 file(s): .github/workflows/release.yml); ✓ PASS — all 15 required contexts completed+success; merged with the printed command gh pr merge 369 --squash --admin --match-head-commit 183fc84639870c2cfba9512a667775a9d2c42be5 verbatim.
  • TestPyPI first-upload dispatch from main (run 34117799084, informational): all build/gate jobs green; Publish to TestPyPI (rehearsal) failed at upload with HTTP 400 Non-user identities cannot create new projects … pending publisher … project name incorrectly — the test.pypi.org record exists but its project name is not markdown-script. User-side fix; not a blocker for this PR.

Snyk: the local Snyk MCP server fails to start (ENOENT), so the security/snyk (dean0x) PR check is the scan of record for this change (no new first-party runtime code; the changes are a gate script, specs, fixtures, workflow comments and docs).

Related Issues

…7 contexts on release.yml check-suite identity (D-PR8, #341); add 2026-09 fixtures
… ids in D-PR7 messages, event-annotated suite log, docblock/comment completeness, drop RED-phase residue, pin assertions, add positive controls (#341)
Extract the repeated `[...releaseSuiteIds].join(', ') || 'none'` expression
(Tier B line and D-PR7 absent-run line in evaluateChecks) into a private
`formatSuiteIds` helper. No logic change; 210/210 gate tests pass.
…ue (#352, #341)

S20's rustCacheSteps anchored detection on `- uses: Swatinem/rust-cache@`, so a
step written `- name: …` / `  uses: Swatinem/rust-cache@…` returned zero steps
and the core per-step assertion iterated nothing. Verified against the live
file: with build-napi's step reordered that way the old helper returns [] (only
the separate build-napi/build-python non-vacuity guards caught it; a third
matrix job would have passed vacuously). Segment steps first, then match `uses:`
anywhere in the segment, and read `key:` from within the segment so a bare step
cannot borrow the next step's key.

hasMatrix anchored `^    strategy:\s*$`, so `strategy:` with a trailing inline
comment or written as a flow mapping read as "not a matrix job" and exempted the
whole job from S20. Match `^    strategy:(\s|$)` instead.

Both hardenings are mutation-verified: with the key dropped and the step
reordered (or `strategy:` given a trailing comment) S20 fails with the PF-041
message; release.yml restored byte-identical afterwards. New positive controls
PC5b (trailing comment / flow mapping), PC6 (uses: not first key) and PC7 (bare
step must not borrow a later step's key) live inside the existing S20 test, so
`npm run test:gates` stays 210 pass / 0 fail.

Also: remove two spec comments citing commit 3a803f5 as the source of the
D-PR7j all-must-pass contract (main's D-PR7j spec cites no commit), remove a
stray `// correct spelling` marker, and correct the release-pipeline KB's
test:gates count from 211 to 210.
…dynamic CodeQL run does appear in /actions/runs; the path filter is what excludes it); PF-013 in fetch messages; spec naming/shape nits (#341)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant