From 5b3369a8f0fa02dc0fb556242ccdcdb852151be9 Mon Sep 17 00:00:00 2001 From: Hendrik Brombeer Date: Wed, 30 Sep 2026 11:00:59 +0200 Subject: [PATCH] ci: route private repos to the shared grounds-runners pool The runner scale sets on hbr1 are now registered at org level, so one pool can serve every private repository instead of one scale set per repo. Private repos go to `grounds-runners`; public ones stay on GitHub-hosted runners, and the org's Default runner group refuses public repositories as a second line. A missing event payload now falls back to hosted amd64 `ubuntu-24.04` instead of the Blacksmith label, which no longer takes jobs; hosted arm64 would not do either, since it does not exist for private repos. --- .../workflows/docker-gradle-build-push.yml | 7 ++-- .github/workflows/gradle-ci.yml | 32 ++++++-------- .github/workflows/gradle-publish.yml | 32 ++++++-------- .github/workflows/label-sync.yml | 22 +++++----- .../platform-test-preview-slashcmd.yml | 20 ++++----- .github/workflows/platform-test-preview.yml | 42 +++++++++---------- .github/workflows/release-please.yml | 22 +++++----- tests/test_docker_workflow.py | 30 ++++++++++++- 8 files changed, 107 insertions(+), 100 deletions(-) diff --git a/.github/workflows/docker-gradle-build-push.yml b/.github/workflows/docker-gradle-build-push.yml index 40919f0..e92aeee 100644 --- a/.github/workflows/docker-gradle-build-push.yml +++ b/.github/workflows/docker-gradle-build-push.yml @@ -36,13 +36,14 @@ jobs: # happened to grounds-forge 4.3.0 in its migrate init container. Building # an amd64 image on an arm64 runner would mean QEMU for the whole build. # Public fork PRs remain on GitHub-hosted runners; private repositories - # use their own ARC scale set. Missing repository payloads safely fall - # back to GitHub-hosted rather than the unavailable Blacksmith pool. + # use the shared self-hosted pool `grounds-runners` (org-level ARC; the + # org's Default runner group refuses public repositories). Missing + # repository payloads safely fall back to GitHub-hosted. runs-on: >- ${{ inputs.runner != '' && inputs.runner || (github.event.repository.visibility == 'public' && 'ubuntu-24.04') - || github.event.repository.name + || (github.event.repository.name && 'grounds-runners') || 'ubuntu-24.04' }} # Cold ARC builds can exceed 30m downloading builder/runtime base images # even after Gradle succeeds. Keep the default repo-ARC path bounded at diff --git a/.github/workflows/gradle-ci.yml b/.github/workflows/gradle-ci.yml index 42e2670..a7007ff 100644 --- a/.github/workflows/gradle-ci.yml +++ b/.github/workflows/gradle-ci.yml @@ -7,11 +7,10 @@ on: description: > Runner label. Leave unset and the job picks one by repository visibility: GitHub's arm64 hosted runner in public repos, where it - is free and has 4 vCPUs, and a 2 vCPU arm64 Blacksmith runner in - private repos, where GitHub-hosted arm64 is not offered. Set it - explicitly to override — an amd64 label if a test image has no - arm64 variant, or a larger Blacksmith label for a build that is - measurably CPU-bound and on someone's critical path. + is free and has 4 vCPUs, and the shared self-hosted `grounds-runners` + pool (amd64) in private repos, where GitHub-hosted arm64 is not + offered. Set it explicitly to override, e.g. `grounds-runners-large` + for a build that needs more memory than the shared pool gives. required: false type: string default: "" @@ -33,26 +32,21 @@ concurrency: jobs: build: - # Falls back to Blacksmith whenever `visibility` is absent from the event - # payload, because GitHub-hosted arm64 does not exist for private repos and - # guessing wrong there means "no runner matches" rather than a bigger bill. - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ inputs.runner != '' && inputs.runner || (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 30 steps: - name: Check out diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index b0061dd..2330e7d 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -7,11 +7,10 @@ on: description: > Runner label. Leave unset and the job picks one by repository visibility: GitHub's arm64 hosted runner in public repos, where it - is free and has 4 vCPUs, and a 2 vCPU arm64 Blacksmith runner in - private repos, where GitHub-hosted arm64 is not offered. Set it - explicitly to override — an amd64 label if a test image has no - arm64 variant, or a larger Blacksmith label for a build that is - measurably CPU-bound and on someone's critical path. + is free and has 4 vCPUs, and the shared self-hosted `grounds-runners` + pool (amd64) in private repos, where GitHub-hosted arm64 is not + offered. Set it explicitly to override, e.g. `grounds-runners-large` + for a build that needs more memory than the shared pool gives. required: false type: string default: "" @@ -33,26 +32,21 @@ concurrency: jobs: release: - # Falls back to Blacksmith whenever `visibility` is absent from the event - # payload, because GitHub-hosted arm64 does not exist for private repos and - # guessing wrong there means "no runner matches" rather than a bigger bill. - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ inputs.runner != '' && inputs.runner || (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 30 steps: - name: Checkout diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml index 10b7aac..18b1f05 100644 --- a/.github/workflows/label-sync.yml +++ b/.github/workflows/label-sync.yml @@ -9,27 +9,25 @@ concurrency: jobs: sync-labels: - # Free GitHub-hosted runner in public repos; Blacksmith in private ones, + # Free GitHub-hosted runner in public repos; `grounds-runners` in private ones, # where GitHub-hosted arm64 is not offered. Tests `visibility` rather than # `repository.private` because GitHub coerces `==` operands to numbers, so # an absent `private` field would compare equal to false and hand a private # repo a label that does not exist there. - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 10 steps: - name: Download labels.yml diff --git a/.github/workflows/platform-test-preview-slashcmd.yml b/.github/workflows/platform-test-preview-slashcmd.yml index 52ab393..d935228 100644 --- a/.github/workflows/platform-test-preview-slashcmd.yml +++ b/.github/workflows/platform-test-preview-slashcmd.yml @@ -52,22 +52,20 @@ jobs: # `if: github.event.issue.pull_request != null`, but we double-up # here because reusable workflows shouldn't trust their callers. if: github.event.issue.pull_request != null - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 10 permissions: issues: write diff --git a/.github/workflows/platform-test-preview.yml b/.github/workflows/platform-test-preview.yml index 29e0f5e..0eb469a 100644 --- a/.github/workflows/platform-test-preview.yml +++ b/.github/workflows/platform-test-preview.yml @@ -87,7 +87,7 @@ jobs: runs-on: >- ${{ (github.event.repository.visibility == 'public' && 'ubuntu-24.04') - || github.event.repository.name + || (github.event.repository.name && 'grounds-runners') || 'ubuntu-24.04' }} timeout-minutes: 30 outputs: @@ -171,22 +171,20 @@ jobs: mint: if: github.event.action != 'closed' needs: build - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 20 permissions: pull-requests: write @@ -386,22 +384,20 @@ jobs: # ── Evict on PR close ───────────────────────────────────────────── evict: if: github.event.action == 'closed' - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 10 steps: - name: Mint forge access token diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 1bdd54b..6579baf 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -14,27 +14,25 @@ concurrency: jobs: release-please: name: Create Release - # Free GitHub-hosted runner in public repos; Blacksmith in private ones, + # Free GitHub-hosted runner in public repos; `grounds-runners` in private ones, # where GitHub-hosted arm64 is not offered. Tests `visibility` rather than # `repository.private` because GitHub coerces `==` operands to numbers, so # an absent `private` field would compare equal to false and hand a private # repo a label that does not exist there. - # Self-hosted ARC runners on hbr1. The label is the calling repository's - # own name, which is how each repo reaches its own scale set without - # passing anything. - # # Read the three branches in order. A public repo keeps a GitHub-hosted # runner: on a self-hosted pool a fork PR would run arbitrary code on - # hbr1, which also hosts production containers. A private repo goes to - # its own runner. And if `repository` is missing from the event payload - # -- which is why this used to fall back to Blacksmith -- the name is - # empty and Blacksmith still catches it, because an empty label matches - # no runner at all and the job would queue until someone cancels it. + # hbr1, which also hosts production containers (the org's Default runner + # group refuses public repositories as a second line). A private repo goes + # to `grounds-runners`, the shared self-hosted ARC pool (org-level, on + # hbr1 today). And if `repository` is missing from the event payload, the + # job lands on hosted amd64 `ubuntu-24.04`, which exists for public and + # private repos alike -- hosted arm64 does not exist for private ones, and + # a label no runner matches queues until someone cancels the job. runs-on: >- ${{ (github.event.repository.visibility == 'public' && 'ubuntu-24.04-arm') - || github.event.repository.name - || 'blacksmith-2vcpu-ubuntu-2404-arm' }} + || (github.event.repository.name && 'grounds-runners') + || 'ubuntu-24.04' }} timeout-minutes: 10 steps: - name: Checkout code diff --git a/tests/test_docker_workflow.py b/tests/test_docker_workflow.py index 6de4510..d9cf7f1 100644 --- a/tests/test_docker_workflow.py +++ b/tests/test_docker_workflow.py @@ -74,7 +74,7 @@ def test_build_and_cache_contract_for_public_and_private_events(self): self.assertEqual(len(pushes), 1) self.assertEqual(len(cache_writes), 1) self.assertEqual(cache_writes[0]['with']['cache-to'], '${{ steps.layer_cache.outputs.to }}') - expected_runner = 'ubuntu-24.04' if visibility == 'public' else 'plugin-notifications' + expected_runner = 'ubuntu-24.04' if visibility == 'public' else 'grounds-runners' self.assertEqual(resolve(job['runs-on'], context).strip(), expected_runner) expected_budget = 30 if visibility == 'public' else 60 self.assertEqual(int(resolve(job['timeout-minutes'], context)), expected_budget) @@ -149,6 +149,34 @@ def test_cold_arc_budget_preserves_hosted_override_and_fallback_limits(self): self.assertEqual(budget, expected) self.assertLessEqual(budget, 60) + def test_every_central_workflow_routes_private_repos_to_the_shared_pool(self): + """Public -> GitHub-hosted, private -> grounds-runners, no payload -> hosted amd64. + + The last case matters: hosted arm64 does not exist for private repos, and a + public repo sent to the self-hosted pool is refused by the org's runner group, + so either wrong guess queues a job forever. + """ + checked = 0 + for path in sorted(WORKFLOW.parent.glob('*.yml')): + jobs = (yaml.safe_load(path.read_text()) or {}).get('jobs', {}) + for name, job in jobs.items(): + runs_on = job.get('runs-on') + if not isinstance(runs_on, str) or 'grounds-runners' not in runs_on: + continue + checked += 1 + base = {'inputs.runner': ''} + with self.subTest(workflow=path.name, job=name): + private = resolve(runs_on, {**base, 'github.event.repository.visibility': 'private', + 'github.event.repository.name': 'plugin-match'}).strip() + self.assertEqual(private, 'grounds-runners') + public = resolve(runs_on, {**base, 'github.event.repository.visibility': 'public', + 'github.event.repository.name': 'service-maps'}).strip() + self.assertTrue(public.startswith('ubuntu-24.04'), public) + missing = resolve(runs_on, {**base, 'github.event.repository.visibility': '', + 'github.event.repository.name': ''}).strip() + self.assertEqual(missing, 'ubuntu-24.04') + self.assertEqual(checked, 9, 'every repo-routed job should target the shared pool') + def test_runner_override_and_missing_event_payload(self): job = yaml.safe_load(WORKFLOW.read_text())['jobs']['docker-build-push'] context = {'inputs.runner': '', 'github.event.repository.visibility': '', 'github.event.repository.name': ''}