ci: replace ubuntu-slim fallback with free ubuntu-24.04#141
Conversation
## What Swap the `ubuntu-slim` hosted fallback for the standard `ubuntu-24.04` runner in the selector job and the three fail-closed selector-result reporters (`select-runner`, `semantic-pr`, `do-not-merge-gate`, `pr-issue-linkage`), update the adjacent cost-framing comments and the `.cjs` contract tests that pin the literal, and reconcile the three README lines that describe this fallback/reporter behavior so the docs land with the change they describe. ## Why `ubuntu-slim` is billed even on public repositories: this public repo burned 6,906 minutes for $13.44 net in July (observed in org billing). `ubuntu-24.04` is free on public repos and quota-covered on private ones, so the fallback carries no minutes cost while preserving the fail-closed reporting contract (a non-success selector outcome still materializes the required check on a hosted runner, and successful calls still consume exactly the caller-selected runner). No consumer contract change: same inputs, permissions, and secrets; only the internal hosted fallback label changes. All 104 node:test assertions pass; `git grep ubuntu-slim` over the whole worktree is now clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hTQ64MDYGvqVUEPPWQV6Q
|
Claude finished @kyle-sexton's task in 2m 19s —— View job Code Review
Verdict: Looks good — approve. The change is mechanically correct, internally consistent, well-motivated, and does not touch any security-sensitive surface. Workflow YAML changesAll four
One detail worth noting: Fail-closed contract preserved. The ternary pattern Adjacent comments updated correctly. CLAUDE.md security rules. No event triggers, permissions, secrets scope, or checkout ordering changed. The Test changes
One pre-existing observation (not introduced here): README changesAll three prose references to the old fallback label are updated accurately. The parenthetical "(free on public repos, quota-covered on private)" is technically correct: One minor wording note — the README at the updated line says the fallback is "free on public repos" without caveating that SummaryClean, well-scoped change. Correct everywhere, consistent everywhere, no security-surface changes, tests updated, docs land with the change. Ready to merge. |
## 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>
## What Move the ci-workflows compatibility pin `c36e881` (v0.5.0) → `90f1c54` (v0.6.1) across all 20 workflow references, the pin-metadata test constants, and `release/dependencies.json`. ## Why v0.5.0's `do-not-merge-gate` shells `gh` in its label refetch; v0.6.1 carries the github-script rewrite (melodic-software/ci-workflows#144) plus the ubuntu-slim fallback removal (melodic-software/ci-workflows#141). This repo's Product policy gate requires every ci-workflows reference to move as one reviewed pin with a canonical `@<sha> # vX.Y.Z` comment, so the org's narrow gate-only bump pattern converges here as a full pin move instead. ## Verification All 28 `.github/scripts` tests pass locally, including the pin-metadata inventory (20 references, one SHA, one release). This PR's own do-not-merge run resolves the bumped ref. `git grep c36e881` = 0. v0.5.0..v0.6.1 caller impact reviewed: new `link-check` inputs all carry defaults; fallback runner labels and gate internals changed inside the reusables only. No caller contract changes. ## Related No linked issue. Sibling bumps: melodic-software/provisioning#167, melodic-software/claude-code-plugins#332, melodic-software/dotfiles#220; gate fix melodic-software/ci-workflows#144. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YZxBP1B8Hf7ZRaqDrP6ma9 --------- Co-authored-by: Claude Fable 5 (1M context) <noreply@anthropic.com>
) Registers the remaining `ci-workflows@90f1c54` contract entries so the full six-file gate-pin bumps (#189 and the dotfiles/provisioning/claude-code-plugins companions) pass runner policy: claude-review, link-check, semantic-pr, pr-issue-linkage, zizmor, osv-scanner in `approvedReusableWorkflowContracts`, plus the select-runner reference in `approvedSelectorReferencesByRepositoryOwner`. Review basis, per contract, against each entry's newest already-approved SHA: - claude-review, link-check, osv-scanner: byte-identical at 90f1c54. - semantic-pr, pr-issue-linkage, select-runner: only the hosted fallback label changes (retired `ubuntu-slim` → allowlisted `ubuntu-24.04`, melodic-software/ci-workflows#141) plus comments. - zizmor: version-pin default bump (v1.26.1 → v1.27.0) and curl timeout hardening; input surface unchanged. No input, secret, routing, or caller-permission surface changes anywhere, so each entry copies its predecessor's reviewed body. do-not-merge-gate@90f1c54 was already registered by #190. Component tests updated for the new selector reference and pass (224/224). No linked issue. ## Related - #189 — full-scope standards pin bump this unblocks - #190 — do-not-merge-gate@90f1c54 registration pattern - melodic-software/ci-workflows#141 — ubuntu-slim retirement behind the fallback-label diffs 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YZxBP1B8Hf7ZRaqDrP6ma9 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## What Declare `visibility: "public"` in `.github/runner-policy.json` (selfHostedCi false, exception inventory emptied) and remove the `select-runner` job from all seven workflows: every lane now runs on GitHub-hosted `ubuntu-24.04`, with each reviewed reusable receiving its `runner` input explicitly. Net −181 lines. ## Why The repository is public (confirmed intentional), and the runner-policy engine fails closed on the visibility mismatch — currently red on main's own CI and on #189. For public repositories the engine forbids local-runner selector routing outright (`public-self-hosted-routing`), so the reconciliation is the full hosted conversion, not a one-line declaration. Hosted minutes are free for public repositories, and this removes the standards CI load from the self-hosted fleet entirely. Reusable pins move to `90f1c54` where a reviewed contract exists at that revision; `standards-sync` stays at its newest registered contract (`ec91c34`). This supersedes the selector-pin portion of #189. ## Verification `node components/runner-policy/runner-policy.mjs --root . --repository-visibility public` passes; 224/224 runner-policy component tests pass; actionlint clean on all seven workflows; lefthook gauntlet green. ## Related No linked issue. #189 (superseded selector pins), #195 (policy exception-category removal this builds on), melodic-software/ci-workflows#141/#144 (the 90f1c54 revision). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YZxBP1B8Hf7ZRaqDrP6ma9 Co-authored-by: Claude Fable 5 (1M context) <noreply@anthropic.com>
## What Declare `visibility: "public"` in `.github/runner-policy.json` (selfHostedCi false) and remove the `select-runner` job from all six workflows: every lane runs on GitHub-hosted `ubuntu-24.04`, each reviewed reusable receives its `runner` input explicitly, and pins move to `90f1c54`. This branch also carries the standards-sync materialization from #345 (the 90f1c54 contract registrations the policy engine needs) — #345's diff becomes empty once this merges. ## Why The repository is public but its runner policy declared private — the engine fails closed on that mismatch, which is the `Runner policy` red on #345 and every current PR. Same reconciliation as melodic-software/standards#200: for public repositories the engine forbids local-runner selector routing outright, so the fix is the full hosted conversion. Hosted minutes are free for public repositories, and this removes ccp's CI + claude-review load from the self-hosted fleet — ccp was the largest queue consumer today (37 queued runs at peak). ## Verification The synced policy engine passes with public visibility evidence (`--repository-visibility public`); actionlint clean on all six workflows. Required-check contexts are preserved (`ci-status`, `do-not-merge / do-not-merge`, `pr-issue-linkage / pr-issue-linkage`, `pr-title / pr-title`). ## Related Closes #347. melodic-software/standards#200 (same posture conversion), #345 (sync distribution folded in), melodic-software/ci-workflows#141/#144 (the 90f1c54 revision). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YZxBP1B8Hf7ZRaqDrP6ma9 --------- Co-authored-by: melodic-standards-sync[bot] <300666570+melodic-standards-sync[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5 (1M context) <noreply@anthropic.com>
What
Swap the paid
ubuntu-slimhosted fallback for the standard, freeubuntu-24.04runner across the selector job and the three fail-closedselector-result reporters:
.github/workflows/select-runner.yml— selector job fallback.github/workflows/semantic-pr.yml—pr-titlereporter fallback.github/workflows/do-not-merge-gate.yml—do-not-mergereporter fallback.github/workflows/pr-issue-linkage.yml—pr-issue-linkagereporter fallbackAlso updated:
"least-expensive/inexpensive hosted" runner) so they state the honest
new behavior;
.cjscontract tests that pin the literal(
select-runner.test.cjs,semantic-pr.test.cjs);README.mdlines that describe this fallback/reporterbehavior, so the docs land with the change they describe.
Why
ubuntu-slimis billed even on public repositories. This public repoburned 6,906 minutes for $13.44 net in July (observed in org billing).
ubuntu-24.04is free on public repos and quota-covered on privateones, so the fallback carries no minutes cost while preserving the
fail-closed reporting contract: a non-success selector outcome still
materializes the required check on a hosted runner, and successful calls
still consume exactly the caller-selected runner.
No consumer contract change — same inputs, permissions, and secrets; only
the internal hosted fallback label changes.
Verification
node --teston both.cjssuites: 104 pass, 0 fail.Sanity grep over the whole worktree is clean:
Related
No linked issue. Part of the org-wide Actions zero-spend effort;
companion PR: melodic-software/medley#1570 (parks the paid hosted lanes
there).
🤖 Generated with Claude Code