From 11b7d67b10d39bdcb757fc72f9ddfbc5bdea3f00 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:01:23 +0000 Subject: [PATCH 1/4] test(ci): every job that holds a secret runs where no variable can move it Extends the App-key rule (#27540, #27557, #27631) to every secret: R2, Vercel, Cloudflare, Stack server keys, Sentry, signing material, GHOSTTY_RELEASE_TOKEN. A job that reads `${{ secrets.X }}` (not its own GITHUB_TOKEN) or passes `secrets: inherit` must run on a fixed ephemeral label, a Blacksmith or GitHub-hosted one, or a runs-on whose every result, for every variable value, is a Blacksmith label the expression names (the bounded LINUX_RUNNER selector, the overflow record's literal pair). A matrix runner is checked per value the job's matrix lists; a local reusable call is checked through its callee's jobs. Jobs that take a picker's output are listed with a verified reason; four that hold a secret on an owned Mac by design are listed as open. It fails on this commit for 31 jobs that still read LINUX_RUNNER, MACOS_RUNNER_BACKGROUND, MACOS_RUNNER_IOS/PR or MACOS_RUNNER_15 unbounded. --- tests/test_ci_fork_runner_routing.py | 106 ++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/test_ci_fork_runner_routing.py b/tests/test_ci_fork_runner_routing.py index bac53c84c720..acf87baf82e9 100644 --- a/tests/test_ci_fork_runner_routing.py +++ b/tests/test_ci_fork_runner_routing.py @@ -6,6 +6,8 @@ import json import re import unittest + +import yaml from pathlib import Path @@ -293,11 +295,79 @@ def runner_label_errors(runs_on: str) -> list[str]: # (workflow, job) -> why its runner may come from elsewhere. Exact jobs only. -APP_KEY_RUNNER_EXCEPTIONS = { +# (workflow, job) -> why a job that holds a secret takes its runner from something this check +# cannot enumerate (a picker job's output, a computed matrix). Exact jobs only. "open:" marks a +# job that runs on a reused owned machine by design and still holds a secret there: each needs +# its secret moved to a separate job on an ephemeral label, as #27622 did for the dogfood capture. +SECRET_RUNNER_EXCEPTIONS = { ("nightly.yml", "generate-nightly-deltas"): "pick-classic-sign-runner's output, which names only Blacksmith macOS labels " "(classic_nightly_build_runner.py), else the literal Blacksmith fallback", + ("nightly.yml", "build-sign-notarize-nightly"): + "the same picker as generate-nightly-deltas: only Blacksmith macOS labels", + ("cmux-next-dev-daily.yml", "tip-deltas"): + "the runner job's `cloud`/`alternate` outputs, from classic_nightly_build_runner.py: " + "only Blacksmith macOS labels", + ("ios-app-store.yml", "upload-and-validate"): + "resolve_runners.py's macos_26_ios capability: .github/runners.json maps it to " + "blacksmith-6vcpu-macos-26 or the hosted macos-26 only", + ("nightly.yml", "build-nightly-app"): + "open: CI_CACHE_R2 and SENTRY_AUTH_TOKEN on the pinned trusted mini (CI_NIGHTLY_TRUSTED_RUNNER) " + "by design; nightlies never use Blacksmith", + ("seed-derived-data.yml", "seed"): + "open: CI_CACHE_R2 on the pools seed_decide.py names, owned Macs included, by design " + "(it seeds their DerivedData)", + ("test-ios.yml", "ios-simulator-build"): + "open: CI_CACHE_R2 on ios_runner_pool.py's pick, an owned simulator Mac when CI_IOS_OWNED is 1", + ("ios-screenshots.yml", "screenshots"): + "open: the App Store Connect API key on ios_runner_pool.py's pick, an owned simulator Mac " + "when CI_IOS_OWNED is 1", } +APP_KEY_RUNNER_EXCEPTIONS = {key: why for key, why in SECRET_RUNNER_EXCEPTIONS.items() + if key == ("nightly.yml", "generate-nightly-deltas")} +# A secret a job reads: `${{ secrets.X }}` (not the job's own GITHUB_TOKEN), `secrets[...]`, or +# `secrets: inherit` into a reusable workflow. Context names are case-insensitive. +SECRET_MARKER = re.compile( + r"\$\{\{(?:[^}]|\}(?!\}))*?\bsecrets\s*(?:\.\s*(?!github_token\b)[a-z_][a-z0-9_]*|\[)|^\s*secrets:\s*inherit\b", + re.IGNORECASE | re.MULTILINE, +) + + +def secret_jobs() -> list[tuple[str, str, object, dict]]: + """(workflow, job, runs-on, job) for every job that holds a secret or a GitHub App key.""" + found = [] + for path in sorted(WORKFLOWS.glob("*.y*ml")): + text = path.read_text(encoding="utf-8") + document = yaml.load(text, Loader=yaml.BaseLoader) or {} + for name, job in (document.get("jobs") or {}).items(): + body = yaml.safe_dump(job, sort_keys=False, width=100000) + if SECRET_MARKER.search(body) or APP_KEY_MARKER.search(body): + found.append((path.name, name, job.get("runs-on"), job)) + return found + + +def job_runner_errors(workflow: str, job: dict) -> list[str]: + """runner_label_errors for a whole job: a local reusable call is checked through its callee's + jobs, and `${{ matrix.KEY }}` through each value the job's own matrix lists.""" + if "uses" in job and "runs-on" not in job: + uses = str(job["uses"]) + if not uses.startswith("./.github/workflows/") or not (ROOT / uses).is_file(): + return [f"passes secrets to {uses}, which is not a workflow of this repository"] + return [] # The callee's jobs are in secret_jobs() themselves. + runs_on = job.get("runs-on") + if not isinstance(runs_on, str): + return [f"runs-on {runs_on!r} is not one label"] + key = re.fullmatch(r"\$\{\{\s*matrix\.([A-Za-z_][A-Za-z0-9_-]*)\s*\}\}", runs_on.strip()) + if key is None: + return runner_label_errors(runs_on) + matrix = (job.get("strategy") or {}).get("matrix") + if not isinstance(matrix, dict): + return [f"runs-on reads matrix.{key.group(1)} from a computed matrix {matrix!r}"] + values = list(matrix.get(key.group(1)) or []) + values += [entry[key.group(1)] for entry in matrix.get("include") or [] if key.group(1) in entry] + if not values: + return [f"matrix.{key.group(1)} lists no value"] + return [error for value in values for error in runner_label_errors(str(value))] def app_key_jobs() -> list[tuple[str, str, str]]: @@ -1259,6 +1329,40 @@ def test_a_job_with_an_app_key_runs_where_no_variable_can_move_it(self) -> None: self.assertEqual(runner_label_errors(runs_on), [], f"{workflow}:{job} holds an App key") self.assertGreater(bounded, 0) + def test_a_job_with_a_secret_runs_where_no_variable_can_move_it(self) -> None: + # Every secret, not only App keys: R2, Vercel, Cloudflare, Stack server keys, Sentry, + # signing material. A job reading one runs where no variable value reaches a reused machine. + jobs = secret_jobs() + names = {(workflow, job) for workflow, job, _, _ in jobs} + for key in (("presence.yml", "deploy"), ("r2-cache-prune.yml", "prune"), ("ci-queue-janitor.yml", "sweep"), + ("build-ghosttykit.yml", "build-ghosttykit"), ("docs-channels.yml", "nightly")): + self.assertIn(key, names) # not vacuous + for key in SECRET_RUNNER_EXCEPTIONS: + self.assertIn(key, names, f"stale exception {key}") + for workflow, job, _, body in jobs: + if (workflow, job) in SECRET_RUNNER_EXCEPTIONS: + continue + with self.subTest(workflow=workflow, job=job): + self.assertEqual(job_runner_errors(workflow, body), [], f"{workflow}:{job} holds a secret") + + def test_the_secret_detector_and_matrix_check(self) -> None: + for held in ("${{ secrets.R2_KEY }}", "${{ SECRETS.Vercel_Token }}", "${{ secrets['X'] }}", + "secrets: inherit", "${{ format('{0}', secrets.A) }}"): + self.assertTrue(SECRET_MARKER.search(held), held) + for clean in ("${{ secrets.GITHUB_TOKEN }}", "${{ secrets.github_token }}", "python3 -c 'import secrets'", + "secrets.token_hex(8)", "${{ github.token }}"): + self.assertFalse(SECRET_MARKER.search(clean), clean) + literal = {"runs-on": "${{ matrix.pool }}", "strategy": {"matrix": {"pool": ["blacksmith-6vcpu-macos-15"]}}} + self.assertEqual(job_runner_errors("x.yml", literal), []) + moved = {"runs-on": "${{ matrix.pool }}", + "strategy": {"matrix": {"pool": ["${{ vars.MACOS_RUNNER_PR || 'blacksmith-6vcpu-macos-15' }}"]}}} + self.assertNotEqual(job_runner_errors("x.yml", moved), []) + computed = {"runs-on": "${{ matrix.pool }}", "strategy": {"matrix": "${{ fromJSON(needs.a.outputs.m) }}"}} + self.assertNotEqual(job_runner_errors("x.yml", computed), []) + self.assertNotEqual(job_runner_errors("x.yml", {"uses": "org/repo/.github/workflows/x.yml@v1"}), []) + self.assertEqual(runner_label_errors("ubuntu-latest"), []) + self.assertNotEqual(runner_label_errors("self-hosted"), []) + def test_key_holding_linux_jobs_follow_linux_runner_through_the_bounded_selector(self) -> None: runs_on = {(workflow, job): line for workflow, job, line in app_key_jobs()} for key in sorted(BOUNDED_SELECTOR_JOBS): From ec142bc56a9c5e44e33bdacde6eb5e3f0e58d30b Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:07:21 +0000 Subject: [PATCH 2/4] ci: every job that holds a secret runs on an ephemeral label no variable can move Extends #27540, #27557, #27622 and #27631 (which built on #27022) from App keys to every secret. 31 jobs read a secret while a variable could point their runner at a reused machine. Linux, now through the bounded LINUX_RUNNER selector (#27631), so a 4 vCPU stall still moves them: ci-artifact-canary canary, cloud-capture-provision provision, cloud-vm-canary canary, cloud-vm-dogfood journey, cloud-vm-env-audit audit, cloud-vm-image-contract promote, cloud-vm-image-reachability reachable, cloud-vm-migrate migrate-staging and migrate-production, cloud-vm-smoke smoke, cmux-tui-artifacts publish, docs-deploy-reusable deploy, dogfood-artifact-publish publish, fleet-artifact-cdn deploy, ios-e2e backend, iroh-v2 deploy-development, presence deploy, r2-binaries-prune prune, r2-cache-prune prune, r2-storage-census binaries, cef and ci-cache, sdk-bootstrap-crates publish-sdk and publish-sidebar, update-homebrew update-cask, vercel-auth-health check. Fork branches are unchanged. macOS, now on the literal label they fell back to: - build-ghosttykit (GHOSTTY_RELEASE_TOKEN): was MACOS_RUNNER_BACKGROUND. - ios-e2e mac-ios-e2e (dogfood Stack login): was MACOS_RUNNER_IOS/PR. - ios-streamed-validate validate (CLAUDE_CODE_OAUTH_TOKEN): was MACOS_RUNNER_IOS. - iroh-release-gate simulator-e2e (production Stack keys): was MACOS_RUNNER_15 under CI_PAID_MACOS_OVERFLOW. - nightly refresh-test-compilation-cache (CI_CACHE_R2): its first matrix pool no longer falls through to MACOS_RUNNER_PR. Today every changed variable holds the label these jobs now name, so nothing moves now. Four jobs keep a secret on an owned Mac by design and are listed open in the test (build-nightly-app, seed-derived-data seed, test-ios ios-simulator-build, ios-screenshots screenshots). One consequence: no signing job on main reads MACOS_RUNNER_15 any more (iroh-release-gate simulator-e2e did), so signing_runner_variables.py no longer derives it, and the overflow switch now fails it over to the owned std pool like MACOS_RUNNER_PR; only jobs without a secret follow it. Its tests and docs/ci-runners.md say so. test_ci_pr_runner_pool.py pins the two new literal labels. The trusted fork exclusion also refuses an index after contains() or fromJSON() (review of #27631; no workflow used it). Tests: the routing test fails on the previous commit (31 jobs) and passes here. --- .github/workflows/build-ghosttykit.yml | 9 +++++---- .github/workflows/ci-artifact-canary.yml | 2 +- .github/workflows/cloud-capture-provision.yml | 2 +- .github/workflows/cloud-vm-canary.yml | 2 +- .github/workflows/cloud-vm-dogfood.yml | 2 +- .github/workflows/cloud-vm-env-audit.yml | 2 +- .github/workflows/cloud-vm-image-contract.yml | 2 +- .github/workflows/cloud-vm-image-reachability.yml | 2 +- .github/workflows/cloud-vm-migrate.yml | 4 ++-- .github/workflows/cloud-vm-smoke.yml | 2 +- .github/workflows/cmux-tui-artifacts.yml | 2 +- .github/workflows/docs-deploy-reusable.yml | 2 +- .github/workflows/dogfood-artifact-publish.yml | 2 +- .github/workflows/fleet-artifact-cdn.yml | 2 +- .github/workflows/ios-e2e.yml | 7 +++++-- .github/workflows/ios-streamed-validate.yml | 9 +++++---- .github/workflows/iroh-release-gate.yml | 4 +++- .github/workflows/iroh-v2.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/presence.yml | 2 +- .github/workflows/r2-binaries-prune.yml | 2 +- .github/workflows/r2-cache-prune.yml | 2 +- .github/workflows/r2-storage-census.yml | 6 +++--- .github/workflows/sdk-bootstrap-crates.yml | 4 ++-- .github/workflows/update-homebrew.yml | 2 +- .github/workflows/vercel-auth-health.yml | 2 +- docs/ci-runners.md | 6 +++--- tests/test_ci_cloud_overflow_switch.py | 9 ++++++--- tests/test_ci_fork_runner_routing.py | 8 ++++++++ tests/test_ci_pr_runner_pool.py | 6 ++++-- 30 files changed, 65 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build-ghosttykit.yml b/.github/workflows/build-ghosttykit.yml index 6ae8b11d8cb2..797631fd53bc 100644 --- a/.github/workflows/build-ghosttykit.yml +++ b/.github/workflows/build-ghosttykit.yml @@ -15,10 +15,11 @@ concurrency: jobs: build-ghosttykit: - # Background lane: dispatch-only and off the PR critical path, so it runs - # on free GitHub-hosted capacity instead of the shared paid macOS pool. - # See docs/ci-runners.md "Background lane". - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || vars.MACOS_RUNNER_BACKGROUND || 'blacksmith-6vcpu-macos-15' }} + # Dispatch-only and off the PR critical path. It holds GHOSTTY_RELEASE_TOKEN, + # so it runs on a literal ephemeral label, not the MACOS_RUNNER_BACKGROUND + # lane a variable could point at a reused machine + # (tests/test_ci_fork_runner_routing.py). + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || 'blacksmith-6vcpu-macos-15' }} timeout-minutes: 60 env: GHOSTTYKIT_CRASH_REPORT_SUBDIR: cmux/crash diff --git a/.github/workflows/ci-artifact-canary.yml b/.github/workflows/ci-artifact-canary.yml index 62464f482ca2..e8e907c6b0cf 100644 --- a/.github/workflows/ci-artifact-canary.yml +++ b/.github/workflows/ci-artifact-canary.yml @@ -14,7 +14,7 @@ concurrency: jobs: canary: if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 20 env: WRANGLER_SEND_METRICS: 'false' diff --git a/.github/workflows/cloud-capture-provision.yml b/.github/workflows/cloud-capture-provision.yml index 45a2155f4aa7..1ecd149a7442 100644 --- a/.github/workflows/cloud-capture-provision.yml +++ b/.github/workflows/cloud-capture-provision.yml @@ -24,7 +24,7 @@ jobs: # The staging server key is handed only to main's own code: cloud-vm-staging deploys only to protected # branches, and main is the one this workflow runs from. if: github.ref == 'refs/heads/main' - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} environment: cloud-vm-staging timeout-minutes: 10 steps: diff --git a/.github/workflows/cloud-vm-canary.yml b/.github/workflows/cloud-vm-canary.yml index df1c29becc8a..79b69a3dfee8 100644 --- a/.github/workflows/cloud-vm-canary.yml +++ b/.github/workflows/cloud-vm-canary.yml @@ -23,7 +23,7 @@ concurrency: jobs: canary: - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 10 # Only the Stack keys and the notify token live here: no reviewers (a # scheduled run cannot wait for approval), deployments from main only. diff --git a/.github/workflows/cloud-vm-dogfood.yml b/.github/workflows/cloud-vm-dogfood.yml index 40063a33b38c..12dca6695f84 100644 --- a/.github/workflows/cloud-vm-dogfood.yml +++ b/.github/workflows/cloud-vm-dogfood.yml @@ -53,7 +53,7 @@ concurrency: jobs: journey: - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 45 environment: cloud-vm-${{ inputs.target || 'staging' }} defaults: diff --git a/.github/workflows/cloud-vm-env-audit.yml b/.github/workflows/cloud-vm-env-audit.yml index ef74e5fc306a..a5f243c6a8e2 100644 --- a/.github/workflows/cloud-vm-env-audit.yml +++ b/.github/workflows/cloud-vm-env-audit.yml @@ -27,7 +27,7 @@ concurrency: jobs: audit: - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/cloud-vm-image-contract.yml b/.github/workflows/cloud-vm-image-contract.yml index 573e04528a18..b85d7ec8fcea 100644 --- a/.github/workflows/cloud-vm-image-contract.yml +++ b/.github/workflows/cloud-vm-image-contract.yml @@ -94,7 +94,7 @@ jobs: promote: if: github.repository == 'manaflow-ai/cmux' && github.ref_type == 'branch' && inputs.promote == true && inputs.confirm == 'PROMOTE' - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 180 environment: cloud-vm-image-checks defaults: diff --git a/.github/workflows/cloud-vm-image-reachability.yml b/.github/workflows/cloud-vm-image-reachability.yml index c19c70f24831..47189575072b 100644 --- a/.github/workflows/cloud-vm-image-reachability.yml +++ b/.github/workflows/cloud-vm-image-reachability.yml @@ -45,7 +45,7 @@ concurrency: jobs: reachable: - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} # The provider key lives in this environment, not in a repo-wide secret, # so only a job that names it can read it. No reviewers and no branch # policy, or the daily run and PR runs could not use it; fork PRs get no diff --git a/.github/workflows/cloud-vm-migrate.yml b/.github/workflows/cloud-vm-migrate.yml index 13a7de4a51d3..7a4542bf3eda 100644 --- a/.github/workflows/cloud-vm-migrate.yml +++ b/.github/workflows/cloud-vm-migrate.yml @@ -54,7 +54,7 @@ jobs: migrate-staging: if: ${{ inputs.target == 'staging' || inputs.target == 'production' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} needs: preflight environment: cloud-vm-staging defaults: @@ -96,7 +96,7 @@ jobs: migrate-production: if: ${{ inputs.target == 'production' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} needs: [preflight, migrate-staging] environment: cloud-vm-production defaults: diff --git a/.github/workflows/cloud-vm-smoke.yml b/.github/workflows/cloud-vm-smoke.yml index c3711d276459..8d784d05d2e5 100644 --- a/.github/workflows/cloud-vm-smoke.yml +++ b/.github/workflows/cloud-vm-smoke.yml @@ -38,7 +38,7 @@ concurrency: jobs: smoke: - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} environment: cloud-vm-${{ inputs.target }} defaults: run: diff --git a/.github/workflows/cmux-tui-artifacts.yml b/.github/workflows/cmux-tui-artifacts.yml index 16fc603c0dcf..d8eb5a0e5d0f 100644 --- a/.github/workflows/cmux-tui-artifacts.yml +++ b/.github/workflows/cmux-tui-artifacts.yml @@ -100,7 +100,7 @@ jobs: # main-push or manual dispatch. if: github.event_name != 'pull_request' needs: build - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/docs-deploy-reusable.yml b/.github/workflows/docs-deploy-reusable.yml index 4cd0465980d4..fc1f7bf80331 100644 --- a/.github/workflows/docs-deploy-reusable.yml +++ b/.github/workflows/docs-deploy-reusable.yml @@ -18,7 +18,7 @@ jobs: deploy: permissions: contents: read - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/dogfood-artifact-publish.yml b/.github/workflows/dogfood-artifact-publish.yml index 290677ddd7cd..be644c0cec6e 100644 --- a/.github/workflows/dogfood-artifact-publish.yml +++ b/.github/workflows/dogfood-artifact-publish.yml @@ -22,7 +22,7 @@ jobs: # pauses this publish while the installation's GITHUB_TOKEN budget is # reserved for the nightly (tests/test_ci_quota_hold_gate.py). if: ${{ vars.CI_QUOTA_HOLD != '1' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' && toJSON(github.event.workflow_run.pull_requests) != '[]' && (github.event.workflow_run.name == 'CI' && github.event.workflow_run.pull_requests[0].base.ref != 'feat-cmux-next' || github.event.workflow_run.name == 'cmux-next' && github.event.workflow_run.pull_requests[0].base.ref == 'feat-cmux-next') }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} environment: artifacts permissions: actions: write diff --git a/.github/workflows/fleet-artifact-cdn.yml b/.github/workflows/fleet-artifact-cdn.yml index 0ed3f90a416f..40db7389cfe1 100644 --- a/.github/workflows/fleet-artifact-cdn.yml +++ b/.github/workflows/fleet-artifact-cdn.yml @@ -45,7 +45,7 @@ jobs: deploy: if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && github.repository == 'manaflow-ai/cmux' needs: test - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 10 env: WRANGLER_SEND_METRICS: 'false' diff --git a/.github/workflows/ios-e2e.yml b/.github/workflows/ios-e2e.yml index e08131da7ced..b00eddbd4159 100644 --- a/.github/workflows/ios-e2e.yml +++ b/.github/workflows/ios-e2e.yml @@ -133,7 +133,7 @@ jobs: success() && needs.route.outputs.run_e2e == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 150 environment: ios-e2e permissions: @@ -303,7 +303,10 @@ jobs: success() && needs.route.outputs.run_clients == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || vars.MACOS_RUNNER_IOS || vars.MACOS_RUNNER_PR || 'blacksmith-6vcpu-macos-26' }} + # It holds the dogfood Stack login: a literal ephemeral label, never the + # MACOS_RUNNER_IOS/PR pools a variable (or the overflow switch) can point at + # a reused owned Mac (tests/test_ci_fork_runner_routing.py). + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || 'blacksmith-6vcpu-macos-26' }} timeout-minutes: 120 environment: ios-e2e permissions: diff --git a/.github/workflows/ios-streamed-validate.yml b/.github/workflows/ios-streamed-validate.yml index ae1d436e1b72..98421df96051 100644 --- a/.github/workflows/ios-streamed-validate.yml +++ b/.github/workflows/ios-streamed-validate.yml @@ -19,9 +19,10 @@ permissions: jobs: validate: # This end-to-end validation is expensive and is run only on explicit - # dispatch. It wants the image the iOS simulator tests use, so it reads the - # variable that names that pool rather than one named after this lane; the - # selector below picks its Xcode 26 toolchain. + # dispatch. It wants the image the iOS simulator tests use (macOS 26); the + # selector below picks its Xcode 26 toolchain. The label is literal: it holds + # CLAUDE_CODE_OAUTH_TOKEN and the dogfood login, and MACOS_RUNNER_IOS, which + # it read before, can name an owned Mac (tests/test_ci_fork_runner_routing.py). # # It stays on an ephemeral Blacksmith machine and never takes an owned Mac # (scripts/ci/ios_runner_pool.py): it changes the whole login session, not @@ -31,7 +32,7 @@ jobs: # dark-mode default, symlinks into /usr/local/bin, launches the Mac app # through launchd, and leaves Postgres and a Next.js server on fixed ports. # On a reused runner every later job would inherit that. - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || 'blacksmith-6vcpu-macos-26' }} timeout-minutes: 120 env: CMUX_PORT: "3000" diff --git a/.github/workflows/iroh-release-gate.yml b/.github/workflows/iroh-release-gate.yml index 1df94fbef370..2c4e4a7c1224 100644 --- a/.github/workflows/iroh-release-gate.yml +++ b/.github/workflows/iroh-release-gate.yml @@ -267,7 +267,9 @@ jobs: max-parallel: 1 matrix: mode: ${{ fromJSON(inputs.mode == 'all' && '["automatic","relay-only","relay-expiry","direct-only","private-path"]' || format('["{0}"]', inputs.mode)) }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || (vars.CI_PAID_MACOS_OVERFLOW == '1' && vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15') }} + # Production Stack credentials: a literal ephemeral label, never MACOS_RUNNER_15, + # which the overflow switch can point at a reused owned Mac. + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || 'blacksmith-6vcpu-macos-15' }} # The real relay rollover probe waits through the 30-minute credential # lifetime after the one-hour workload. Keep enough room for the hosted # build, simulator setup, artifact upload, and runner cleanup so a passing diff --git a/.github/workflows/iroh-v2.yml b/.github/workflows/iroh-v2.yml index 15d2e2bb6d8f..245592adf979 100644 --- a/.github/workflows/iroh-v2.yml +++ b/.github/workflows/iroh-v2.yml @@ -88,7 +88,7 @@ jobs: deploy-development: if: github.event_name == 'workflow_dispatch' && inputs.deploy_development needs: backend - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'blacksmith-4vcpu-ubuntu-2404' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 10 concurrency: group: iroh-v2-deploy-development diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a80e8b09012a..8c8a50eb3ab8 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -944,7 +944,7 @@ jobs: fail-fast: false matrix: pool: - - ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || vars.MACOS_RUNNER_PR == 'blacksmith-6vcpu-macos-26' && 'blacksmith-12vcpu-macos-26' || vars.MACOS_RUNNER_PR || 'blacksmith-6vcpu-macos-15' }} + - ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || vars.MACOS_RUNNER_PR == 'blacksmith-6vcpu-macos-26' && 'blacksmith-12vcpu-macos-26' || 'blacksmith-6vcpu-macos-15' }} - ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || 'blacksmith-6vcpu-macos-15' }} runs-on: ${{ matrix.pool }} environment: ${{ github.ref == 'refs/heads/main' && 'ci-cache-writer' || '' }} diff --git a/.github/workflows/presence.yml b/.github/workflows/presence.yml index 284e5972ca3e..f73c96c215b9 100644 --- a/.github/workflows/presence.yml +++ b/.github/workflows/presence.yml @@ -76,7 +76,7 @@ jobs: deploy: if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' needs: test - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} concurrency: group: presence-deploy cancel-in-progress: false diff --git a/.github/workflows/r2-binaries-prune.yml b/.github/workflows/r2-binaries-prune.yml index cd83c6b042ee..8fed57ae5238 100644 --- a/.github/workflows/r2-binaries-prune.yml +++ b/.github/workflows/r2-binaries-prune.yml @@ -29,7 +29,7 @@ jobs: name: Prune superseded branch builds # The bucket credentials only ever run main's copy of this script. if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 60 environment: artifacts permissions: diff --git a/.github/workflows/r2-cache-prune.yml b/.github/workflows/r2-cache-prune.yml index 388cbdf4f36b..a9e1e21d8539 100644 --- a/.github/workflows/r2-cache-prune.yml +++ b/.github/workflows/r2-cache-prune.yml @@ -27,7 +27,7 @@ jobs: name: Prune expired cache archives # The bucket credentials only ever run main's copy of this script. if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 20 # Only main may use the R2 write credentials (see #14147). environment: ${{ github.ref == 'refs/heads/main' && 'ci-cache-writer' || '' }} diff --git a/.github/workflows/r2-storage-census.yml b/.github/workflows/r2-storage-census.yml index 6a638b945f55..0160026ceccc 100644 --- a/.github/workflows/r2-storage-census.yml +++ b/.github/workflows/r2-storage-census.yml @@ -20,7 +20,7 @@ jobs: binaries: name: Census (binaries) if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 30 environment: artifacts permissions: @@ -54,7 +54,7 @@ jobs: cef: name: Census (cef) if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 30 permissions: contents: read @@ -87,7 +87,7 @@ jobs: ci-cache: name: Census (ci-cache) if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 30 environment: ${{ github.ref == 'refs/heads/main' && 'ci-cache-writer' || '' }} permissions: diff --git a/.github/workflows/sdk-bootstrap-crates.yml b/.github/workflows/sdk-bootstrap-crates.yml index e8c090569f61..c485f18945c6 100644 --- a/.github/workflows/sdk-bootstrap-crates.yml +++ b/.github/workflows/sdk-bootstrap-crates.yml @@ -275,7 +275,7 @@ jobs: - preflight - decisions if: needs.decisions.outputs.sdk_need_publish == 'true' - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 10 permissions: {} environment: @@ -425,7 +425,7 @@ jobs: needs.preflight.result == 'success' && needs.decisions.result == 'success' && needs.decisions.outputs.sidebar_need_publish == 'true' - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 10 permissions: {} environment: diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml index 2598470a258b..aafe53035403 100644 --- a/.github/workflows/update-homebrew.yml +++ b/.github/workflows/update-homebrew.yml @@ -92,7 +92,7 @@ jobs: # environment's deployment branch policy: main, tags v*). environment: release needs: gate - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} if: ${{ needs.gate.outputs.proceed == 'true' }} steps: - name: Get version diff --git a/.github/workflows/vercel-auth-health.yml b/.github/workflows/vercel-auth-health.yml index b76882f94450..b2066e7752db 100644 --- a/.github/workflows/vercel-auth-health.yml +++ b/.github/workflows/vercel-auth-health.yml @@ -10,7 +10,7 @@ permissions: jobs: check: - runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || contains(fromJSON('["blacksmith-4vcpu-ubuntu-2404","blacksmith-16vcpu-ubuntu-2404","blacksmith-32vcpu-ubuntu-2404"]'), vars.LINUX_RUNNER) && vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 5 steps: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 diff --git a/docs/ci-runners.md b/docs/ci-runners.md index 26ef2b0ddfa8..c2010f113a59 100644 --- a/docs/ci-runners.md +++ b/docs/ci-runners.md @@ -94,10 +94,10 @@ gh variable list --repo manaflow-ai/cmux | Variable | Used by | Intended steady state | Fallback baked into the workflow | | --- | --- | --- | --- | -| `LINUX_RUNNER` | every Linux job (`ci.yml` web/typecheck/db, presence, cloud-vm, nightly/ios decide jobs, homebrew, tmux fuzz) A job that holds a GitHub App key reads it only through a bounded selector: `LINUX_RUNNER` when it names `blacksmith-4vcpu-ubuntu-2404`, `blacksmith-16vcpu-ubuntu-2404` or `blacksmith-32vcpu-ubuntu-2404`, else `blacksmith-4vcpu-ubuntu-2404`, so a 4 vCPU stall can move it and no value reaches a reused machine (`tests/test_ci_fork_runner_routing.py` tries every value) | `blacksmith-4vcpu-ubuntu-2404` | `blacksmith-4vcpu-ubuntu-2404` | +| `LINUX_RUNNER` | every Linux job (`ci.yml` web/typecheck/db, presence, cloud-vm, nightly/ios decide jobs, homebrew, tmux fuzz) A job that holds a GitHub App key reads it only through a bounded selector: `LINUX_RUNNER` when it names `blacksmith-4vcpu-ubuntu-2404`, `blacksmith-16vcpu-ubuntu-2404` or `blacksmith-32vcpu-ubuntu-2404`, else `blacksmith-4vcpu-ubuntu-2404`, so a 4 vCPU stall can move it and no value reaches a reused machine (`tests/test_ci_fork_runner_routing.py` tries every value). The same holds for every job that reads any secret (R2, Vercel, Cloudflare, Stack, Sentry, signing): it runs on a fixed ephemeral label or this selector | `blacksmith-4vcpu-ubuntu-2404` | `blacksmith-4vcpu-ubuntu-2404` | | `CI_TRUSTED_RUNNER` | jobs holding trusted tokens that must run on an ephemeral VM: the required `backend migrations applied` check and `web-complexity-trusted.yml` (`validate-cla-policy.rb` accepts this exact selector for the CLA checks too, with a runner guard step that admits only GitHub-hosted runners and Blacksmith VMs; `cla.yml` and `cla-policy-guard.yml` pin `ubuntu-24.04` until they move to it). Only `ubuntu-24.04`, `blacksmith-2vcpu-ubuntu-2404` or `blacksmith-4vcpu-ubuntu-2404` is accepted; any other value, owned label included, falls back. Set it to `ubuntu-24.04` when Blacksmith stalls, or leave it unset when GitHub-hosted runners stall | unset | `blacksmith-4vcpu-ubuntu-2404` (forks: `ubuntu-24.04`) | | `LINUX_ARM64_RUNNER` | native ARM64 package entrypoint verification | `ubuntu-24.04-arm` | `ubuntu-24.04-arm` | -| `MACOS_RUNNER_15` | the macOS 15 default: `macos-compile-admission`, non-PR `app-host-unit-tests`, nightly helper and test-cache jobs, `iroh-release-gate.yml` streamed validation | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | +| `MACOS_RUNNER_15` | the macOS 15 default: `macos-compile-admission`, non-PR `app-host-unit-tests`, nightly helper and test-cache jobs; never a job with a secret, which takes a fixed label | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | | `MACOS_RUNNER_PR` | **pull-request** macOS jobs in `ci-macos.yml` (the app-host shards and `tests-build-and-lag` follow `macos-compile-admission`), `terminal-hang-diagnostics.yml`, `ci.yml` (`claude-wrapper`) and `nightly.yml` (`refresh-test-compilation-cache`) | unset (see "Lanes" below) | `blacksmith-6vcpu-macos-15` | | `MACOS_RUNNER_TESTS` | test-only lanes that pick their Xcode by SDK and sign nothing: `test-e2e.yml`, `test-macos-suite.yml`, `test-ios.yml` (`auto`) and the `iroh-v2.yml` client | unset (see "Lanes" below) | each lane's own variable or Blacksmith label: `blacksmith-6vcpu-macos-26` for `test-e2e.yml`, `blacksmith-6vcpu-macos-15` for `test-macos-suite.yml`, `MACOS_RUNNER_IOS` for `test-ios.yml` and `iroh-v2.yml` | | `MACOS_RUNNER_DUAL_XCODE` | `swift-package-tests` (SDK 15 release helper, then SDK 26 package tests) on **every** event, pull requests included, except attempt 1 of a pull request run whose picker placed it on an owned Mac (no helper build in that run; see "Pull request pool preference") | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | @@ -1106,7 +1106,7 @@ control jobs: a nightly's Linux cross-builds still wait for Blacksmith Linux read. That is the lever pulled by hand on 2026-09-29. A variable a signing job's `runs-on` reads is read from the workflows of `main` and `feat-cmux-next` (`scripts/ci/signing_runner_variables.py`; today - `LINUX_RUNNER`, `MACOS_RUNNER_26` and `MACOS_RUNNER_15`), and the switch + `LINUX_RUNNER` and `MACOS_RUNNER_26`; every job with a secret on `main` takes a fixed label, so `MACOS_RUNNER_15` is not one), and the switch never writes a label other than `blacksmith-*` or `ubuntu-*` to one: such a variable has no owned failover, an override naming a reused machine for one is refused, and a stalled probe never re-applies an older record's owned value diff --git a/tests/test_ci_cloud_overflow_switch.py b/tests/test_ci_cloud_overflow_switch.py index 1f2b1f054d96..85015d9274f3 100644 --- a/tests/test_ci_cloud_overflow_switch.py +++ b/tests/test_ci_cloud_overflow_switch.py @@ -160,9 +160,11 @@ def test_a_missing_directory_fails_closed(self) -> None: def test_this_tree_and_feat_together(self) -> None: derived = signing.signing_variables() - # main: iroh-release-gate.yml's release-environment job reads MACOS_RUNNER_15. - self.assertIn("MACOS_RUNNER_15", derived) + # main: every job with a secret runs on a fixed label or the bounded LINUX_RUNNER selector + # (test_ci_fork_runner_routing.py), so no signing job reads MACOS_RUNNER_15 any more; the + # stand-in feat-cmux-next workflow reads MACOS_RUNNER_26. self.assertIn("MACOS_RUNNER_26", derived) + self.assertNotIn("MACOS_RUNNER_15", derived) def test_the_switch_and_checker_read_feat_cmux_next(self) -> None: for name in ("ci-cloud-overflow-probe.yml", "ci-repo-variables.yml"): @@ -176,7 +178,8 @@ def test_a_derived_variable_never_gets_an_owned_failover(self) -> None: if name in values: self.assertTrue(signing.ephemeral_label(values[name]), (name, values[name])) self.assertNotIn("MACOS_RUNNER_26", values) - self.assertNotIn("MACOS_RUNNER_15", values) + # No signing job reads MACOS_RUNNER_15 now, so it fails over to the owned std pool like _PR. + self.assertEqual(values["MACOS_RUNNER_15"], "glaeda-std-xcode-26.6") self.assertEqual(values["MACOS_RUNNER_PR"], "glaeda-std-xcode-26.6") diff --git a/tests/test_ci_fork_runner_routing.py b/tests/test_ci_fork_runner_routing.py index acf87baf82e9..85393dab370c 100644 --- a/tests/test_ci_fork_runner_routing.py +++ b/tests/test_ci_fork_runner_routing.py @@ -680,9 +680,11 @@ def _trusted_fork_exclusion(node: tuple) -> bool: return ( node[0] == "not" and node[1][0] == "call" + and len(node[1]) == 3 # no index after contains() or fromJSON() (review of #27631) and node[1][1] == "contains" and len(node[1][2]) == 2 and node[1][2][0][0] == "call" + and len(node[1][2][0]) == 3 and node[1][2][0][1] == "fromJSON" and _refs(node[1][2][0][2][0]) and _refs(node[1][2][0][2][0])[0].endswith("owned_head_repos") @@ -1361,6 +1363,12 @@ def test_the_secret_detector_and_matrix_check(self) -> None: self.assertNotEqual(job_runner_errors("x.yml", computed), []) self.assertNotEqual(job_runner_errors("x.yml", {"uses": "org/repo/.github/workflows/x.yml@v1"}), []) self.assertEqual(runner_label_errors("ubuntu-latest"), []) + # An index after fromJSON turns the trusted fork exclusion into a substring test. + indexed = parse_expression("!contains(fromJSON(inputs.owned_head_repos)[0], " + "github.event.pull_request.head.repo.full_name)") + self.assertFalse(_trusted_fork_exclusion(indexed)) + self.assertTrue(_trusted_fork_exclusion(parse_expression( + "!contains(fromJSON(inputs.owned_head_repos), github.event.pull_request.head.repo.full_name)"))) self.assertNotEqual(runner_label_errors("self-hosted"), []) def test_key_holding_linux_jobs_follow_linux_runner_through_the_bounded_selector(self) -> None: diff --git a/tests/test_ci_pr_runner_pool.py b/tests/test_ci_pr_runner_pool.py index eae071325b7c..f0ffa2cc8907 100644 --- a/tests/test_ci_pr_runner_pool.py +++ b/tests/test_ci_pr_runner_pool.py @@ -3898,8 +3898,9 @@ def test_the_picker_offers_only_owned_labels_of_trusted_first_attempts(self): def test_simulator_e2e_stays_on_blacksmith(self): job = self.jobs["simulator-e2e"] self.assertEqual(job["needs"], "resolve-ref") + # Production Stack credentials: a literal label, never MACOS_RUNNER_15 (test_ci_fork_runner_routing.py). self.assertEqual(job["runs-on"], "${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || " - f"{self.BLACKSMITH} }}}}") + "'blacksmith-6vcpu-macos-15' }}") class IOSWiring(unittest.TestCase): @@ -3996,8 +3997,9 @@ def test_streamed_validation_and_signing_stay_on_blacksmith(self): "vars.CI_PR_POOL_OWNED"): self.assertNotIn(routed, text, f"{name} must not route to an owned Mac") validate = self.workflow("ios-streamed-validate.yml")["jobs"]["validate"] + # It holds CLAUDE_CODE_OAUTH_TOKEN: a literal label, never MACOS_RUNNER_IOS. self.assertEqual(validate["runs-on"], "${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || " - "vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26' }}") + "'blacksmith-6vcpu-macos-26' }}") def test_home_secrets_are_removed_however_the_job_ends(self): # A reused runner keeps $HOME: every job that writes a secret file there From 0f6f7120e849b2176ecdd1fb1bc8e46e158e1062 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:50:52 +0000 Subject: [PATCH 3/4] ci: the App Store upload takes the capability map's label only when it is Blacksmith Review of #27648 found the ios-app-store exception false: its runner came from the capability map, which reads CMUX_CI_RUNNER_OVERRIDES, a variable that may name any machine, and the job holds the App Store Connect key. Its runs-on now keeps the map's answer only when it is blacksmith-6vcpu-macos-26 (else that label), and the exception is gone; the structural rule's bounded pick accepts the same expression in contains() and as the result (a capability lookup), never two different ones. The detector also counts a workflow-level env, toJSON(secrets) and a bare secrets context; a computed matrix list or include fails closed. Docs no longer list build-ghosttykit in the background lane, ios-streamed-validate under MACOS_RUNNER_IOS or the refresh seeder as running on MACOS_RUNNER_PR. --- .github/workflows/ios-app-store.yml | 5 +- docs/ci-runners.md | 9 ++-- tests/test_ci_fork_runner_routing.py | 51 +++++++++++++++------ tests/test_ci_runner_capability_resolver.py | 2 +- tests/test_ci_self_hosted_guard.sh | 2 +- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ios-app-store.yml b/.github/workflows/ios-app-store.yml index b26adc8aca61..55091a0f6274 100644 --- a/.github/workflows/ios-app-store.yml +++ b/.github/workflows/ios-app-store.yml @@ -51,7 +51,10 @@ jobs: # The app source is always main, including dispatches of workflow repairs. if: github.event_name == 'workflow_dispatch' needs: runners - runs-on: ${{ fromJSON(needs.runners.outputs.map).macos_26_ios }} + # It holds the App Store Connect key, so the capability map's answer is used only + # when it is the Blacksmith label: the map reads CMUX_CI_RUNNER_OVERRIDES, which may + # name any machine (tests/test_ci_fork_runner_routing.py). + runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || contains(fromJSON('["blacksmith-6vcpu-macos-26"]'), fromJSON(needs.runners.outputs.map).macos_26_ios) && fromJSON(needs.runners.outputs.map).macos_26_ios || 'blacksmith-6vcpu-macos-26' }} timeout-minutes: 90 env: ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }} diff --git a/docs/ci-runners.md b/docs/ci-runners.md index c2010f113a59..06ef04c84d66 100644 --- a/docs/ci-runners.md +++ b/docs/ci-runners.md @@ -98,15 +98,15 @@ gh variable list --repo manaflow-ai/cmux | `CI_TRUSTED_RUNNER` | jobs holding trusted tokens that must run on an ephemeral VM: the required `backend migrations applied` check and `web-complexity-trusted.yml` (`validate-cla-policy.rb` accepts this exact selector for the CLA checks too, with a runner guard step that admits only GitHub-hosted runners and Blacksmith VMs; `cla.yml` and `cla-policy-guard.yml` pin `ubuntu-24.04` until they move to it). Only `ubuntu-24.04`, `blacksmith-2vcpu-ubuntu-2404` or `blacksmith-4vcpu-ubuntu-2404` is accepted; any other value, owned label included, falls back. Set it to `ubuntu-24.04` when Blacksmith stalls, or leave it unset when GitHub-hosted runners stall | unset | `blacksmith-4vcpu-ubuntu-2404` (forks: `ubuntu-24.04`) | | `LINUX_ARM64_RUNNER` | native ARM64 package entrypoint verification | `ubuntu-24.04-arm` | `ubuntu-24.04-arm` | | `MACOS_RUNNER_15` | the macOS 15 default: `macos-compile-admission`, non-PR `app-host-unit-tests`, nightly helper and test-cache jobs; never a job with a secret, which takes a fixed label | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | -| `MACOS_RUNNER_PR` | **pull-request** macOS jobs in `ci-macos.yml` (the app-host shards and `tests-build-and-lag` follow `macos-compile-admission`), `terminal-hang-diagnostics.yml`, `ci.yml` (`claude-wrapper`) and `nightly.yml` (`refresh-test-compilation-cache`) | unset (see "Lanes" below) | `blacksmith-6vcpu-macos-15` | +| `MACOS_RUNNER_PR` | **pull-request** macOS jobs in `ci-macos.yml` (the app-host shards and `tests-build-and-lag` follow `macos-compile-admission`), `terminal-hang-diagnostics.yml`, `ci.yml` (`claude-wrapper`) and `nightly.yml` (`refresh-test-compilation-cache`, only to pick its 12 vCPU sibling when it names `blacksmith-6vcpu-macos-26`; it holds `CI_CACHE_R2` and never runs on the variable's value) | unset (see "Lanes" below) | `blacksmith-6vcpu-macos-15` | | `MACOS_RUNNER_TESTS` | test-only lanes that pick their Xcode by SDK and sign nothing: `test-e2e.yml`, `test-macos-suite.yml`, `test-ios.yml` (`auto`) and the `iroh-v2.yml` client | unset (see "Lanes" below) | each lane's own variable or Blacksmith label: `blacksmith-6vcpu-macos-26` for `test-e2e.yml`, `blacksmith-6vcpu-macos-15` for `test-macos-suite.yml`, `MACOS_RUNNER_IOS` for `test-ios.yml` and `iroh-v2.yml` | | `MACOS_RUNNER_DUAL_XCODE` | `swift-package-tests` (SDK 15 release helper, then SDK 26 package tests) on **every** event, pull requests included, except attempt 1 of a pull request run whose picker placed it on an owned Mac (no helper build in that run; see "Pull request pool preference") | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | | `MACOS_RUNNER_26` | the macOS 26 image: compatibility jobs, `release.yml` and nightly sign/notarize, the disk-heavy `release-build` universal app, and the nightly compilation-cache warmer | `blacksmith-6vcpu-macos-26` | `blacksmith-6vcpu-macos-26` | | `MACOS_RUNNER_26_LARGE` | the larger macOS 26 machine: changed-revision universal Nightly app builds | `blacksmith-12vcpu-macos-26` | `blacksmith-12vcpu-macos-26` | | `MACOS_RUNNER_DISPLAY` | macOS GUI, XCUITest, and virtual-display tests (`tests-build-and-lag`) | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | -| `MACOS_RUNNER_IOS` | the iOS image: simulator tests, TestFlight upload, and `ios-streamed-validate.yml` (`test-ios.yml`, `ios-testflight.yml`) | `blacksmith-6vcpu-macos-26` | `blacksmith-6vcpu-macos-26` | +| `MACOS_RUNNER_IOS` | the iOS image: simulator tests and TestFlight upload (`test-ios.yml`, `ios-testflight.yml`); `ios-streamed-validate.yml` and `ios-e2e.yml` hold secrets and take `blacksmith-6vcpu-macos-26` | `blacksmith-6vcpu-macos-26` | `blacksmith-6vcpu-macos-26` | | `CI_PAID_MACOS_OVERFLOW` | the repository-side switch for metered capacity; gates the four paid-overflow variables above (see "Break-glass" below) | unset (free capacity) | unset means the Blacksmith fallback wins | -| `MACOS_RUNNER_BACKGROUND` | non-urgent macOS work only: `build-ghosttykit` and the macOS legs of `cmux-tui-artifacts` (post-merge). See "Background lane" below | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | +| `MACOS_RUNNER_BACKGROUND` | non-urgent macOS work only: the macOS legs of `cmux-tui-artifacts` (post-merge); `build-ghosttykit` holds `GHOSTTY_RELEASE_TOKEN` and takes `blacksmith-6vcpu-macos-15`. See "Background lane" below | `blacksmith-6vcpu-macos-15` | `blacksmith-6vcpu-macos-15` | A runner variable names a **machine capability** — an OS version, a GUI, a simulator, both SDKs, or a larger instance — and every job needing that @@ -789,8 +789,7 @@ A job belongs in the lane only if all of these hold: compare on the same hardware; - it does not need a GUI console session. -Members today: `build-ghosttykit.yml` (Xcode from the image default, Zig -xcframework build), and the two macOS Rust legs of +Members today: the two macOS Rust legs of `cmux-tui-artifacts.yml` (passed as `macos_runner` to `cmux-tui-build-package.yml`; release and full-suite callers keep their own runner). diff --git a/tests/test_ci_fork_runner_routing.py b/tests/test_ci_fork_runner_routing.py index 85393dab370c..94450a7fb673 100644 --- a/tests/test_ci_fork_runner_routing.py +++ b/tests/test_ci_fork_runner_routing.py @@ -233,14 +233,19 @@ def _string(node: tuple) -> str | None: def _bounded_pick(conditions: list[tuple], picked: tuple) -> list[str]: """The labels `... && contains(fromJSON('[labels]'), R) && R` can yield: R only when it equals - (ignoring case) one of the listed labels. [] when no such contains() guards R.""" - name = _refs(picked)[0] if picked[0] == "ref" else None + (ignoring case) one of the listed labels. [] when no such contains() guards R. R is a ref + (`vars.X` and `vars['X']` are the same) or any expression written the same both times, such + as a capability `fromJSON(needs.runners.outputs.map).key`: an expression has one value.""" + def same(needle: tuple) -> bool: + if picked[0] == "ref" and needle[0] == "ref": + return _refs(needle) == _refs(picked) + return picked[0] != "literal" and needle == picked for condition in conditions: - if (name and condition[0] == "call" and len(condition) == 3 and condition[1] == "contains" - and len(condition[2]) == 2 + if (picked[0] in ("ref", "call") and condition[0] == "call" and len(condition) == 3 + and condition[1] == "contains" and len(condition[2]) == 2 and condition[2][0][0] == "call" and len(condition[2][0]) == 3 and condition[2][0][1] == "fromJSON" and len(condition[2][0][2]) == 1 and _string(condition[2][0][2][0]) is not None - and condition[2][1][0] == "ref" and _refs(condition[2][1]) == [name]): + and same(condition[2][1])): try: labels = json.loads(_string(condition[2][0][2][0])) except json.JSONDecodeError: @@ -308,9 +313,6 @@ def runner_label_errors(runs_on: str) -> list[str]: ("cmux-next-dev-daily.yml", "tip-deltas"): "the runner job's `cloud`/`alternate` outputs, from classic_nightly_build_runner.py: " "only Blacksmith macOS labels", - ("ios-app-store.yml", "upload-and-validate"): - "resolve_runners.py's macos_26_ios capability: .github/runners.json maps it to " - "blacksmith-6vcpu-macos-26 or the hosted macos-26 only", ("nightly.yml", "build-nightly-app"): "open: CI_CACHE_R2 and SENTRY_AUTH_TOKEN on the pinned trusted mini (CI_NIGHTLY_TRUSTED_RUNNER) " "by design; nightlies never use Blacksmith", @@ -328,7 +330,7 @@ def runner_label_errors(runs_on: str) -> list[str]: # A secret a job reads: `${{ secrets.X }}` (not the job's own GITHUB_TOKEN), `secrets[...]`, or # `secrets: inherit` into a reusable workflow. Context names are case-insensitive. SECRET_MARKER = re.compile( - r"\$\{\{(?:[^}]|\}(?!\}))*?\bsecrets\s*(?:\.\s*(?!github_token\b)[a-z_][a-z0-9_]*|\[)|^\s*secrets:\s*inherit\b", + r"\$\{\{(?:[^}]|\}(?!\}))*?\bsecrets\b(?!\s*\.\s*github_token\b)|^\s*secrets:\s*inherit\b", re.IGNORECASE | re.MULTILINE, ) @@ -339,8 +341,10 @@ def secret_jobs() -> list[tuple[str, str, object, dict]]: for path in sorted(WORKFLOWS.glob("*.y*ml")): text = path.read_text(encoding="utf-8") document = yaml.load(text, Loader=yaml.BaseLoader) or {} + # A workflow-level env reaches every job, so a secret there counts for each. + shared = yaml.safe_dump(document.get("env") or {}, sort_keys=False, width=100000) for name, job in (document.get("jobs") or {}).items(): - body = yaml.safe_dump(job, sort_keys=False, width=100000) + body = shared + yaml.safe_dump(job, sort_keys=False, width=100000) if SECRET_MARKER.search(body) or APP_KEY_MARKER.search(body): found.append((path.name, name, job.get("runs-on"), job)) return found @@ -363,8 +367,13 @@ def job_runner_errors(workflow: str, job: dict) -> list[str]: matrix = (job.get("strategy") or {}).get("matrix") if not isinstance(matrix, dict): return [f"runs-on reads matrix.{key.group(1)} from a computed matrix {matrix!r}"] - values = list(matrix.get(key.group(1)) or []) - values += [entry[key.group(1)] for entry in matrix.get("include") or [] if key.group(1) in entry] + listed, include = matrix.get(key.group(1)) or [], matrix.get("include") or [] + if not isinstance(listed, list) or not isinstance(include, list): + return [f"runs-on reads matrix.{key.group(1)} from a computed list"] + values = list(listed) + values += [entry[key.group(1)] for entry in include if isinstance(entry, dict) and key.group(1) in entry] + if any(not isinstance(entry, dict) for entry in include): + return [f"matrix.include of matrix.{key.group(1)} holds a computed entry"] if not values: return [f"matrix.{key.group(1)} lists no value"] return [error for value in values for error in runner_label_errors(str(value))] @@ -1351,6 +1360,8 @@ def test_the_secret_detector_and_matrix_check(self) -> None: for held in ("${{ secrets.R2_KEY }}", "${{ SECRETS.Vercel_Token }}", "${{ secrets['X'] }}", "secrets: inherit", "${{ format('{0}', secrets.A) }}"): self.assertTrue(SECRET_MARKER.search(held), held) + for held in ("${{ toJSON(secrets) }}", "${{ secrets }}", "${{ fromJSON(toJSON(secrets)).X }}"): + self.assertTrue(SECRET_MARKER.search(held), held) for clean in ("${{ secrets.GITHUB_TOKEN }}", "${{ secrets.github_token }}", "python3 -c 'import secrets'", "secrets.token_hex(8)", "${{ github.token }}"): self.assertFalse(SECRET_MARKER.search(clean), clean) @@ -1361,6 +1372,12 @@ def test_the_secret_detector_and_matrix_check(self) -> None: self.assertNotEqual(job_runner_errors("x.yml", moved), []) computed = {"runs-on": "${{ matrix.pool }}", "strategy": {"matrix": "${{ fromJSON(needs.a.outputs.m) }}"}} self.assertNotEqual(job_runner_errors("x.yml", computed), []) + # Review of #27648: a computed include must not pass as an empty list. + included = {"runs-on": "${{ matrix.pool }}", "strategy": {"matrix": { + "pool": ["blacksmith-6vcpu-macos-15"], "include": "${{ fromJSON(vars.EXTRA) }}"}}} + self.assertNotEqual(job_runner_errors("x.yml", included), []) + listed = {"runs-on": "${{ matrix.pool }}", "strategy": {"matrix": {"pool": "${{ fromJSON(vars.P) }}"}}} + self.assertNotEqual(job_runner_errors("x.yml", listed), []) self.assertNotEqual(job_runner_errors("x.yml", {"uses": "org/repo/.github/workflows/x.yml@v1"}), []) self.assertEqual(runner_label_errors("ubuntu-latest"), []) # An index after fromJSON turns the trusted fork exclusion into a substring test. @@ -1464,6 +1481,12 @@ def test_the_app_key_runner_rule_refuses_a_variable(self) -> None: "'blacksmith-4vcpu-ubuntu-2404' }}", "${{ 'github.repository_owner' != 'manaflow-ai' && 'ubuntu-24.04' || " "'blacksmith-4vcpu-ubuntu-2404' }}", + # A capability pick must name the same expression in contains() and as the result. + f"${{{{ {fork}contains(fromJSON('[\"blacksmith-6vcpu-macos-26\"]'), fromJSON(needs.r.outputs.map).a) " + "&& fromJSON(needs.r.outputs.map).b || 'blacksmith-6vcpu-macos-26' }}", + f"${{{{ {fork}contains(fromJSON('[\"blacksmith-6vcpu-macos-26\"]'), needs.r.outputs.a) " + "&& fromJSON(needs.r.outputs.map).a || 'blacksmith-6vcpu-macos-26' }}", + f"${{{{ {fork}fromJSON(needs.r.outputs.map).a || 'blacksmith-6vcpu-macos-26' }}}}", "${{ 'ubuntu-24.04' }}", "glaeda-std-xcode-26.6", "self-hosted"): self.assertNotEqual(runner_label_errors(refused), [], refused) for allowed in ("blacksmith-6vcpu-macos-26", f"${{{{ {fork}'blacksmith-4vcpu-ubuntu-2404' }}}}", @@ -1476,7 +1499,9 @@ def test_the_app_key_runner_rule_refuses_a_variable(self) -> None: f"${{{{ {fork}github.event_name == 'merge_group' && 'blacksmith-32vcpu-ubuntu-2404' || " f"{BOUNDED_LINUX_SELECTOR} }}}}", f"${{{{ {fork}vars.MACOS_RUNNER_PR == 'blacksmith-6vcpu-macos-26' && " - "'blacksmith-12vcpu-macos-26' || 'blacksmith-6vcpu-macos-15' }}"): + "'blacksmith-12vcpu-macos-26' || 'blacksmith-6vcpu-macos-15' }}", + f"${{{{ {fork}contains(fromJSON('[\"blacksmith-6vcpu-macos-26\"]'), fromJSON(needs.r.outputs.map).a) " + "&& fromJSON(needs.r.outputs.map).a || 'blacksmith-6vcpu-macos-26' }}"): self.assertEqual(runner_label_errors(allowed), [], allowed) def test_trusted_runner_selector_admits_only_ephemeral_labels(self) -> None: diff --git a/tests/test_ci_runner_capability_resolver.py b/tests/test_ci_runner_capability_resolver.py index f4c39ec93430..3546aca3a3d2 100755 --- a/tests/test_ci_runner_capability_resolver.py +++ b/tests/test_ci_runner_capability_resolver.py @@ -261,7 +261,7 @@ def test_the_proof_of_concept_names_a_declared_capability(self) -> None: declared = set(resolver.load_map(REAL_MAP)["capabilities"]) used = set() for chunk in text.split(marker)[1:]: - used.add(chunk.split()[0].strip("}").strip()) + used.add(chunk.split()[0].strip("})").strip()) self.assertTrue(used) self.assertEqual(used - declared, set()) diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index eee132e985c6..9e130ad1501f 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -1807,7 +1807,7 @@ run_check check_release_build_runner_disk_capacity run_check check_display_runner_identity_guard "$CI_MACOS_FILE" "tests-build-and-lag" run_check check_build_lag_deriveddata_cache_path -# build-ghosttykit.yml (routed through the MACOS_RUNNER_BACKGROUND repo var) +# build-ghosttykit.yml (a literal Blacksmith label: it holds GHOSTTY_RELEASE_TOKEN) run_check check_macos_runner "$GHOSTTYKIT_FILE" "build-ghosttykit" # ci-macos-compat.yml (matrix.os routed through the MACOS_RUNNER_* repo vars) From 91226bb32e61ec71d35f511e43f852834f92e7e0 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:07:26 +0000 Subject: [PATCH 4/4] test(seed): the nightly seeder's pools match the per-push seeder's by value CI on #27648: test_seed_derived_data compared the nightly refresh-test-compilation-cache pool expressions with the seeder's as text, so the nightly row that no longer falls through to MACOS_RUNNER_PR failed it. It now evaluates both for each lane MACOS_RUNNER_PR may name (Blacksmith sizes, an owned pool, unset): each nightly pool is a pool the per-push seeder uses, and a Blacksmith label. On the base it fails for an owned lane: the nightly seeder, which holds CI_CACHE_R2, ran on the overflow switch's owned pool. --- tests/test_seed_derived_data.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_seed_derived_data.py b/tests/test_seed_derived_data.py index e960c2dc5187..7a3b3b4a4cfc 100644 --- a/tests/test_seed_derived_data.py +++ b/tests/test_seed_derived_data.py @@ -963,7 +963,17 @@ def test_every_main_push_seeds_incrementally_under_the_key_admission_reads(self) # Every nightly cold seed lands on a pool the per-push seeder uses, on # the same Xcode, and the macOS 15 pool gets one too. self.assertEqual(nightly["runs-on"], "${{ matrix.pool }}") - self.assertLessEqual(set(nightly["strategy"]["matrix"]["pool"]), set(seed_pools())) + # By value, for each lane MACOS_RUNNER_PR may name: the nightly seeder holds CI_CACHE_R2, so it + # never runs on the variable's own value (an owned pool in an overflow; + # tests/test_ci_fork_runner_routing.py), but each pool it picks is one the per-push seeder uses. + for lane in ("blacksmith-6vcpu-macos-26", "blacksmith-6vcpu-macos-15", "blacksmith-12vcpu-macos-26", + "glaeda-std-xcode-26.6", ""): + context = github_context("schedule", MACOS_RUNNER_PR=lane) + seeded = {evaluate(pool, context) for pool in seed_pools()} + for pool in nightly["strategy"]["matrix"]["pool"]: + picked = evaluate(pool, context) + self.assertIn(picked, seeded, (lane, pool)) + self.assertTrue(picked.startswith("blacksmith-"), (lane, picked)) self.assertEqual(nightly["strategy"]["matrix"]["pool"][-1], seed_pools()[-1]) self.assertEqual(nightly["env"]["CMUX_CI_XCODE_APP"], job["env"]["CMUX_CI_XCODE_APP"]) # On the lane's pools the seeder uses the nightly's Xcode.