feat(runner-policy): admit review-tier variable as a governed selector input - #155
Conversation
…r input
Allow the selector self-hosted-label input to be either
${{ vars.CI_SELF_HOSTED_LABEL }} (default fleet tier) or
${{ vars.CI_REVIEW_SELF_HOSTED_LABEL }} (dedicated capped review tier), so a
claude-review caller can route to the review tier through a separate
select-review job while the repository's other self-hosted jobs keep the
default (provisioning#135). exactCanonicalMap becomes set-membership; a
single-string canonical value keeps identical exact-match behavior, so the
string-valued secret and caller-permission maps are unchanged. The schema
gains canonicalSelectorInputMap (a string or a non-empty unique array).
Tests cover the admitted review variable, a rejected ungoverned expression,
and an end-to-end claude-review caller routing to the review tier with zero
findings. README documents the two-expression allowance and the
required-no-default default-tier limit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
|
Claude finished @kyle-sexton's task in 3m 53s —— View job Review:
|
…eview tier Add select-runner.yml@cdc5917c (the ci-workflows#123 merge commit, which admits melodic-review-ubuntu-24.04-x64 in the strict allowlist) to the melodic-software owner-scoped approved selector references, and register it in the allowlist drift-guard test. Completes the review-tier admission chain: a consumer's select-review job pins this revision to route claude-review to the review tier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
|
Claude finished @kyle-sexton's task in 1m 22s —— View job
Review of
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0cdc03017
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add the cdc5917c review note to the selector revision lockstep paragraph - it documents that this revision admits melodic-review-ubuntu-24.04-x64, that the control-plane job still runs on the default fleet label, and that a review lane must pin it because older pins fail closed on the review label. Update the approved-revision counts (seven to eight total, four to five owner-scoped strict-scheduling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
|
Claude finished @kyle-sexton's task in 2m 0s —— View job
Review of
|
…racts (#156) ## Summary Register `runner-input` contracts in `components/runner-policy/policy.json` for two floor-conversion reusables, both pinning ci-workflows PR #124's squash-merge commit `3dfb18452a8c6059a22e62456390d84feb10b42f` (the reviewed source that carries the governed `runner` input): - `link-check.yml` — `allowedInputs: ["runner", "args"]`, `allowedCallerPermissions: {contents: read, issues: write}`. - `pulumi-version-drift-check.yml` — `allowedInputs: ["runner"]`, `allowedCallerPermissions: {contents: read, issues: write}`. Each carries `allowedCallerPermissions` because its rolling tracking-issue lane writes issues through the caller `GITHUB_TOKEN`; the waiver is honored only while the call is genuinely selector-routed, and the reusables' own permissions are unchanged. This also corrects the now-false README claim that the Pulumi version-drift monitor "remains fixed to explicit GitHub-hosted images," adds a review note for both contracts, and extends the runner-policy golden fixture (`runner-policy.test.mjs`) to assert them. `hostedExceptionReasons` is **unchanged** — enum-tightening is the separate last wave and must not land before every consumer has dropped its floor exceptions. ## Why Wave 2 of the private-repo hosted-floor elimination (melodic-software/github-iac#78, decision of record 2026-07-16). ci-workflows PR #124 (Wave 1) added the governed `runner` input to these reusables' source; this PR registers the reviewed contracts so each private consumer (Wave 3) can add a `select-runner` job, route the reusable to the fleet, and drop its `hosted-control-plane` / `privileged-control-plane` exception. `policy.json` is the upstream source of truth synced to five consumers, so the contract belongs here. ## Deferred — standards-sync The third floor-conversion reusable, `standards-sync.yml`, is **intentionally not registered here.** Its caller `GITHUB_TOKEN` is read-only (`sync.yml` declares `permissions: contents: read`; all writes go through a minted App token), but a `runner-input` contract that passes secrets must carry `allowedCallerPermissions`, which the validator requires to include at least one `write` (`runner-policy.mjs:186-198`). There is no honest caller write to declare, so registering it would bake a least-privilege inaccuracy into the managed contract. This is a contract-model gap (the secrets waiver is coupled to a write requirement) routed to the runner-policy component owner for a "reviewed secrets + read-only caller" contract shape; `standards-sync` keeps its hosted-only contract until that lands. ## Test plan - [x] `npm run test:runner-policy` — passing with two new golden assertions on the registered contracts: 143/143 on this branch base, and 146/146 verified against a local rebase onto current `main` (post #155 review-tier admission, #153 dependabot-policy component). - [x] `npm run lint:runner-policy` (`GITHUB_REPOSITORY=melodic-software/standards`) — `Runner policy passed` on both the branch base and the current-`main` rebase (the #155 validator). - [x] biome, markdownlint — clean (pre-commit). - [x] Merge SHA finalized to `3dfb184…` (PR #124 squash-merge) across `policy.json` (both contract keys), the golden-fixture constant, and the README review note. ## Related No linked standards issue. Cross-repo references: - Wave 2 of melodic-software/github-iac#78 (private-repo hosted-floor elimination epic). - Depends on melodic-software/ci-workflows#124 (merged) — source of the pinned reusable SHA. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ier (#158) ## What - Add a `select-review` job resolving `${{ vars.CI_REVIEW_SELF_HOSTED_LABEL }}` through the governed selector (pinned `select-runner@cdc5917c`). - Route the `review` job's runner from the selector output to the fleet claude-review reusable (pinned `claude-review@4dbb0dfc`), permissions exactly per the reviewed caller-perms waiver. - Drop the `privileged-control-plane` hosted exception for `claude-review.yml#review`. ## Why Campaign A of the zero-hosted directive (epic melodic-software/github-iac#78): automated review moves to the dedicated capped review tier. Shape matches this repo's own runner-policy E2E fixture (#155); `runner-policy.mjs --root .` passes locally with zero findings. Supersedes the closed draft #142. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt --------- Co-authored-by: Claude Fable 5 (1M context) <noreply@anthropic.com>
## What
Two changes to the managed `runner-policy` component that stop a paid
hosted label from silently becoming the governed fallback:
1. **Remove `ubuntu-slim` from `approvedHostedRunnerLabels`.** It can no
longer be an explicit `runs-on` target, a matrix value, or the governed
fallback default. `ubuntu-24.04` is the sanctioned free fallback; the
`ubuntu-slim` tier was eliminated (billed even on public repos).
2. **Add a `fallbackLabelAllowlist` policy field** (default
`["ubuntu-24.04"]`) and require `governedReusableRunnerInput.default`
to be a member — a gate strictly tighter than
`approvedHostedRunnerLabels`. A label can be an approved explicit
target yet still be barred from becoming the silent recovery fallback.
## Why this shape (and not the originally-scoped per-workflow rule)
The initial framing was a per-workflow finding rejecting any
non-allowlisted
literal in the hosted-fallback arm of a selector `runs-on`. The
validator's
architecture does not have that gap: the selector-output arm
(`${{ needs.X.outputs.runner || '<label>' }}`) is already pinned by
`routeStatus` to exactly `governedReusableRunnerInput.default`, and the
ternary arm (`… && '<fleet>' || '<label>'`) is rejected wholesale as
unresolvable dynamic routing. A consumer cannot write `|| 'ubuntu-slim'`
unless the policy default itself is `ubuntu-slim`. The real
silent-return
path is therefore the **policy default**, validated only against
`approvedHostedRunnerLabels` (which still contained `ubuntu-slim`). This
PR
closes it at that single source, plus removes the label so bare
`runs-on: ubuntu-slim` also dies.
## Compatibility
- **Default allowlist does not break any consumer.** All consumer
selector
fallbacks are `|| 'ubuntu-24.04'` (org-verified 2026-07-18) and
`ubuntu-24.04` is the default, so it is in the default allowlist. No
consumer `runner-policy.json` references `ubuntu-slim`.
- This repository's own workflows pass the audit under CI conditions
(`CI_REPOSITORY_VISIBILITY=private`, owner evidence present): "Runner
policy passed.", exit 0.
## schemaVersion
Not bumped. `schemaVersion` has no runtime semantics (never read by
`runner-policy.mjs`; it is a declarative `const` marker), no test
hardcodes
it, and prior schema field-additions (#140, #155) did not bump it.
`policy.json`
and `policy.schema.json` are `managed` and sync verbatim as one unit, so
no
consumer ships a hand-authored policy the new required field could
reject.
## Tests
`node --test components/runner-policy/runner-policy.test.mjs` — 213
pass, 0
fail (was 208; +5). New config-level cases, matching the real
`ConfigurationError` architecture rather than a per-workflow finding:
- required-field presence for `fallbackLabelAllowlist`;
- default not in the allowlist (approved `windows-2025`) rejects;
- `ubuntu-slim` default now rejects as not-approved (locks the removal);
- shipped `|| 'ubuntu-24.04'` selector route passes clean;
- allowlist extended to admit a second approved default passes clean.
Also updated the existing "selector recovery derives its literal
fallback"
test to extend `fallbackLabelAllowlist` alongside its `default`
override.
`lint:md`, `biome check`, and the runner-policy self-audit are green.
## Reconciliation checklist
- [x] Cross-doc reconciliation run. Updated `README.md` (fallback
passage)
and `THREAT-MODEL.md` (residual-risk bullet) to state the new allowlist
bound; both are consistent additions, not restatements.
`conventions/review/ai-review-bot-composition.md` checked — its
"default"
usage is bot-composition, unrelated; not contradicted.
- [x] No other doc in the catalog references the fallback default or the
approved-label set in a way this change contradicts.
## Related
No linked issue: this PR delivers the fallback-allowlist guardrail
directly
without a tracking issue.
- melodic-software/ci-workflows#141 — companion
- melodic-software/medley#1570 — companion
- melodic-software/ci-workflows#143 — context for why the `ubuntu-slim`
tier is dead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_015hTQ64MDYGvqVUEPPWQV6Q
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary Delivers the ci-runner-alignment audit: a full-surface review of the self-hosted CI system (ci-runner, provisioning, ci-workflows selector, standards runner-policy, github-iac routing governance) against GitHub's official documentation, fetched 2026-07-18. - `docs/topics/ci-runner-alignment/REPORT.md` — headline synthesis, severity-ordered triage index, per-divergence verdicts D2–D12 (D1 withdrawn as phantom), aligned findings, unused-features dispositions with adopt/skip/defer + triggers, limitations appendix. Every claim cites an official source. - `docs/topics/ci-runner-alignment/PLAN.md` — locked Brief + architect-filled Plan (single-file report layout decision, phase records, acceptance-criteria PASS table). - `docs/topics/ci-runner-alignment/design/design-resolution.md` — Tier C early-exit record (docs-only stage). Verdict counts: 6 confirmed, 5 needs-decision, 1 withdrawn. IMPORTANT findings: D11 (30-day runner minimum-version SLA + critical-CVE no-grace clause) and D12 (queue monitor scheduler is a silent SPOF via public-repo 60-day auto-disable). Dispositions on needs-decision items are reserved for the one-by-one walkthrough; follow-up fixes/issues land after it. ## Related No linked issue — the audit's follow-up fixes and issues are filed per-repo after the user walkthrough, not closed by this report PR. - melodic-software/github-iac#141 — review-tier org variable (audit-verified merged; D2 evidence) - melodic-software/standards#155 — runner-policy review-tier admission (D2 evidence) - melodic-software/provisioning#146 — claude-review routed through the governed selector (D2 evidence) - `docs/topics/runner-performance/` — closed perf topic the D3 capacity-clamp verdict rests on ## Verification - markdownlint-cli2 exit 0 over the topic directory. - Mechanical report checks: 11 divergence entries, verdict + official citation in every entry, zero memory-tier references. - All 29 cited URLs verified against same-day research artifacts (3 cross-repo PR URLs live-verified merged with matching titles). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PDxVbV5HvMA7pji43USWcQ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
canonicalSelectorInputs.self-hosted-labelmay now be either${{ vars.CI_SELF_HOSTED_LABEL }}(default fleet tier) or${{ vars.CI_REVIEW_SELF_HOSTED_LABEL }}(dedicated capped review tier). Aclaude-reviewcaller passes the review variable from a separateselect-reviewjob so its review workload routes to that tier while the repository's other self-hosted jobs keep the default.exactCanonicalMapbecomes set-membership: a canonical value is one exact governed expression or an exact set of them. Fail-closed is preserved — any expression outside the reviewed set is rejected. A single-string canonical value keeps identical exact-match behavior, so the string-valued selector-secret, reusable-secret, and reusable caller-permission maps are unchanged.canonicalSelectorInputMap(each value is anonWhitespaceStringor anonEmptyUniqueStringArray);canonicalSelectorSecretsstays string-only.select-runner.yml@cdc5917c15aade1995bd810b60d818cadc635b52inapprovedSelectorReferencesByRepositoryOwner.melodic-software(the revision that admitsmelodic-review-ubuntu-24.04-x64in the selector's strict allowlist), registered in the allowlist drift-guard test.self-hosted-labelexpression is rejected (selector-pin, "must be one of"); and an end-to-endclaude-reviewcaller (dedicatedselect-reviewjob +runnerfrom its output under the feat(runner-policy): review privileged reusable callers #140 caller-perms waiver) audits to zero findings. That last test doubles as the flip agent's executable spec and proves the review label threads through claude-review's optional-default path without tripping the hardcoded default-label routing-condition guards.Scope / retained fallback
The consumer
runnerfallback|| 'ubuntu-24.04'is intentionally retained for now — it is the selector-failure recovery path. The floor-conversion wave (owner directive: zero hosted minutes on private repos) will revisit inert hosted fallbacks separately; this PR keeps them so it stays a focused input-contract change.Merge order
CI_REVIEW_SELF_HOSTED_LABEL) can merge in parallel.This PR and the wave-12 reusable-contract PR both touch
components/runner-policy/policy.json, but in disjoint objects — this one editscanonicalSelectorInputsandapprovedSelectorReferencesByRepositoryOwner; wave-12 editsapprovedReusableWorkflowContracts. Whichever merges second rebases onto the first; no semantic conflict.Validation
node --test components/runner-policy: 146 passed (3 new + the drift-guard updated for the new SHA).policy.jsonvalidates.Related
No linked issue.
🤖 Generated with Claude Code
https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt