ci: gate OIDC publish mint surface + durable mutation-score reporter#105
ci: gate OIDC publish mint surface + durable mutation-score reporter#105Goosterhof wants to merge 1 commit into
Conversation
Two CI/tooling hardenings (no src/, no version bumps):
1. OIDC publish-token mint-surface gate (Sapper M2 H1 / STALE-4)
- Narrow publish.yml paths from '**/package.json' to
'packages/*/package.json' so only a package-version edit (the real
release signal) can start the id-token:write mint job. Root/devDep
manifest churn no longer mints OIDC tokens.
- Add `environment: npm-publish` to the publish job. The npm-publish
environment is created with a required-reviewer protection rule +
protected-branch deployment policy — a human gate in front of the mint.
2. Durable mutation-score reporter (Scout M5 / QM M4 F-4)
- Add json + html Stryker reporters across all 11 packages, writing to
packages/<pkg>/reports/mutation/ (gitignored — CI artifact only).
- Add an always()-conditioned upload-artifact step in the CI mutation
gate so per-package scores are retrievable run artifacts, not just
ephemeral stdout. break:90 thresholds unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying fs-packages with
|
| Latest commit: |
79bcdb6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://504b81c4.fs-packages.pages.dev |
| Branch Preview URL: | https://armorer-oidc-gate-mutation-r.fs-packages.pages.dev |
Goosterhof
left a comment
There was a problem hiding this comment.
✅ Approve-worthy
0 blockers · 2 concerns · 2 nits · 2 praise
Two CI-only hardenings: narrow the OIDC-mint trigger on publish.yml to packages/*/package.json plus an npm-publish environment gate, and add json+html Stryker reporters across all 11 packages with an if: always() artifact upload in ci.yml. No src/ changes, no version bumps. The supply-chain narrowing is the load-bearing change and it's correct — verified the glob, the environment, and the gitignore claim all hold.
Concerns
-
publish.yml:7— the narrowedpaths: packages/*/package.jsoncouples the release trigger to the assumption that every release bumps a versioned manifest underpackages/<pkg>/.- That assumption holds for the Changesets flow here:
npx changeset publish(publish.yml:73) is preceded by a "Version Packages" PR that editspackages/*/package.jsonversion fields, so the merge tomainstill matches and the job fires. - The failure mode it introduces: a release path that touches only a root-level or non-
packages/*manifest (workspace-version-only bump, a hypothetical hoisted publishable at repo root) would now be silently dropped — no job, no error. Today there is no such package (git ls-treeconfirms all 11 publishables live underpackages/*/), so this is a latent coupling, not a live bug. Worth a one-line comment on thepaths:block naming the invariant ("every publishable manifest lives underpackages/*/") so a future root-level package doesn't silently lose its release trigger.
- That assumption holds for the Changesets flow here:
-
publish.yml:35— the PR body flags it but it belongs in the record: thenpm-publishenvironment carries GitHub's defaultcan_admins_bypass: true. The reviewer gate is real for non-admins, but a repo admin can bypass the human checkpoint on the mint. Tightening tofalseis the honest hardening; the tradeoff (it also blocks admin-driven emergency publishes) is a legitimate reason to defer. Acceptable as-is given it's declared.
Nits
ci.yml:33—if-no-files-found: warnmeans a silent regression where Stryker stops emittingreports/mutation/leaves CI green and the artifact simply absent, with only a buried warning. Since the stated goal is durable score evidence,errorwould make "reports vanished" a loud failure. Minor —warnis defensible if you'd rather not couple the gate to reporter output shape.- All 11
stryker.config.mjsretainincremental: truealongside the newjson/htmlreporters. On CI the cache is cold (.stryker-incremental.jsonis gitignored), so the emitted report reflects the full run — correct. Flagging only so it's on record that the durable artifact is full-scope because CI starts cold, not by config intent; a future cached-incremental CI run would emit a partial-scope report under the same filename.
Praise
- The trigger narrowing is the right lever — gating at the
paths:filter stops the mint job from ever starting on devDep/tooling churn, which is strictly better than gating inside the job. Pairing it with a realenvironmentprotection rule (created live, not just referenced) closes both the "what starts the mint" and "who approves the mint" halves. if: always()on the upload is the correct call — capturing the report whenbreak:90fails is exactly when the score matters most.
Automated war-room agent review — posted because this PR carries the Agent Review Requested label.
PR Reviewer · claimed
|
PR Reviewer · 9/10 · PASS — 🟡 2fs-packages #105 · AC anchor: none 🟡 MINOR
why + fixRow 3 (privileged-surface gate, ADR-0006/0024 analog — canonical https://adrs.script.nl/decisions/two-tier-authorization.html). The publish job mints an Fix: No code change required. Optionally: a reviewer with repo-admin access confirms the
why + fixRow 6 (convention enforcement — ADR-0021 Escalation Ladder, canonical https://adrs.script.nl/decisions/phpstan-rules-package.html). The PR introduces a standing structural rule: 'only a Fix: Accept as L4. If a guard is wanted later, a lightweight workflow-lint/CI assertion (e.g. an actionlint or a grep step asserting the publish job retains Actionmerge-ready |
jasperboerhof
left a comment
There was a problem hiding this comment.
Auto-approved — review verdict is PASS. See the verdict comment for the per-reviewer breakdown.
Summary
Two CI/tooling hardenings bundled in one PR, both surfaced by the 2026-06-01 spy-refresh wave. No
src/changes, no version bumps — workflow + Stryker config only.Deployment order:
orders/fs-packages/oidc-gate-and-mutation-reporter-armorer-deployment.md.Item 1 — OIDC publish-token mint-surface gate
Problem (Sapper M2 H1 / STALE-4):
publish.ymlruns anid-token: write(OIDC Trusted Publishing) job that triggers onpaths: '**/package.json'and is ungated — so devDep/tooling churn at the root manifest mints a publish-capable OIDC token. This is the supply-chain root for every@script-developmentconsumer.Fix (two-part):
paths:changed from'**/package.json'to'packages/*/package.json'. Only a real package-version edit (the release signal) can now start the mint job; root/devDep churn cannot. Verified the glob matches all 11 package manifests and excludes the root manifest.environment: npm-publishto the publish job. Thenpm-publishGitHub environment was created live (gh api) with:mainonly).The gate is real, not just the workflow reference. One caveat for reviewers: the environment carries GitHub's default
can_admins_bypass: true— repo admins can bypass the reviewer gate. Tightening that tofalseis an optional follow-up (it would also block admin-driven emergency publishes).Publish-flow safety: the existing release mechanism (editing a
packages/<pkg>/package.jsonversion onmain) still matches the narrowedpaths:, so it still fires — now behind the reviewer gate.Item 2 — Durable mutation-score reporter
Problem (Scout M5 / QM M4 F-4): every package's
stryker.config.mjsused only['clear-text', 'progress']reporters — transient stdout. The per-package mutation score is gate-enforced (break:90) but never published to a durable artifact; each spy re-derives it from a CI-run capture.Fix:
json+htmlStryker reporters across all 11 packages, writing topackages/<pkg>/reports/mutation/(already covered by the root.gitignorereports/rule — these are CI artifacts, not committed).if: always()upload-artifactstep in the CI mutation gate (mutation-reports, 30-day retention) so the JSON/HTML reports are retrievable per run.always()captures the score even whenbreak:90fails — the score is the evidence.break:90thresholds unchanged — reporters/artifact only.(Committing a badge/table is a larger follow-up, deliberately out of scope; the CI artifact is the durable-publication bar this order sets.)
Verification
npm run format:check— clean (145 files).npm run lint(oxlint) — clean.break:90intact — "Final mutation score of 91.20 is greater than or equal to break threshold 90" — and both reporters emittedreports/mutation/mutation.json+mutation.html. Confirmed gitignored (not staged).🤖 Generated with Claude Code