From fc231d2e196da34a040483a84e1444294ac8c49d Mon Sep 17 00:00:00 2001 From: Neil Galvin Date: Mon, 19 Oct 2026 07:15:15 +0100 Subject: [PATCH 1/3] fix: always run public callers' jobs on GitHub-hosted runners secret-scan, lint-workflows, dependabot-auto-merge and claude-code-review default runs-on to the self-hosted pool, and their comments said no public repo consumes them. Nine public nkg/* repos do. The ones that left runs-on unset, or had no self-hosted runner registered, queued for 24h and were cancelled on every run, and a self-hosted pool would run fork PRs' code on our own hosts. Each job's runs-on now resolves to the hosted label whenever github.event.repository.visibility is "public", overriding the input. Private callers are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A9MVeJNZcr9jAqT62a71i6 --- .github/workflows/claude-code-review.yml | 17 ++++++++++------- .github/workflows/dependabot-auto-merge.yml | 17 ++++++++++++----- .github/workflows/lint-workflows.yml | 19 +++++++++++++------ .github/workflows/secret-scan.yml | 17 ++++++++++++----- CHANGELOG.md | 17 +++++++++++++++++ 5 files changed, 64 insertions(+), 23 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 44f22a1..a09236d 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -7,7 +7,7 @@ on: workflow_call: inputs: runs-on: - description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Public/open-source repos opt out by passing '[\"ubuntu-24.04\"]'." + description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Ignored for public repos, which always run on '[\"ubuntu-24.04\"]'." type: string # Self-hosted by default since 2026-09-20, matching claude.yml. # @@ -34,11 +34,14 @@ on: # advisory regardless: the claude-code-action step below is # continue-on-error, so it cannot block a PR on either runner. # - # PUBLIC REPOS SHOULD OPT OUT, and not only for the free minutes: - # running fork PRs on a self-hosted pool is the hazard GitHub warns - # about. No public repo consumes this reusable today (checked across - # the three orgs and nkg/*), so the default is safe as it stands -- - # but a new public consumer must pass '["ubuntu-24.04"]'. + # PUBLIC REPOS never use this input. The job's runs-on forces + # ["ubuntu-24.04"] whenever github.event.repository.visibility is + # "public": fork PRs on a self-hosted pool are the hazard GitHub + # warns about, hosted minutes are free there, and a public caller + # that omits `runs-on` would otherwise queue for 24h on a pool with + # no runner registered. An event whose payload carries no + # repository object falls back to this input, so public callers + # should still pass a hosted `runs-on` as well. # # Revisit when credits reset; hosted is the right default the moment # it can actually start a job. @@ -87,7 +90,7 @@ jobs: contains(inputs.filter-authors, github.event.pull_request.user.login) || (inputs.filter-first-time-only && (github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' || github.event.pull_request.author_association == 'FIRST_TIMER')) ) - runs-on: ${{ fromJSON(inputs.runs-on) }} + runs-on: ${{ fromJSON(github.event.repository.visibility == 'public' && '["ubuntu-24.04"]' || inputs.runs-on) }} timeout-minutes: ${{ inputs.timeout-minutes }} # `claude-code-action` posts review comments back on the PR, so it # needs write on pull-requests and issues. The `claude.yml` (bot) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index b20d7db..085716f 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -29,7 +29,7 @@ on: workflow_call: inputs: runs-on: - description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Public/open-source repos opt out by passing '[\"ubuntu-latest\"]'." + description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Ignored for public repos, which always run on '[\"ubuntu-latest\"]'." type: string # Self-hosted by default since 2026-09-20. Actions credits are # exhausted across HordiaLabs, Sproncy and Regularmusic, so a hosted @@ -39,9 +39,16 @@ on: # running nothing at all: 4 calls (Sproncy: sproncy-schemas, sproncy-secrets-dashboard, sproncy-transcription, sproncy-website-scrapers). # # Every other consumer already passes `runs-on` explicitly, so this - # changes behaviour only for those that did not. Public repos should - # pass '["ubuntu-latest"]' -- fork PRs on a self-hosted pool are the - # hazard GitHub warns about; no public repo consumes this today. + # changes behaviour only for those that did not. + # + # Public repos never use this input. The job's runs-on forces + # ["ubuntu-latest"] whenever github.event.repository.visibility is + # "public": fork PRs on a self-hosted pool are the hazard GitHub + # warns about, hosted minutes are free there, and a public caller + # that omits `runs-on` would otherwise queue for 24h on a pool with + # no runner registered. An event whose payload carries no + # repository object falls back to this input, so public callers + # should still pass a hosted `runs-on` as well. default: '["self-hosted", "linux", "x64"]' merge-strategy: description: "squash | merge | rebase" @@ -73,7 +80,7 @@ jobs: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.actor.login == 'dependabot[bot]') - runs-on: ${{ fromJSON(inputs.runs-on) }} + runs-on: ${{ fromJSON(github.event.repository.visibility == 'public' && '["ubuntu-latest"]' || inputs.runs-on) }} timeout-minutes: 10 permissions: contents: write diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml index 5a50bd4..2f7cdb0 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-workflows.yml @@ -7,7 +7,7 @@ on: workflow_call: inputs: runs-on: - description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Public/open-source repos opt out by passing '[\"ubuntu-latest\"]'." + description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Ignored for public repos, which always run on '[\"ubuntu-latest\"]'." type: string # Self-hosted by default since 2026-09-20. Actions credits are # exhausted across HordiaLabs, Sproncy and Regularmusic, so a hosted @@ -17,9 +17,16 @@ on: # running nothing at all: 1 call (Sproncy/agent-skills). # # Every other consumer already passes `runs-on` explicitly, so this - # changes behaviour only for those that did not. Public repos should - # pass '["ubuntu-latest"]' -- fork PRs on a self-hosted pool are the - # hazard GitHub warns about; no public repo consumes this today. + # changes behaviour only for those that did not. + # + # Public repos never use this input. The job's runs-on forces + # ["ubuntu-latest"] whenever github.event.repository.visibility is + # "public": fork PRs on a self-hosted pool are the hazard GitHub + # warns about, hosted minutes are free there, and a public caller + # that omits `runs-on` would otherwise queue for 24h on a pool with + # no runner registered. An event whose payload carries no + # repository object falls back to this input, so public callers + # should still pass a hosted `runs-on` as well. default: '["self-hosted", "linux", "x64"]' actionlint-version: type: string @@ -39,7 +46,7 @@ on: jobs: actionlint: - runs-on: ${{ fromJSON(inputs.runs-on) }} + runs-on: ${{ fromJSON(github.event.repository.visibility == 'public' && '["ubuntu-latest"]' || inputs.runs-on) }} timeout-minutes: 10 permissions: contents: read @@ -68,7 +75,7 @@ jobs: yamllint: if: inputs.run-yamllint - runs-on: ${{ fromJSON(inputs.runs-on) }} + runs-on: ${{ fromJSON(github.event.repository.visibility == 'public' && '["ubuntu-latest"]' || inputs.runs-on) }} timeout-minutes: 10 permissions: contents: read diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 0cb8532..ebdebff 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -13,7 +13,7 @@ on: workflow_call: inputs: runs-on: - description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Public/open-source repos opt out by passing '[\"ubuntu-latest\"]'." + description: "Runner label(s) as a JSON array string (parsed with fromJSON). Defaults to the self-hosted pool. Ignored for public repos, which always run on '[\"ubuntu-latest\"]'." type: string # Self-hosted by default since 2026-09-20. Actions credits are # exhausted across HordiaLabs, Sproncy and Regularmusic, so a hosted @@ -23,9 +23,16 @@ on: # running nothing at all: 6 calls (Sproncy: GitHub-runners, agent-skills, sproncy-schemas, sproncy-secrets-dashboard, sproncy-transcription, sproncy-website-scrapers). # # Every other consumer already passes `runs-on` explicitly, so this - # changes behaviour only for those that did not. Public repos should - # pass '["ubuntu-latest"]' -- fork PRs on a self-hosted pool are the - # hazard GitHub warns about; no public repo consumes this today. + # changes behaviour only for those that did not. + # + # Public repos never use this input. The job's runs-on forces + # ["ubuntu-latest"] whenever github.event.repository.visibility is + # "public": fork PRs on a self-hosted pool are the hazard GitHub + # warns about, hosted minutes are free there, and a public caller + # that omits `runs-on` would otherwise queue for 24h on a pool with + # no runner registered. An event whose payload carries no + # repository object falls back to this input, so public callers + # should still pass a hosted `runs-on` as well. default: '["self-hosted", "linux", "x64"]' betterleaks-version: type: string @@ -66,7 +73,7 @@ on: jobs: scan: - runs-on: ${{ fromJSON(inputs.runs-on) }} + runs-on: ${{ fromJSON(github.event.repository.visibility == 'public' && '["ubuntu-latest"]' || inputs.runs-on) }} timeout-minutes: 15 permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c4677..cea0b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ project uses [SemVer](https://semver.org/) for the `vMAJOR.MINOR.PATCH` tags. ## [Unreleased] +### Changed + +- **Public repos always get GitHub-hosted runners from `secret-scan.yml`, + `lint-workflows.yml`, `dependabot-auto-merge.yml` and + `claude-code-review.yml`.** Each job's `runs-on` now overrides the input + with `["ubuntu-latest"]` (`["ubuntu-24.04"]` for the review) whenever + `github.event.repository.visibility` is `public`. The comments added in + 3.6.0 said no public repo consumes these, which was wrong: nine `nkg/*` + public repos call `secret-scan` or `lint-workflows`. The ones that left + `runs-on` unset (`lint-workflows` in `nkg/gha-token-server` and + `nkg/distrobuilder-proxmox-lxc-images`), or had no self-hosted runner + registered (`nkg/gha-nomad-dispatcher`), queued for 24h and were cancelled + on every run. Self-hosted would also have run fork PRs on our own hosts. + Private callers are unchanged. An event with no `repository` in its payload + falls back to the input, so public callers should keep passing a hosted + `runs-on`. + ### Fixed - **`docker-build.yml` no longer tags an image before Trivy has scanned it.** From 4662ead2c6dd122ae311934fb7816c8d74f071af Mon Sep 17 00:00:00 2001 From: Neil Galvin Date: Mon, 19 Oct 2026 07:31:57 +0100 Subject: [PATCH 2/3] docs(changelog): six public consumers, not nine Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A9MVeJNZcr9jAqT62a71i6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cea0b61..e914ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ project uses [SemVer](https://semver.org/) for the `vMAJOR.MINOR.PATCH` tags. `claude-code-review.yml`.** Each job's `runs-on` now overrides the input with `["ubuntu-latest"]` (`["ubuntu-24.04"]` for the review) whenever `github.event.repository.visibility` is `public`. The comments added in - 3.6.0 said no public repo consumes these, which was wrong: nine `nkg/*` + 3.6.0 said no public repo consumes these, which was wrong: six `nkg/*` public repos call `secret-scan` or `lint-workflows`. The ones that left `runs-on` unset (`lint-workflows` in `nkg/gha-token-server` and `nkg/distrobuilder-proxmox-lxc-images`), or had no self-hosted runner From ad16cfe7f5a3b02ecbe1342fb13af59af2c017e8 Mon Sep 17 00:00:00 2001 From: Neil Galvin Date: Mon, 19 Oct 2026 07:40:27 +0100 Subject: [PATCH 3/3] docs(changelog): roll [Unreleased] into [3.7.0] Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A9MVeJNZcr9jAqT62a71i6 --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e914ab0..161cc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ project uses [SemVer](https://semver.org/) for the `vMAJOR.MINOR.PATCH` tags. ## [Unreleased] +## [3.7.0] - 2026-10-19 + ### Changed - **Public repos always get GitHub-hosted runners from `secret-scan.yml`, @@ -21,7 +23,7 @@ project uses [SemVer](https://semver.org/) for the `vMAJOR.MINOR.PATCH` tags. on every run. Self-hosted would also have run fork PRs on our own hosts. Private callers are unchanged. An event with no `repository` in its payload falls back to the input, so public callers should keep passing a hosted - `runs-on`. + `runs-on`. (#93) ### Fixed @@ -1336,7 +1338,8 @@ README; everything below is the actual content of this repo. - Bumped `actions/checkout` from `@v4` to `@v6` across all existing workflows for consistency with new files. -[Unreleased]: https://github.com/nkg/github-actions/compare/v3.6.0...HEAD +[Unreleased]: https://github.com/nkg/github-actions/compare/v3.7.0...HEAD +[3.7.0]: https://github.com/nkg/github-actions/compare/v3.6.0...v3.7.0 [3.6.0]: https://github.com/nkg/github-actions/compare/v3.5.0...v3.6.0 [3.5.0]: https://github.com/nkg/github-actions/compare/v3.4.0...v3.5.0 [3.4.0]: https://github.com/nkg/github-actions/compare/v3.3.0...v3.4.0