Skip to content

feat: add governed runner input to three hosted-only reusables - #124

Merged
kyle-sexton merged 1 commit into
mainfrom
feat/runner-input-hosted-only-reusables
Jul 16, 2026
Merged

feat: add governed runner input to three hosted-only reusables#124
kyle-sexton merged 1 commit into
mainfrom
feat/runner-input-hosted-only-reusables

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

  • Add an optional runner input (type: string, default: ubuntu-24.04) and route runs-on through ${{ inputs.runner }} in three previously hosted-only reusable workflows: link-check.yml, standards-sync.yml (all three jobs — plan, attest, sync), and pulumi-version-drift-check.yml.
  • Mirrors the governed runner-input contract already carried by zizmor.yml and claude-review.yml: selector-routed callers pass their runner label; the ubuntu-24.04 default keeps direct and public callers on the same GitHub-hosted image.
  • Permissions blocks and every SHA-pinned uses: step are byte-for-byte unchanged.
  • Update pulumi-deploy-guard.test.cjs to assert the governed runner input and runs-on: ${{ inputs.runner }} in place of the retired hosted pin.

Why

Wave 1 of the private-repo floor conversion (melodic-software/github-iac#78, decision of record 2026-07-16). These three are the hosted-only privileged reusables still lacking a runner-input contract. Adding the input here is the source-side prerequisite that lets:

  • Wave 2melodic-software/standards register runner-input contracts for these three merged SHAs in components/runner-policy/policy.json, and
  • Wave 3 — each private-repo caller add a select-runner job and route these to the fleet, dropping the hosted-control-plane / privileged-control-plane exceptions.

Under the org's strict self-hosted-only policy the selector returns the fleet label or the ci-runner-selection-failed sentinel (never empty), so a converted caller spends zero hosted minutes; the retained ubuntu-24.04 default only serves direct and public callers, which this repo's own dogfood exercises.

Scope note — source only

This PR changes only the reusable definitions. It adds/repins no caller, and no consumer references these new SHAs until Wave 2 registers them and Wave 3 wires them. The optional-default form (matching zizmor / claude-review) is deliberate: these are scheduled maintenance jobs, not required PR checks, so they do not need the required/no-default + sentinel-guard caller contract that osv-scanner and semantic-pr use.

Validation

  • actionlint (schema + expression syntax) on all three workflows — clean.
  • node --test across the affected and runner-posture guard suites (pulumi-deploy-guard, hosted-runner-pins, link-check, network-timeout-policy, standards-sync-app-attestation, zizmor-native, osv-scanner-pin) — 59 passed, 0 failed.
  • select-runner.yml untouched.

Related

  • Refs melodic-software/github-iac#78 (epic — zero hosted floor for private repos).
  • Prerequisite for the standards runner-policy registration PR (Wave 2) and the per-repo caller conversions (Wave 3).

No linked issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt

Add the optional `runner` input (string, default `ubuntu-24.04`) and route
`runs-on` through `${{ inputs.runner }}` for link-check, standards-sync (all
three jobs), and pulumi-version-drift-check, mirroring the zizmor and
claude-review governed runner-input contracts. Permissions and every
SHA-pinned step are unchanged; the `ubuntu-24.04` default keeps direct and
public callers on the same hosted image while strict private callers pass
their governed selector output.

Wave 1 of the private-repo floor conversion (melodic-software/github-iac#78):
the reusable-source prerequisite that lets standards register runner-input
contracts for these three (Wave 2) and per-repo callers route them to the
fleet (Wave 3).

Update the Pulumi drift-guard test to assert the governed runner input and
`runs-on: ${{ inputs.runner }}` in place of the retired hosted pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 43s —— View job


PR Review: feat/runner-input-hosted-only-reusables

  • Gather context and understand the request
  • Read changed files
  • Review for correctness, security, and CLAUDE.md alignment
  • Post findings

Summary

The change is mechanically correct and well-scoped. It mirrors the established runner-input contract from zizmor.yml and claude-review.yml byte-for-byte in structure, leaves all SHA pins and permission blocks untouched, and preserves the ubuntu-24.04 default so existing direct callers are unaffected. I have two observations — one minor test-coverage gap, one latent architectural coupling.


Correctness

Input/runs-on wiring — link-check.yml

link-check.yml L13–16, L40: runner added as first input; runs-on switched to ${{ inputs.runner }}. Correct.

Input/runs-on wiring — pulumi-version-drift-check.yml

pulumi-version-drift-check.yml L7–11, L22: inputs: block created under the previously bare workflow_call:, runner added, job switched to ${{ inputs.runner }}. Correct.

Input/runs-on wiring — standards-sync.yml

standards-sync.yml L9–12: runner added as first input; all three jobs (plan at L53, attest at L132, sync at L357) switched to ${{ inputs.runner }}. Correct — every job in the workflow is covered.

Test update — pulumi-deploy-guard.test.cjs

Lines 152–156: The old assertion assert.match(drift, /^ {4}runs-on: ubuntu-24\.04$/mu) is replaced with two assertions — one for the full input block structure (indentation, type, default) and one for runs-on: ${{ inputs.runner }}. The indentation in the regex (^ {4}inputs:, {6}runner:, {8}description/type/default) correctly reflects the YAML structure under on.workflow_call. The $mu flag ensures multiline and unicode matching. Correct.


Security

  • workflow_call only — no pull_request_target or workflow_run with secrets introduced. ✓
  • All uses: SHA pins are byte-for-byte unchanged across all three workflows. ✓
  • Permission blocks on all jobs are unchanged. ✓
  • inputs.runner flows only into runs-on:, which is evaluated by GitHub infrastructure before job dispatch — no shell injection surface. ✓
  • No secrets: inherit added (validated by hosted-runner-pins.test.cjs's existing assertion). ✓
  • ubuntu-24.04 default is a pinned, non-moving label — passes the hosted-runner-pins.test.cjs moving-label check. ✓

Observations

1. Test coverage gap for link-check.yml and standards-sync.yml runner contracts (low)

pulumi-deploy-guard.test.cjs now asserts the runner input block and runs-on: ${{ inputs.runner }} for the drift workflow, but neither link-check.test.cjs nor standards-sync-app-attestation.test.cjs adds a parallel assertion for their workflows. A future commit that reverts runs-on back to a hardcoded pin in either file would not be caught by the test suite. This is consistent with how those test files are scoped (rolling-issue behaviour and App attestation respectively), but the asymmetry is worth noting. Fix this →

2. standards-sync.yml has an implicit Linux AMD64 runner requirement (informational)

The plan and sync jobs download yq_linux_amd64 by name and verify its SHA256. This silently couples inputs.runner to a Linux AMD64 host. If the fleet selector ever returns a non-AMD64 label for this workflow, the Install checksum-verified yq step will fail at runtime rather than at submission. zizmor.yml guards against this class of failure with an explicit early step (echo '::error::The native zizmor release requires a Linux X64 runner.'). This is a pre-existing coupling — not introduced by this PR — but surfacing a runner input makes it slightly more discoverable. No action required here unless the fleet label set expands beyond AMD64 Linux.


Verdict

Approve. The two items above are not blockers. Observation 1 is a low-priority test-coverage improvement; observation 2 is pre-existing and informational only. The change correctly implements the governed runner-input contract and is safe to merge as the Wave 1 source-side prerequisite.

@kyle-sexton
kyle-sexton merged commit 3dfb184 into main Jul 16, 2026
33 checks passed
@kyle-sexton
kyle-sexton deleted the feat/runner-input-hosted-only-reusables branch July 16, 2026 20:17
kyle-sexton added a commit to melodic-software/standards that referenced this pull request Jul 16, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant