Skip to content

ci: let private callers select a runner for the shared workflows - #125

Closed
chrisdpurcell wants to merge 1 commit into
mainfrom
ci/runner-labels-input
Closed

ci: let private callers select a runner for the shared workflows#125
chrisdpurcell wants to merge 1 commit into
mainfrom
ci/runner-labels-input

Conversation

@chrisdpurcell

@chrisdpurcell chrisdpurcell commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Not for merge yet — opened for review. Requested as a draft-the-change task; no tag is cut and the floating v5 is untouched.

Problem

The four reusable workflows here hardcode runs-on: ubuntu-latest and expose no runner input. A reusable workflow bills the caller, so every private consumer burns GitHub-hosted minutes on these gates with no opt-out.

A fleet audit found 35 such jobs across 12 private repositories. The org's 2,000-minute monthly allowance was exhausted on 2026-08-04, which stopped every hosted job org-wide — surfacing as "recent account payments have failed or your spending limit needs to be increased", wording that points at billing when nothing is unpaid.

Change

Each of the four workflows gains an optional runner-labels input, defaulting to "":

runs-on: ${{ inputs.runner-labels && fromJSON(inputs.runner-labels) || 'ubuntu-latest' }}

Callers opt in with a JSON array string:

with:
  runner-labels: '["self-hosted","linux","x64","l3digital-private"]'

fromJSON is what turns it back into the list runs-on expects — a bare string would be read as a single label.

This does not put self-hosted runners on a public repo

Worth stating plainly, because that would be a genuine security problem — fork PRs executing untrusted code on your own hardware.

Per GitHub's docs: "Called workflows that are owned by the same user or organization as the caller workflow can access self-hosted runners from the caller's context."

So:

  • The runner is allocated against the caller's org access. Nothing is registered to or usable by this repository.
  • This repo is public and stays hosted. On its own push / pull_request events the inputs context is empty, the expression falls through to ubuntu-latest, and behavior is byte-identical to today.
  • The runner group independently sets allows_public_repositories=false, so a public repo could not use the pool even if a workflow asked.

Backward compatibility

The default preserves current behavior exactly. All 50 caller jobs currently pinning @v5 are unaffected until each one deliberately passes the input — so moving v5 after merge would be a no-op for every existing consumer.

Verification

  • All four files parse as valid YAML; runner-labels present with default: "", required: false.
  • prettier@3.8.3 --check clean on .github/workflows/*.yml.
  • The pattern is proven in production on the same pool: agent-session-data's Check gate and agent-configs' check/go/format now run on gh-runner-{1,2} in homelab-private, all green.

Not included

  • No version tag and no v5 repoint — deliberate, pending your review.
  • The @v3 ref of lint-markdown.yml is unchanged; control-center is the only consumer still on it and should bump to @v5 instead.
  • Consumer-side adoption (passing the input in each private repo) is follow-up work.

Summary by CodeRabbit

  • New Features
    • Reusable automation workflows can now accept optional runner labels, enabling execution on caller-provided self-hosted runners.
    • Workflows continue to use hosted runners by default when no labels are supplied.
  • Compatibility
    • Direct workflow runs retain their existing hosted-runner behavior.

The four reusable workflows hardcode `runs-on: ubuntu-latest` with no way
for a caller to override it. Because a reusable workflow bills the CALLER,
every private consumer burns GitHub-hosted minutes on these gates and has
no opt-out. Across the fleet that is 35 jobs in 12 private repositories,
against a 2,000-minute monthly org allowance that was exhausted on
2026-08-04.

Each workflow gains an optional `runner-labels` input defaulting to the
empty string, so `runs-on` falls through to `ubuntu-latest` for every caller
that does not set it. All 50 caller jobs currently pinning @v5 are therefore
unaffected until each opts in deliberately.

This attaches no self-hosted runner to this repository. A reusable
workflow's runner is allocated from the caller's context, so a private
caller passing self-hosted labels gets its own organization's runners. This
repository is PUBLIC and must stay on GitHub-hosted runners — runner groups
set allows_public_repositories=false, and self-hosted runners on a public
repository would let fork pull requests execute untrusted code on the host.
On this repo's own push and pull_request events the `inputs` context is
empty, so the expression selects the hosted runner exactly as before.

Callers must pass a JSON array string, e.g.

    with:
      runner-labels: '["self-hosted","linux","x64","l3digital-private"]'

`fromJSON` is what turns it back into the list `runs-on` expects; a bare
label string would be read as a single label.
Copilot AI lite review requested due to automatic review settings August 4, 2026 18:41
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6594c9d6-1243-4e84-a599-c6c951daedae

📥 Commits

Reviewing files that changed from the base of the PR and between cc92d06 and d9a1ba1.

📒 Files selected for processing (4)
  • .github/workflows/format.yml
  • .github/workflows/lint-markdown.yml
  • .github/workflows/validate-markdown-frontmatter.yml
  • .github/workflows/validate-specs.yml

📝 Walkthrough

Walkthrough

The reusable format, Markdown lint, frontmatter validation, and specification validation workflows now accept optional JSON runner labels. They use caller-provided runners when labels are present and otherwise use ubuntu-latest.

Changes

Reusable workflow runner selection

Layer / File(s) Summary
Format and Markdown lint runner selection
.github/workflows/format.yml, .github/workflows/lint-markdown.yml
Both workflows add the optional runner-labels input. Their jobs parse supplied labels and fall back to ubuntu-latest when the input is empty.
Validation workflow runner selection
.github/workflows/validate-markdown-frontmatter.yml, .github/workflows/validate-specs.yml
Both validation workflows add runner-labels and use caller-provided labels when available. They retain ubuntu-latest as the default runner.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: allowing private callers to select runners for shared workflows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/runner-labels-input

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the repository’s four reusable GitHub Actions workflows to let private same-organization callers choose their own runner labels (including self-hosted) while keeping this public repository’s direct push/PR behavior effectively unchanged (defaulting to ubuntu-latest).

Changes:

  • Added an optional runner-labels workflow input (string, default "") to each reusable workflow.
  • Switched each job’s runs-on to an expression that selects either the caller-provided runner labels (parsed via fromJSON) or falls back to ubuntu-latest.
  • Added inline documentation clarifying that runner allocation occurs in the caller’s context and does not attach runners to this public repo.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
.github/workflows/validate-specs.yml Adds runner-labels input and uses it to parameterize runs-on for the specs gate.
.github/workflows/validate-markdown-frontmatter.yml Adds runner-labels input and makes runs-on selectable for frontmatter validation.
.github/workflows/lint-markdown.yml Adds runner-labels input and makes the markdownlint job runner configurable for callers.
.github/workflows/format.yml Adds runner-labels input and makes the Prettier job runner configurable for callers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chrisdpurcell

Copy link
Copy Markdown
Collaborator Author

Triage: the change is right; the delivery mechanism is the blocker

Review of the approach, plus what the red CI actually means. Noted that this is explicitly not for merge yet.

The design is the correct fix

Per the runner-ownership rule, a shared reusable workflow that hardcodes runs-on should be fixed in the callee so every consumer can comply, rather than worked around per repository. Exposing runner-labels is exactly that, and the reasoning in the description holds up:

  • A reusable workflow bills the caller, so private consumers currently burn hosted minutes with no opt-out.
  • Self-hosted runners are allocated from the caller's context, so nothing is registered to or reachable by this public repository.
  • The homelab-private group sets allows_public_repositories=false, an independent backstop.
  • The "" default means this repo's own push/pull_request runs fall through to ubuntu-latest unchanged — public repos stay hosted, as they must.

The fromJSON detail is right too: runs-on needs a list, and a bare string would be read as a single label.

Why CI is red — and it is not this PR's logic

ERROR CP-MODIFIED-MANAGED .github/workflows/validate-markdown-frontmatter.yml
ERROR CP-MODIFIED-MANAGED .github/workflows/validate-specs.yml

All four workflows edited here are installed payload resources, not hand-maintained files:

Workflow Payload
format.yml markdown-tooling 1.12
lint-markdown.yml markdown-tooling 1.12
validate-markdown-frontmatter.yml markdown-frontmatter 1.8
validate-specs.yml project-spec 1.6

Editing the installed copy desynchronizes it from the version it declares. Released payloads are immutable, so this capability has to ship as new payload minors across all three standards, with the canonical bytes changed under standards/<name>/versions/<new>/resources/ and the installed copies reconciled from them — not edited directly.

One point worth raising before that cycle

runner-labels is a public interface addition to three published packages. Since consumers pin @v5, the input's name, its JSON-array-string encoding, and the empty-string default become a compatibility contract the moment the floating tag moves. Worth settling deliberately — particularly whether a JSON-encoded string is the shape you want long-term versus a simpler single-label input, because changing it later is a breaking change for anyone who adopted it.

Also relevant

#33 and #110 want to edit these same payload-managed workflows for GitHub Action SHA bumps. They are natural passengers on this same payload cycle rather than three separate ones — this PR is the capability change that justifies cutting it.

Automated triage; no code changed.

chrisdpurcell added a commit that referenced this pull request Aug 5, 2026
Dependabot cannot produce a mergeable PR in this repository. Four root
workflows are installed payload resources byte-locked to published package
versions, so a bump to one desynchronizes it from the version it declares
(CP-MODIFIED-MANAGED); released payloads are immutable, so the bump has to
travel through a new payload minor instead. The Node pins fail differently:
their versions are documented behaviorally in shipped package prose, so
moving one silently invalidates guidance this repository publishes.

All six open PRs (#33, #110, #111, #112, #113 and the #125 capability change)
are blocked on that same producer/consumer split rather than on anything
upstream. Each carries a triage comment explaining what it needs.

Scoping Dependabot to unmanaged paths only was rejected: the ignore list
would duplicate the lock's record of which paths are payload-managed, and
would rot silently whenever a package claims or relinquishes one.

Dependabot alerts stay enabled at the repository level. They raise no pull
requests, and security updates were already disabled, so vulnerability
visibility is unchanged.

Replaces the config-shape assertion with an absence assertion so re-adding
the file is a deliberate decision rather than an accident.
chrisdpurcell added a commit that referenced this pull request Aug 5, 2026
…uv 9

Supersedes the payload-coupled scope of PRs #125 and #33 for this
package. The 1.9 self-hosted workflow adds the optional runner-labels
workflow_call input allocated from the caller's context and advances
astral-sh/setup-uv to 9.0.0 with prune-cache: true preserving the v8
cache behavior. 1.8 bytes are untouched and stay advertised; the root
workflow keeps the 1.8 render until the release-prep reconcile, so the
1.8 root-parity assertion moves to a reviewed-pin assertion. The
catalogs/5.toml default advance and family landing pages batch at
release prep with the other v5.16.0 cuts.
chrisdpurcell added a commit that referenced this pull request Aug 5, 2026
Supersedes the payload-coupled scope of PR #125 for this package. The
1.7 self-hosted workflow adds the optional runner-labels input and the
setup-uv 9.0.0 pin with prune-cache: true; both digest-history chains
(payload known_content_digests and the provider self-host set) append
the new sha256:52e058a3 generation so earlier consumers still
authenticate. adopt.md documents runner-labels and gains the
SL-STRUCTURE troubleshooting row for the new strict-lint gate. The
root workflow keeps the released 1.6 render until the release-prep
reconcile; catalog advance batches with the other v5.16.0 cuts.
Aggregate sha256:2d012e3d.
chrisdpurcell added a commit that referenced this pull request Aug 5, 2026
…#114, #119)

The documented markdownlint command now selects parent-repo tracked
files only: git ls-files with :(glob) positives and :(glob,exclude)
negations, literal paths prefixed with ':' (a bare '#'- or '!'-leading
filename is otherwise parsed as a glob and silently dropped), and
--no-globs so a consumer runner config cannot re-widen the selection —
each part probed against markdownlint-cli2 0.23.2 in a nested-repo
fixture, where CLI negations provably do not filter literal paths. The
unbounded --fix recovery recipe is bounded the same way. The #119
unmatched-pattern hazard is documented with its flag and proven
tolerated by both rendered forms. self-host-format.yml gains
runner-labels, setup-node 7.0.0, and the prettier@3.9.6 enforced pin;
self-host-lint-markdown.yml gains runner-labels (payload scope of PRs
#110/#113/#125); prose re-cites 0.23.2. Root workflows keep released
1.12 bytes until the release-prep reconcile. Aggregate sha256:be1e0630.
@chrisdpurcell

Copy link
Copy Markdown
Collaborator Author

Superseded in v5.16.0 with the payload work this change needed: the runner-labels input landed verbatim on all four reusable workflows through their owning package cuts — markdown-tooling 1.13 (format, lint-markdown; commit 7ccdbc9), markdown-frontmatter 1.9 (commit 6f8c793), project-spec 1.7 (commit 8a2d1b9's activation re-render, cut 8d809ca) — plus validate-specs via its managed render. The root workflows were re-rendered by the release reconcile at 8a2d1b9, and the caller-context comment from this PR ships with each payload. The v5 repoint happened with the release.

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.

2 participants