From fb82d87cada297859fd0cbebb236aaa776aa05f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 13 Aug 2026 10:18:35 -0700 Subject: [PATCH] fix: keep CI running during Blacksmith outages (#123263) * ci: add runner-backend circuit breaker with GitHub-hosted fallback * fix(ci): extend hosted timeouts to circuit-breaker runs --- .agents/skills/openclaw-ci-limits/SKILL.md | 30 ++++ .github/workflows/ci.yml | 154 +++++++++--------- docs/ci.md | 20 ++- test/scripts/ci-workflow-guards.test.ts | 175 ++++++++++++++++++++- 4 files changed, 295 insertions(+), 84 deletions(-) diff --git a/.agents/skills/openclaw-ci-limits/SKILL.md b/.agents/skills/openclaw-ci-limits/SKILL.md index 28d378640b0f..06b434e26a00 100644 --- a/.agents/skills/openclaw-ci-limits/SKILL.md +++ b/.agents/skills/openclaw-ci-limits/SKILL.md @@ -175,12 +175,42 @@ These are intentionally guarded by `test/scripts/ci-workflow-guards.test.ts`: - lower-weight Node/check shards on `blacksmith-4vcpu-ubuntu-2404`. - heavy retained Linux/Android shards on `blacksmith-8vcpu-ubuntu-2404`. - CodeQL Critical Quality on `ubuntu-24.04` with no `blacksmith-` labels. +- `OPENCLAW_CI_RUNNER_BACKEND=github` routes every configurable `ci.yml` job + to its existing GitHub-hosted fallback label. Unset or `blacksmith` preserves + the normal Blacksmith-first route. - Vitest/test compile caches are restore-only in CI and use immutable Actions caches; the daily/dispatch warmer is their sole writer. Build compile cache writes rotate at most once per UTC day. PRs create no runtime-cache archives. When changing one knob, update `docs/ci.md` and the guard test in the same PR. +## Blacksmith Outage Circuit Breaker + +Use the repository variable only after confirming a Blacksmith outage or +unavailable runner capacity. Do not set it merely for a failing test that has +already started. + +```bash +gh variable set OPENCLAW_CI_RUNNER_BACKEND --repo openclaw/openclaw --body github +``` + +In degraded mode, `ci.yml` uses the same hosted labels and non-Blacksmith paths +as manual dispatches and fork pull requests. Blacksmith-only Docker and sticky +steps stay off, dependency setup uses the ordinary Actions pnpm-store cache, +and Android's large build uses separate low-memory Gradle processes. Standard +4-core hosted runners make builds and test lanes slower. Blacksmith runner +registration is no longer part of the budget, while GitHub-hosted concurrency +limits apply. + +Flip back after the outage by deleting the variable: + +```bash +gh variable delete OPENCLAW_CI_RUNNER_BACKEND --repo openclaw/openclaw +``` + +Scheduled health detection and automatic flipping are a follow-up, not part of +the current circuit breaker. + ## Validation For workflow-only or docs/skill-only changes in a Codex worktree: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b8dcdb95f8..2a7ea153b74b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: permissions: contents: read if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 outputs: checkout_revision: ${{ steps.checkout_ref.outputs.sha }} @@ -871,10 +871,11 @@ jobs: run: node scripts/check-protocol-event-coverage.mjs # Publish one immutable semantic dependency archive before same-repo - # Blacksmith jobs fan out. Pull-request archives remain merge-ref scoped; + # Blacksmith jobs fan out. The GitHub backend uses the pnpm-store cache. + # Pull-request archives remain merge-ref scoped; # main archives seed later pull requests through the default-branch scope. - name: Publish exact dependency cache - if: github.repository == 'openclaw/openclaw' && steps.manifest.outputs.run_node == 'true' && ((github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) + if: vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.repository == 'openclaw/openclaw' && steps.manifest.outputs.run_node == 'true' && ((github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) uses: ./.github/actions/setup-node-env with: dependency-cache: "true" @@ -1088,14 +1089,14 @@ jobs: run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high # Warm the lockfile- and pnpm-pinned Actions cache for fork PRs, manual runs, - # and docs-only same-repo PRs. Node-relevant canonical main and same-repo PRs - # already publish it through the exact dependency-cache writer in preflight. + # the GitHub backend, and docs-only same-repo PRs. Node-relevant Blacksmith + # runs already publish it through the exact dependency-cache writer in preflight. pnpm-store-warmup: permissions: contents: read needs: [preflight] - if: ${{ (needs.preflight.outputs.run_node == 'true' || needs.preflight.outputs.run_check_docs == 'true') && !(github.repository == 'openclaw/openclaw' && github.event_name == 'push' && github.ref == 'refs/heads/main') && !(github.repository == 'openclaw/openclaw' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.preflight.outputs.run_node == 'true') }} - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + if: ${{ (needs.preflight.outputs.run_node == 'true' || needs.preflight.outputs.run_check_docs == 'true') && (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || (!(github.repository == 'openclaw/openclaw' && github.event_name == 'push' && github.ref == 'refs/heads/main') && !(github.repository == 'openclaw/openclaw' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.preflight.outputs.run_node == 'true'))) }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 steps: - &linux_node_checkout_step @@ -1165,8 +1166,8 @@ jobs: # 32 vCPU: the dist build sits on the light-run critical path alongside # check-lint/check-dependencies; tsdown parallelizes across the extra # cores for roughly the same billed core-minutes. - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }} - timeout-minutes: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 35 || 20 }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + timeout-minutes: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 35 || 20 }} outputs: channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }} core-support-boundary-result: ${{ steps.built_artifact_checks.outputs['core-support-boundary-result'] }} @@ -1189,8 +1190,8 @@ jobs: node-compile-cache-scope: "build" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} save-node-compile-cache: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'true' || 'false' }} - name: Restore build-all step cache @@ -1448,7 +1449,7 @@ jobs: name: check-sqlite-session-lifecycle needs: [preflight, build-artifacts] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_sqlite_session_lifecycle == 'true' && needs.build-artifacts.result == 'success' }} - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 steps: - *linux_node_checkout_step @@ -1456,8 +1457,8 @@ jobs: uses: ./.github/actions/setup-node-env with: install-bun: "false" - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Download exact-run built runtime uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -1479,7 +1480,7 @@ jobs: contents: read needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_native_i18n == 'true' }} - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 10 steps: - name: Checkout @@ -1494,8 +1495,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Verify native app i18n source run: | @@ -1523,7 +1524,7 @@ jobs: name: checks-ui needs: [preflight] if: needs.preflight.outputs.run_ui_tests == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 env: COMPATIBILITY_TARGET: ${{ needs.preflight.outputs.compatibility_target }} @@ -1536,8 +1537,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - &install_playwright_chromium name: Install Playwright Chromium @@ -1582,9 +1583,9 @@ jobs: # Compatibility targets pin a frozen Control UI whose e2e expectations track # that release, not current main. if: needs.preflight.outputs.run_ui_tests == 'true' && needs.preflight.outputs.compatibility_target != 'true' - # First-attempt same-repo runs use Blacksmith; manual dispatches, forks, and - # same-repo PR retries use GitHub-hosted capacity as an independent fallback. - runs-on: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + # First-attempt same-repo runs use Blacksmith unless the backend breaker is + # set; manual dispatches, forks, and same-repo PR retries also use GitHub-hosted capacity. + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} # Keep Chromium ownership serial within each independently required shard. timeout-minutes: 25 env: @@ -1603,8 +1604,8 @@ jobs: install-bun: "false" # Hosted paths use the pnpm store cache; first-attempt same-repo # Blacksmith runs restore the preflight-published dependency tree. - dependency-cache: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'false' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false') }} - use-actions-cache: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }} + dependency-cache: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'false' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false') }} + use-actions-cache: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }} - *install_playwright_chromium @@ -1628,9 +1629,9 @@ jobs: # Compatibility targets pin a frozen Control UI whose e2e expectations track # that release, not current main. if: needs.preflight.outputs.run_ui_tests == 'true' && needs.preflight.outputs.compatibility_target != 'true' - # First-attempt same-repo runs use Blacksmith; manual dispatches, forks, and - # same-repo PR retries use GitHub-hosted capacity as an independent fallback. - runs-on: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + # First-attempt same-repo runs use Blacksmith unless the backend breaker is + # set; manual dispatches, forks, and same-repo PR retries also use GitHub-hosted capacity. + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 steps: - *linux_node_checkout_step @@ -1641,8 +1642,8 @@ jobs: install-bun: "false" # Hosted paths use the pnpm store cache; first-attempt same-repo # Blacksmith runs restore the preflight-published dependency tree. - dependency-cache: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'false' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false') }} - use-actions-cache: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }} + dependency-cache: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'false' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false') }} + use-actions-cache: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }} - *install_playwright_chromium @@ -1666,7 +1667,7 @@ jobs: name: control-ui-i18n needs: [preflight] if: needs.preflight.outputs.run_control_ui_i18n == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 10 env: COMPATIBILITY_TARGET: ${{ needs.preflight.outputs.compatibility_target }} @@ -1679,8 +1680,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Verify Control UI i18n source run: | @@ -1706,7 +1707,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_fast_core == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} timeout-minutes: 60 strategy: fail-fast: false @@ -1767,8 +1768,8 @@ jobs: install-bun: ${{ matrix.task == 'bun-launcher' && 'true' || 'false' }} # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) env: @@ -1844,7 +1845,7 @@ jobs: name: QA Smoke CI (${{ matrix.name }}) needs: [preflight] if: needs.preflight.outputs.run_qa_smoke_ci == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 60 strategy: fail-fast: false @@ -1872,11 +1873,11 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Set up Blacksmith Docker layer cache - if: ${{ matrix.docker_cache == true && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') }} + if: ${{ matrix.docker_cache == true && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') }} uses: useblacksmith/setup-docker-builder@6ff44f8e5255f9d8aa31ef22f7e57a2d926b7da0 # v1 with: max-cache-size-mb: 800000 @@ -1908,7 +1909,7 @@ jobs: env: PROFILE_PART: ${{ matrix.lane }} PROFILE_PART_SLUG: ${{ matrix.slug }} - OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && '0' || '1500' }} + OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && '0' || '1500' }} shell: bash run: | set -euo pipefail @@ -2038,7 +2039,7 @@ jobs: name: ${{ matrix.checkName }} needs: [preflight] if: needs.preflight.outputs.run_plugin_contracts_shards == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 60 strategy: fail-fast: false @@ -2052,8 +2053,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Run plugin contract shard env: @@ -2080,7 +2081,7 @@ jobs: name: ${{ matrix.checkName }} needs: [preflight] if: needs.preflight.outputs.run_channel_contracts_shards == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 60 strategy: fail-fast: false @@ -2094,8 +2095,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Run channel contract shard env: @@ -2123,7 +2124,7 @@ jobs: name: checks-node-compat-node22 needs: [preflight] if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 60 steps: - *linux_node_checkout_step @@ -2153,7 +2154,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_node_core_nondist == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} timeout-minutes: ${{ matrix.timeout_minutes || 60 }} strategy: fail-fast: false @@ -2170,8 +2171,8 @@ jobs: install-bun: "false" # Only the preflight writer saves an archive. Same-repo Blacksmith # shards restore it; hosted and non-Node-24 paths use the pnpm store. - dependency-cache: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} vitest-fs-cache: "true" node-compile-cache: "true" node-compile-cache-scope: "test" @@ -2264,7 +2265,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }} - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} timeout-minutes: 20 strategy: fail-fast: false @@ -2306,15 +2307,15 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} # check-lint's shard runner rebuilds the same plugin-sdk boundary # artifacts the boundary lane snapshots (~72s cold); restore them from # the shared sticky. Strictly read-only (commit: false): only the # protected boundary lane may publish this repository-global snapshot. - name: Mount extension boundary sticky disk - if: matrix.task == 'lint' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') + if: matrix.task == 'lint' && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') uses: useblacksmith/stickydisk@6d373c96a74cbde0c99fedc5ea5d3a7ba66ba494 # main (post-v1.4.0 hot-attach fix) with: # One stable disk for the whole repository. The v1 per-PR/per-config @@ -2327,7 +2328,7 @@ jobs: commit: "false" - name: Restore extension boundary artifacts from sticky disk - if: matrix.task == 'lint' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') + if: matrix.task == 'lint' && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') shell: bash env: # Config/toolchain inputs the tree-OID gate below cannot see; must @@ -2544,7 +2545,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }} - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }} timeout-minutes: 20 strategy: fail-fast: false @@ -2610,8 +2611,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} # Same-repo runs carry boundary artifacts on a Blacksmith sticky disk: # the GitHub cache below is evicted so quickly under the repo quota that @@ -2619,7 +2620,7 @@ jobs: # plugin-sdk declarations. Fork PRs must never produce writable # repository-global snapshots, so they keep the GitHub cache path. - name: Mount extension boundary sticky disk - if: matrix.group == 'extension-package-boundary' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') + if: matrix.group == 'extension-package-boundary' && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') uses: useblacksmith/stickydisk@6d373c96a74cbde0c99fedc5ea5d3a7ba66ba494 # main (post-v1.4.0 hot-attach fix) with: # One stable disk for the whole repository. The v1 per-PR/per-config @@ -2640,7 +2641,7 @@ jobs: - name: Restore extension boundary artifacts from sticky disk id: boundary-sticky-restore - if: matrix.group == 'extension-package-boundary' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') + if: matrix.group == 'extension-package-boundary' && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') shell: bash env: # Config/toolchain inputs the tree-OID gate below cannot see; must @@ -2861,7 +2862,7 @@ jobs: # clone). Seed the payload before the sticky post-action flushes. # rsync -aR mirrors the repo-relative layout the restore step replays. - name: Seed extension boundary sticky disk - if: success() && steps.boundary-sticky-restore.outputs.restored == 'false' && matrix.group == 'extension-package-boundary' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' && github.repository == 'openclaw/openclaw' + if: success() && steps.boundary-sticky-restore.outputs.restored == 'false' && matrix.group == 'extension-package-boundary' && vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' && github.repository == 'openclaw/openclaw' shell: bash env: # Must stay identical to the restore gates above. @@ -2889,7 +2890,7 @@ jobs: contents: read needs: [preflight] if: needs.preflight.outputs.run_check_docs == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 steps: - *linux_node_checkout_step @@ -2899,8 +2900,8 @@ jobs: install-bun: "false" # Same-repo Blacksmith runs restore the dependency cache published by # preflight; hosted paths use the pnpm store cache instead. - dependency-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} - use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + dependency-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} - name: Check formatting if: needs.preflight.outputs.run_format_check == 'true' @@ -2958,7 +2959,7 @@ jobs: contents: read needs: [preflight] if: needs.preflight.outputs.run_skills_python_job == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 steps: - name: Checkout @@ -3013,7 +3014,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_windows == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-8vcpu-windows-2025') || 'windows-2025') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'windows-2025' || github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && (matrix.runner || 'blacksmith-8vcpu-windows-2025') || 'windows-2025') }} timeout-minutes: 60 env: NODE_OPTIONS: --max-old-space-size=8192 @@ -3159,7 +3160,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_macos_node == 'true' }} - runs-on: ${{ (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'macos-15' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-6vcpu-macos-15' || 'macos-15') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'macos-15' || (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'macos-15' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-6vcpu-macos-15' || 'macos-15') }} timeout-minutes: 20 strategy: fail-fast: false @@ -3198,10 +3199,10 @@ jobs: name: "macos-swift" needs: [preflight] if: needs.preflight.outputs.run_macos_swift == 'true' - runs-on: ${{ (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'macos-26' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'macos-26' || (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'macos-26' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} # Hosted runs may still be finalizing large Swift caches after every test # passes; keep that post-job work non-blocking. - timeout-minutes: ${{ (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 30 || 20 }} + timeout-minutes: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 30 || 20 }} env: HISTORICAL_TARGET: ${{ needs.preflight.outputs.compatibility_target }} SWIFT_TEST_EXECUTION: ${{ (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'serial' || 'parallel' }} @@ -3417,7 +3418,7 @@ jobs: name: "ios-build" needs: [preflight] if: needs.preflight.outputs.run_ios_build == 'true' - runs-on: ${{ (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'macos-26' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'macos-26' || (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) && 'macos-26' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} timeout-minutes: 150 env: HISTORICAL_TARGET: ${{ needs.preflight.outputs.compatibility_target }} @@ -3598,7 +3599,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_android_job == 'true' - runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} timeout-minutes: 20 strategy: fail-fast: false @@ -3672,11 +3673,11 @@ jobs: # build caches) on a Blacksmith sticky disk: the setup-java gradle cache # above is evicted so quickly under the repo quota that it rarely # restores, leaving every run to re-resolve the full dependency graph. - # Fork PRs and manual dispatches must never produce writable + # Fork PRs, manual dispatches, and the GitHub backend must never produce writable # repository-global snapshots, so they keep the GitHub cache path (their # runs-on already selects non-Blacksmith ubuntu-24.04 runners). - name: Mount Gradle sticky disk - if: github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') + if: vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') uses: useblacksmith/stickydisk@6d373c96a74cbde0c99fedc5ea5d3a7ba66ba494 # main (post-v1.4.0 hot-attach fix) with: # One stable disk per matrix task. The v1 per-PR/per-dependency-hash @@ -3698,7 +3699,7 @@ jobs: commit: ${{ github.event_name != 'pull_request' && 'true' || 'false' }} - name: Point Gradle at the sticky disk - if: github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') + if: vars.OPENCLAW_CI_RUNNER_BACKEND != 'github' && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') shell: bash env: # Gradle caches are content-addressed, so a stale snapshot is safe @@ -3731,6 +3732,7 @@ jobs: - name: Run Android ${{ matrix.task }} working-directory: apps/android env: + CI_RUNNER_BACKEND: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND }} TASK: ${{ matrix.task }} shell: bash run: | @@ -3753,7 +3755,7 @@ jobs: ./gradlew --no-daemon --build-cache :wear:testDebugUnitTest ;; build-play) - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then + if [ "$CI_RUNNER_BACKEND" = "github" ] || [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then # GitHub-hosted runners have less memory headroom. Separate Gradle # processes release each variant's build state before the next starts. ./gradlew --no-daemon --build-cache \ diff --git a/docs/ci.md b/docs/ci.md index 2727e50e6870..766e5f3daf9e 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -202,6 +202,24 @@ for commands and recovery. | `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | | `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` | +### Blacksmith outage circuit breaker + +The repository variable `OPENCLAW_CI_RUNNER_BACKEND` controls the runner backend for `ci.yml`. Leave it unset or set it to `blacksmith` for the normal Blacksmith-first routing. During a Blacksmith outage, set it to `github` to send every configurable CI job to that job's existing GitHub-hosted fallback label: + +```bash +gh variable set OPENCLAW_CI_RUNNER_BACKEND --repo openclaw/openclaw --body github +``` + +Degraded mode uses the same hosted paths exercised by manual dispatches and fork pull requests. Blacksmith-only Docker and sticky-disk steps are skipped, dependency setup uses the ordinary Actions pnpm-store cache, and low-memory Android builds use separate Gradle processes. Expect slower builds and test lanes on standard 4-core hosted runners. Blacksmith's runner-registration budget is irrelevant while the breaker is active, but GitHub-hosted concurrency limits apply. + +Flip back after the outage by deleting the variable, which restores the default behavior: + +```bash +gh variable delete OPENCLAW_CI_RUNNER_BACKEND --repo openclaw/openclaw +``` + +Scheduled health detection and automatic flipping are an explicit follow-up; `ci.yml` does not probe Blacksmith or mutate this variable. + ## Runner registration budget OpenClaw's current GitHub runner-registration bucket reports 10,000 self-hosted @@ -222,7 +240,7 @@ concurrent repositories, retries, and burst overlap. The changed-target PR plan reduces the common Node test burst from 14 Blacksmith registrations to one. Broad-risk PRs keep the 14-registration compact fallback, so the worst case does not increase. -Canonical-repo CI keeps Blacksmith as the default runner path for pushes and first-attempt same-repo pull-request runs. Pull-request retries of both UI E2E jobs use GitHub-hosted Ubuntu; push retries stay on Blacksmith. All `workflow_dispatch` runs, including `release_gate`, and non-canonical repository runs use GitHub-hosted runners. Normal canonical runs do not currently probe Blacksmith queue health or automatically fall back to GitHub-hosted labels when Blacksmith is unavailable. +Canonical-repo CI keeps Blacksmith as the default runner path for pushes and first-attempt same-repo pull-request runs. Pull-request retries of both UI E2E jobs use GitHub-hosted Ubuntu; push retries stay on Blacksmith. All `workflow_dispatch` runs, including `release_gate`, and non-canonical repository runs use GitHub-hosted runners. The [Blacksmith outage circuit breaker](#blacksmith-outage-circuit-breaker) provides a manual repository-wide fallback; canonical runs do not probe Blacksmith queue health or flip it automatically. ## Surface ratchets diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 918460c192b1..5af2ca4ec5af 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -80,7 +80,9 @@ function evaluateWorkflowExpression( context: { eventName: "pull_request" | "push" | "workflow_dispatch"; headRepository?: string; + matrix?: Record; repository: string; + runnerBackend?: "" | "blacksmith" | "github"; runAttempt: number; }, ) { @@ -108,6 +110,10 @@ function evaluateWorkflowExpression( } : {}, }, + matrix: context.matrix ?? {}, + vars: { + OPENCLAW_CI_RUNNER_BACKEND: context.runnerBackend ?? "", + }, }); } @@ -2753,9 +2759,9 @@ NODE ); const buildPlayBranches = expectDefined( buildPlayCase.match( - /if \[ "\$GITHUB_EVENT_NAME" = "workflow_dispatch" \]; then\n([\s\S]*?)\n\s*else\n([\s\S]*?)\n\s*fi/u, + /if \[ "\$CI_RUNNER_BACKEND" = "github" \] \|\| \[ "\$GITHUB_EVENT_NAME" = "workflow_dispatch" \]; then\n([\s\S]*?)\n\s*else\n([\s\S]*?)\n\s*fi/u, ), - "Android build-play event branches", + "Android build-play runner branches", ); const dispatchBuild = expectDefined(buildPlayBranches[1], "hosted dispatch build branch"); const blacksmithBuild = expectDefined(buildPlayBranches[2], "Blacksmith build branch"); @@ -2772,8 +2778,9 @@ NODE expect(source).toContain('task: useCompatibleAndroidCi ? "build-play-compat" : "build-play"'); expect(androidJob.name).toBe("${{ matrix.check_name }}"); expect(androidJob["runs-on"]).toBe( - "${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}", + "${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}", ); + expect(runStep.env.CI_RUNNER_BACKEND).toBe("${{ vars.OPENCLAW_CI_RUNNER_BACKEND }}"); expect(runStep.run).toContain(":app:testPlayDebugUnitTest"); expect(runStep.run).toContain(":app:testThirdPartyDebugUnitTest"); expect(dispatchBuild.match(/^\s*\.\/gradlew\b/gmu)).toHaveLength(3); @@ -2843,6 +2850,110 @@ NODE expect(workflow.jobs["pnpm-store-warmup"]["runs-on"]).toContain("blacksmith-4vcpu-ubuntu-2404"); }); + it("routes every configurable CI job to GitHub-hosted runners when the backend breaker is set", () => { + const workflow = readCiWorkflow(); + const jobs = workflow.jobs as Record; + const expectedHostedRunners = { + android: "ubuntu-24.04", + "build-artifacts": "ubuntu-24.04", + "check-additional-shard": "ubuntu-24.04", + "check-docs": "ubuntu-24.04", + "check-shard": "ubuntu-24.04", + "checks-fast-channel-contracts-shard": "ubuntu-24.04", + "checks-fast-core": "ubuntu-24.04", + "checks-fast-plugin-contracts-shard": "ubuntu-24.04", + "checks-node-compat": "ubuntu-24.04", + "checks-node-core-test-nondist-shard": "ubuntu-24.04", + "checks-ui": "ubuntu-24.04", + "checks-ui-e2e": "ubuntu-24.04", + "checks-ui-e2e-real-gateway": "ubuntu-24.04", + "control-ui-i18n": "ubuntu-24.04", + "ios-build": "macos-26", + "macos-node": "macos-15", + "macos-swift": "macos-26", + "native-i18n": "ubuntu-24.04", + "pnpm-store-warmup": "ubuntu-24.04", + preflight: "ubuntu-24.04", + "qa-smoke-ci-profile": "ubuntu-24.04", + "skills-python": "ubuntu-24.04", + "sqlite-session-lifecycle": "ubuntu-24.04", + "checks-windows": "windows-2025", + } as const; + const configurableJobs = Object.entries(jobs) + .filter(([, job]) => String(job["runs-on"]).startsWith("${{")) + .map(([jobName]) => jobName) + .toSorted(); + const canonicalPullRequest = { + eventName: "pull_request", + headRepository: "openclaw/openclaw", + matrix: { runner: "blacksmith-32vcpu-ubuntu-2404" }, + repository: "openclaw/openclaw", + runAttempt: 1, + } as const; + + expect(configurableJobs).toEqual(Object.keys(expectedHostedRunners).toSorted()); + for (const [jobName, hostedRunner] of Object.entries(expectedHostedRunners)) { + const expression = jobs[jobName]?.["runs-on"]; + expect(expression, jobName).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND == 'github'"); + expect( + evaluateWorkflowExpression(expression, { + ...canonicalPullRequest, + runnerBackend: "github", + }), + jobName, + ).toBe(hostedRunner); + expect( + evaluateWorkflowExpression(expression, { + ...canonicalPullRequest, + runnerBackend: "blacksmith", + }), + jobName, + ).toBe(evaluateWorkflowExpression(expression, canonicalPullRequest)); + } + }); + + it("gives breaker-routed hosted jobs their hosted timeout budgets", () => { + const workflow = readCiWorkflow(); + const jobs = workflow.jobs as Record; + const expectedHostedTimeouts = { + "build-artifacts": 35, + "macos-swift": 30, + } as const; + const routeDependentTimeoutJobs = Object.entries(jobs) + .filter(([, job]) => { + const timeout = job["timeout-minutes"]; + return typeof timeout === "string" && timeout.includes("github."); + }) + .map(([jobName]) => jobName) + .toSorted(); + const canonicalPullRequest = { + eventName: "pull_request", + headRepository: "openclaw/openclaw", + repository: "openclaw/openclaw", + runAttempt: 1, + } as const; + + expect(routeDependentTimeoutJobs).toEqual(Object.keys(expectedHostedTimeouts).toSorted()); + for (const [jobName, hostedTimeout] of Object.entries(expectedHostedTimeouts)) { + const expression = jobs[jobName]?.["timeout-minutes"]; + expect(expression, jobName).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND == 'github'"); + expect( + evaluateWorkflowExpression(expression, { + ...canonicalPullRequest, + runnerBackend: "github", + }), + jobName, + ).toBe(hostedTimeout); + expect( + evaluateWorkflowExpression(expression, { + ...canonicalPullRequest, + runnerBackend: "blacksmith", + }), + jobName, + ).toBe(20); + } + }); + it("scans only the pull request commit range for leaked credentials", () => { const securitySteps = readCiWorkflow().jobs["security-fast"].steps as WorkflowStep[]; const fetchScanHistoryIndex = securitySteps.findIndex( @@ -2995,6 +3106,10 @@ NODE }); expect(writers[0]?.step.if).toContain("github.ref == 'refs/heads/main'"); expect(writers[0]?.step.if).toContain("github.event_name == 'pull_request'"); + expect(writers[0]?.step.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'"); + expect(workflow.jobs["pnpm-store-warmup"].if).toContain( + "vars.OPENCLAW_CI_RUNNER_BACKEND == 'github'", + ); const consumers = dependencySetups.filter(({ jobName }) => jobName !== "preflight"); expect(consumers.map(({ jobName }) => jobName).toSorted()).toEqual([ "build-artifacts", @@ -3019,6 +3134,29 @@ NODE expect(consumer.with, jobName).not.toHaveProperty("save-dependency-cache"); expect(consumer.with?.["dependency-cache"], jobName).toContain("'true' || 'false'"); expect(consumer.with?.["use-actions-cache"], jobName).toContain("'false' || 'true'"); + expect(consumer.with?.["dependency-cache"], jobName).toContain( + "vars.OPENCLAW_CI_RUNNER_BACKEND", + ); + expect( + evaluateWorkflowExpression(consumer.with?.["dependency-cache"], { + eventName: "push", + matrix: { node_version: "24.x" }, + repository: "openclaw/openclaw", + runnerBackend: "github", + runAttempt: 1, + }), + jobName, + ).toBe("false"); + expect( + evaluateWorkflowExpression(consumer.with?.["use-actions-cache"], { + eventName: "push", + matrix: { node_version: "24.x" }, + repository: "openclaw/openclaw", + runnerBackend: "github", + runAttempt: 1, + }), + jobName, + ).toBe("true"); } for (const { jobName, step: setup } of Object.entries(workflow.jobs).flatMap(([jobName, job]) => ((job as { steps?: WorkflowStep[] }).steps ?? []) @@ -3638,7 +3776,7 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre expect(source).toContain("createNodeTestShardBundles"); expect(workflow.jobs["build-artifacts"]["runs-on"]).toContain("blacksmith-32vcpu-ubuntu-2404"); expect(workflow.jobs["build-artifacts"]["timeout-minutes"]).toBe( - "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 35 || 20 }}", + "${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 35 || 20 }}", ); expect(buildArtifactsTestbox.jobs["build-artifacts"]["runs-on"]).toBe( "blacksmith-16vcpu-ubuntu-2404", @@ -3705,6 +3843,9 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre ); expect(boundaryMount.with.key).toBe("${{ github.repository }}-ext-boundary-v2"); expect(lintMount.with.key).toBe(boundaryMount.with.key); + for (const gate of [boundaryMount, lintMount]) { + expect(gate.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'"); + } // Single semantic writer: protected pushes commit explicitly (not // on-change/if-missing, whose allocated-byte heuristic can strand a stale // marker); PR clones and the lint consumer stay read-only. @@ -3731,6 +3872,7 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre expect(lintRestoreStep.env.BOUNDARY_CONFIG_HASH).toBe(configHash); for (const gate of [restoreStep, lintRestoreStep, seedStep]) { expect(gate.run).toContain('echo "$BOUNDARY_CONFIG_HASH"'); + expect(gate.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'"); } // Seeding is writer-only work: PR mounts never commit, so seeding there // would burn wall clock on a discarded clone. @@ -3756,6 +3898,10 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre // minted a backing disk per PR/bump until Blacksmith's installation-wide // budget 429-failed every mount fleet-wide. expect(mountWith.key).toBe("${{ github.repository }}-gradle-v2-${{ matrix.task }}"); + expect(androidSteps.find((step) => step.name === "Mount Gradle sticky disk")?.if).toContain( + "vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'", + ); + expect(pointStep.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'"); // Single semantic writer: protected pushes commit explicitly (on-change's // allocated-byte heuristic can miss a same-size refresh and strand the // fingerprint marker); PR clones stay read-only. @@ -5579,9 +5725,9 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" "node-version": "24.x", "install-bun": "false", "dependency-cache": - "${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'false' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false') }}", + "${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'false' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false') }}", "use-actions-cache": - "${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }}", + "${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }}", } as const; expect(uiE2eSetup.with).toEqual(expectedUiE2eSetup); const realGatewaySetup = expectDefined( @@ -5615,6 +5761,17 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" }, expected: { blacksmith: true, dependencyCache: "true", useActionsCache: "false" }, }, + { + name: "same-repo pull request with GitHub backend", + context: { + eventName: "pull_request", + headRepository: "openclaw/openclaw", + repository: "openclaw/openclaw", + runnerBackend: "github", + runAttempt: 1, + }, + expected: { blacksmith: false, dependencyCache: "false", useActionsCache: "true" }, + }, { name: "same-repo pull request retry", context: { @@ -5656,7 +5813,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" ] as const; for (const { blacksmithRunner, job, name: jobName, setup } of routedUiE2eJobs) { expect(job["runs-on"]).toBe( - "${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && '" + + "${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && '" + blacksmithRunner + "' || 'ubuntu-24.04') }}", ); @@ -7413,6 +7570,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" "useblacksmith/setup-docker-builder@6ff44f8e5255f9d8aa31ef22f7e57a2d926b7da0", ); expect(smokeDockerCacheStep.if).toContain("matrix.docker_cache == true"); + expect(smokeDockerCacheStep.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'"); expect(smokeDockerCacheStep.if).toContain("github.event_name != 'workflow_dispatch'"); expect(smokeDockerCacheStep.if).toContain("github.repository == 'openclaw/openclaw'"); expect(smokeDockerCacheStep.if).toContain( @@ -7450,6 +7608,9 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" expect(smokeRunStep.env.OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS).toContain( "github.event_name != 'workflow_dispatch'", ); + expect(smokeRunStep.env.OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS).toContain( + "vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'", + ); expect(smokeRunStep.env.OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS).toContain( "github.repository == 'openclaw/openclaw'", );