Skip to content

ci: gate OIDC publish mint surface + durable mutation-score reporter#105

Open
Goosterhof wants to merge 1 commit into
mainfrom
armorer/oidc-gate-mutation-reporter
Open

ci: gate OIDC publish mint surface + durable mutation-score reporter#105
Goosterhof wants to merge 1 commit into
mainfrom
armorer/oidc-gate-mutation-reporter

Conversation

@Goosterhof
Copy link
Copy Markdown
Contributor

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.yml runs an id-token: write (OIDC Trusted Publishing) job that triggers on paths: '**/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-development consumer.

Fix (two-part):

  1. Narrowed triggerpaths: 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.
  2. Environment gate — added environment: npm-publish to the publish job. The npm-publish GitHub environment was created live (gh api) with:
    • a required-reviewer protection rule (a human gate in front of the mint), and
    • a protected-branch deployment policy (deploy from main only).

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 to false is an optional follow-up (it would also block admin-driven emergency publishes).

Publish-flow safety: the existing release mechanism (editing a packages/<pkg>/package.json version on main) still matches the narrowed paths:, 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.mjs used 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:

  • Added the json + html Stryker reporters across all 11 packages, writing to packages/<pkg>/reports/mutation/ (already covered by the root .gitignore reports/ rule — these are CI artifacts, not committed).
  • Added an if: always() upload-artifact step in the CI mutation gate (mutation-reports, 30-day retention) so the JSON/HTML reports are retrievable per run. always() captures the score even when break:90 fails — the score is the evidence.
  • break:90 thresholds 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.
  • Workflow YAML — both files parse (js-yaml), no tabs.
  • Stryker smoke (fs-router, full non-incremental): break:90 intact — "Final mutation score of 91.20 is greater than or equal to break threshold 90" — and both reporters emitted reports/mutation/mutation.json + mutation.html. Confirmed gitignored (not staged).
  • Full CI runs on this PR.

🤖 Generated with Claude Code

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>
@Goosterhof Goosterhof added the Agent Review Requested Requesting review of specialized AI review agents. label Jun 1, 2026
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

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

View logs

Copy link
Copy Markdown
Contributor Author

@Goosterhof Goosterhof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 narrowed paths: packages/*/package.json couples the release trigger to the assumption that every release bumps a versioned manifest under packages/<pkg>/.

    • That assumption holds for the Changesets flow here: npx changeset publish (publish.yml:73) is preceded by a "Version Packages" PR that edits packages/*/package.json version fields, so the merge to main still 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-tree confirms all 11 publishables live under packages/*/), so this is a latent coupling, not a live bug. Worth a one-line comment on the paths: block naming the invariant ("every publishable manifest lives under packages/*/") so a future root-level package doesn't silently lose its release trigger.
  • publish.yml:35 — the PR body flags it but it belongs in the record: the npm-publish environment carries GitHub's default can_admins_bypass: true. The reviewer gate is real for non-admins, but a repo admin can bypass the human checkpoint on the mint. Tightening to false is 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:33if-no-files-found: warn means a silent regression where Stryker stops emitting reports/mutation/ leaves CI green and the artifact simply absent, with only a buried warning. Since the stated goal is durable score evidence, error would make "reports vanished" a loud failure. Minor — warn is defensible if you'd rather not couple the gate to reporter output shape.
  • All 11 stryker.config.mjs retain incremental: true alongside the new json/html reporters. On CI the cache is cold (.stryker-incremental.json is 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 real environment protection 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 when break:90 fails is exactly when the score matters most.

Automated war-room agent review — posted because this PR carries the Agent Review Requested label.

@jasperboerhof
Copy link
Copy Markdown
Contributor

PR Reviewer · claimed

@jasperboerhof
Copy link
Copy Markdown
Contributor

PR Reviewer · 9/10 · PASS — 🟡 2

fs-packages #105 · AC anchor: none
Scores: acceptance SKIP · simplicity 9 · surface 9 · silent-failure – · efficiency –

🟡 MINOR

.github/workflows/publish.yml:39 — OIDC mint gate's load-bearing protection rule lives in repo settings, not the diff — unverifiable from the branch

why + fix

Row 3 (privileged-surface gate, ADR-0006/0024 analog — canonical https://adrs.script.nl/decisions/two-tier-authorization.html). The publish job mints an id-token: write OIDC Trusted-Publishing token — the supply-chain root for every @script-development consumer. The PR adds environment: npm-publish (line 39) and narrows the trigger from '**/package.json' to 'packages/*/package.json' (line 11), both correct tightenings of an over-broad mint surface. The actual human gate, however, is the required-reviewer + protected-branch protection rule on the npm-publish environment, which was created out-of-band via gh api and lives in GitHub repo settings — it is NOT in this diff, so a reviewer cannot confirm the gate exists or that it survives. The PR body discloses this AND the can_admins_bypass: true caveat (admins can bypass the reviewer gate) honestly. Because it is a disclosed, directionally-tightening change with the gate-existence claimed in prose, this is informational, not a defect — recorded MINOR per the disclosed-contract-ripple rule.

Fix: No code change required. Optionally: a reviewer with repo-admin access confirms the npm-publish environment carries the required-reviewer rule + protected-branch policy before merge; consider tightening can_admins_bypass to false as the disclosed follow-up (weighed against blocking admin-driven emergency publishes).

.github/workflows/publish.yml:11 — Narrowed mint-trigger + environment gate have no CI regression guard — a future workflow edit can silently re-broaden them

why + fix

Row 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 packages/*/package.json version edit may start the mint job, and the publish job must carry environment: npm-publish.' Nothing pins this — a later edit could revert paths: to '**/package.json' or drop the environment: line and no test/lint would catch it. fs-packages has no arch-test or lint layer over .github/workflows/ (consistent with the repo's tooling), so the de-facto enforcement level is L4 (code-review-only). That is the conventional level for workflow files and acceptable here; flagged only so the absence of a regression guard on a supply-chain-critical trigger is on the record. Note also the paths: 'packages/*/package.json' glob is single-level — it fails CLOSED (a hypothetical nested package wouldn't fire a publish), so it is not a security defect.

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 environment: and the narrowed paths:) is the L1-equivalent escalation lever.

Action

merge-ready

Copy link
Copy Markdown
Contributor

@jasperboerhof jasperboerhof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved — review verdict is PASS. See the verdict comment for the per-reviewer breakdown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Review Requested Requesting review of specialized AI review agents.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants