Skip to content

feat(workflows): every reusable workflow takes a runner input - #15

Merged
stephane-segning merged 1 commit into
mainfrom
claude/runner-input
Sep 22, 2026
Merged

stephane-segning merged 1 commit into
mainfrom
claude/runner-input

Conversation

@stephane-segning

Copy link
Copy Markdown
Contributor

1. Summary

This PR changes:

  • Adds a runner input (type: string, required: false, default: ubuntu-latest) to all
    six workflow_call workflows: lint.yml, trivy.yml, sast.yml,
    issue-governance.yml, pr-governance.yml, sync-repo-settings.yml.
  • Rewrites all seven job runs-on: lines across them to ${{ inputs.runner }}.
  • Adds a "Choosing a runner" section to README.md, including what each workflow needs
    from a non-hosted runner — which is not uniform across the six.

It solves:

  • https://github.com/vaam-apps/vaam-apps/pull/291 — that repo moved its own four hosted
    jobs onto its ARC pool and could not move lint, trivy or issue-governance,
    because those are uses: calls into this repository and runs-on lives here.

Every existing caller is unchanged: the default is the value that was hardcoded.


2. Intent

The intent of this PR is:

A GitHub-hosted job does not fail when an account's hosted minutes are unavailable — it
never runs, and reports that as conclusion: failure, steps: 0, runner_name: "". No
log, no annotation, nothing naming the cause. From 2026-09-19 that hit four jobs in
vaam-apps/vaam-apps on every branch for days, and because two of them are named "title
is a conventional commit" and "the squash message parses as a conventional commit", a
billing problem read as a malformed PR title on every open PR.

A caller cannot fix that alone, and the half-fix is worse than none. With its own gate job
moved to ARC and green again, the two quality jobs that gate dispatches went from
skipping — neutral, and quietly hiding the dependency — to dispatched and refused.
Adopting a self-hosted pool halfway relocates the outage into this org's checks instead of
removing it. This input is what makes the move completable.


3. Scope

In Scope

  • The six workflow_call workflows and their seven jobs.
  • README.md: a new "Choosing a runner" section.

Out of Scope

  • pr-title.yml and release-please.yml in this repository. They are pull_request and
    push triggered — this repo's own CI, not callable — so a runner input would mean
    nothing for them. They are still on ubuntu-latest and would still be dead in the same
    outage; deciding whether this repo's own CI should leave hosted runners is a separate
    question from giving callers a choice, and I have not pre-empted it.
  • Changing any caller. vaam-apps/vaam-apps will pass runner: and re-pin in its own PR
    once this is released.
  • A single label only, deliberately. runs-on: ${{ inputs.runner }} takes one label;
    [self-hosted, macOS, ARM64] would need fromJSON, which changes the expression for
    every caller including those passing a plain string. No caller needs it today. The README
    says so rather than leaving it to be discovered.

4. Verification

I verified this change by:

  • Running automated tests
  • Running manual tests
  • Checking logs
  • Checking metrics
  • Testing error cases
  • Testing permissions/security behavior
  • Testing rollback or failure behavior, if relevant

Commands run:

actionlint
python3 -c "import yaml, glob; ..."   # per-workflow schema assertion, below

Results:

$ actionlint
(no output, exit 0 — all eight workflows)

$ python3 … assert the input and every runs-on
issue-governance.yml:   runner input OK, jobs=['validate']
lint.yml:               runner input OK, jobs=['lint']
pr-governance.yml:      runner input OK, jobs=['validate']
pr-title.yml:           not callable — skipped
release-please.yml:     not callable — skipped
sast.yml:               runner input OK, jobs=['codeql', 'docs']
sync-repo-settings.yml: runner input OK, jobs=['sync']
trivy.yml:              runner input OK, jobs=['scan']

The assertion checks, per callable workflow, that runner exists with
type: string, required: false, default: ubuntu-latest, and that every job's
runs-on is exactly ${{ inputs.runner }} — read out of the parsed YAML rather than
eyeballed from the diff, so a job left behind cannot pass.

What is not verified, and the honest limit of this PR: nothing here runs any of these
on a self-hosted runner, and this change adds no such test. The default is unchanged, so
this repo's own CI still exercises only the hosted path. The first real evidence that a
given pool satisfies a given workflow is a caller switching it over. The README says this
and says to switch one at a time.


5. Screenshots / Evidence

The requirement table added to the README, which is the part most likely to bite and was
read out of the workflows rather than assumed:

workflow needs why
lint.yml Docker, Linux super-linter/super-linter/slim is a container action
trivy.yml Docker, Linux aquasecurity/trivy-action, likewise
sast.yml network, disk github/codeql-action downloads the CodeQL bundle per run
issue-governance.yml gh, jq 7 gh api calls
pr-governance.yml gh, jq 5 gh api calls
sync-repo-settings.yml gh, jq, python3 the widest tool surface of the six

The two container actions are the ones to notice: a pool without Docker, or a non-Linux
one, fails lint and trivy for a reason that has nothing to do with capacity.


6. Risk Assessment

Risk level:

  • Low
  • Medium
  • High

Potential risks:

  • A caller points a workflow at a pool that cannot run it — most likely lint or
    trivy on a pool without Docker. Mitigated by the README table above, and bounded: it
    affects only the repo that opted in, and reverts by deleting one with: line.
  • The default is misread as "nothing changed anywhere" — true for behaviour, not for
    the interface. Six reusable workflows gain a public input, and release-please will cut a
    minor for it.
  • Self-hosted runners see this org's App token. Any caller moving these off hosted
    runners is choosing to run App-authenticated jobs on its own infrastructure. That is the
    caller's decision to make and this PR only makes it expressible — worth naming, since
    lint.yml pushes fix commits and sync-repo-settings.yml writes rulesets.

Mitigation:

  • The default is the exact value that was hardcoded, so no caller changes until it asks to.
  • The README states the per-workflow requirements and that none of them are tested on a
    self-hosted runner here.

7. AI Usage Declaration

AI was used for:

  • Understanding existing code
  • Generating code
  • Refactoring
  • Generating tests
  • Drafting documentation
  • Reviewing the diff
  • Not used

Human verification:

  • I understand every meaningful change in this PR
  • I checked generated code manually
  • I checked generated tests manually
  • I removed unsupported AI assumptions
  • I accept responsibility for this PR

The per-workflow requirement table was derived by reading each workflow's uses: steps at
the pinned shas, not inferred from workflow names — the two container actions are the
finding that table exists for.


8. Reviewer Focus

Please focus your review on:

  • Correctness
  • Architecture
  • Security
  • Performance
  • Tests
  • Maintainability
  • Product intent
  • Edge cases

Specifically:

  • Whether sync-repo-settings.yml should have been included. It is manual and
    administrative rather than a PR check, so it is not part of the outage this PR responds
    to. I included it for uniformity — six callable workflows, one rule — on the view that a
    list of "the ones that take a runner" which is a subset of "the ones that are callable"
    is the kind of split that goes stale. Easy to drop if you disagree.
  • The single-label constraint, if any repo in this org is likely to want a multi-label
    self-hosted target sooner than I think.
  • Input naming. runner reads well at the call site (with: runner: ubuntu-amd-x64);
    runs-on would mirror the Actions key but shadows a reserved-looking name.

🤖 Generated with Claude Code

All six `workflow_call` workflows hardcoded `runs-on: ubuntu-latest`, so a
caller could not run them anywhere else. They now take `runner`, a string
defaulting to `ubuntu-latest` — every existing caller is unchanged.

A GitHub-hosted job does not fail when an account's hosted minutes are
unavailable. It never runs, and reports that as `conclusion: failure` with
`steps: 0` and an empty `runner_name` — no log, no annotation, nothing
naming the cause. In vaam-apps/vaam-apps that hit four jobs from 2026-09-19
onward, two of them named "title is a conventional commit" and "the squash
message parses as a conventional commit", so a billing problem read as a
malformed PR title on every open PR for days.

That repo could move its own four jobs to its ARC pool. It could not move
`lint`, `trivy` or `issue-governance`, because those are `uses:` calls into
this repository and `runs-on` lives here. And the half-move is worse than no
move: with the caller's own gate job green again, the two quality jobs it
gates went from `skipping` — neutral, and quietly hiding the dependency — to
dispatched and refused. Adopting a self-hosted pool halfway relocates the
outage into this org's checks rather than removing it.

A single label, not a JSON array. `runs-on: ${{ inputs.runner }}` takes one
label; `[self-hosted, macOS, ARM64]` would need `fromJSON`, which changes
the expression for every caller including the ones passing a plain string.
No caller needs it, so it is not done — the README says so rather than
leaving it to be discovered.

README gains "Choosing a runner", including the part that is not uniform
across the six: `lint` and `trivy` both run container actions
(super-linter/slim, trivy-action) and therefore need Docker and a Linux
runner, `sast` downloads the CodeQL bundle per run, and the three governance
workflows need `gh`, `jq` and — for sync-repo-settings — `python3`. Nothing
here tests any of them on a self-hosted runner and this change adds no such
test, because the default is unchanged and the org's own CI still exercises
only the hosted path. That is stated in the README too.

Verified: `actionlint` clean over all eight workflows, and a YAML assertion
that each callable workflow declares the input with the right type, default
and `required: false`, and that all seven jobs across them now read
`${{ inputs.runner }}`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stephane-segning
stephane-segning merged commit 4340229 into main Sep 22, 2026
4 checks passed
@vaam-apps vaam-apps Bot mentioned this pull request Sep 22, 2026
@stephane-segning
stephane-segning deleted the claude/runner-input branch September 22, 2026 05:46
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