diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2761a7b0d3b..344cb400b85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: concurrency: group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: true env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" @@ -38,9 +38,8 @@ jobs: id: check uses: ./.github/actions/detect-docs-changes - # Detect which heavy areas are touched so PRs can skip unrelated expensive jobs. - # Push to main keeps broad coverage, but this job still needs to run so - # downstream jobs that list it in `needs` are not skipped. + # Detect which heavy areas are touched so CI can skip unrelated expensive jobs. + # Fail-safe: if detection fails, downstream jobs run. changed-scope: needs: [docs-scope] if: needs.docs-scope.outputs.docs_only != 'true' @@ -82,7 +81,7 @@ jobs: # Build dist once for Node-relevant changes and share it with downstream jobs. build-artifacts: needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout @@ -141,7 +140,7 @@ jobs: checks: needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 strategy: fail-fast: false @@ -149,6 +148,13 @@ jobs: include: - runtime: node task: test + shard_index: 1 + shard_count: 2 + command: pnpm canvas:a2ui:bundle && pnpm test + - runtime: node + task: test + shard_index: 2 + shard_count: 2 command: pnpm canvas:a2ui:bundle && pnpm test - runtime: node task: extensions @@ -179,11 +185,18 @@ jobs: - name: Configure Node test resources if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' + env: + SHARD_COUNT: ${{ matrix.shard_count || '' }} + SHARD_INDEX: ${{ matrix.shard_index || '' }} run: | # `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes. # Default heap limits have been too low on Linux CI (V8 OOM near 4GB). echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV" echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV" + if [ -n "$SHARD_COUNT" ] && [ -n "$SHARD_INDEX" ]; then + echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV" + echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV" + fi - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) if: matrix.runtime != 'bun' || github.event_name != 'push' @@ -193,7 +206,7 @@ jobs: check: name: "check" needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout @@ -239,7 +252,7 @@ jobs: compat-node22: name: "compat-node22" needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout @@ -272,7 +285,7 @@ jobs: skills-python: needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true' || needs.changed-scope.outputs.run_skills_python == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_skills_python == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout @@ -365,7 +378,7 @@ jobs: checks-windows: needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_windows == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_windows == 'true' runs-on: blacksmith-32vcpu-windows-2025 timeout-minutes: 45 env: @@ -727,7 +740,7 @@ jobs: android: needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true') + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_android == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 strategy: fail-fast: false diff --git a/docs/ci.md b/docs/ci.md index 16a7e670964..fb4c4a252e5 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -9,32 +9,32 @@ read_when: # CI Pipeline -The CI runs on every push to `main` and every pull request. It uses smart scoping to skip expensive jobs when only docs or native code changed. +The CI runs on every push to `main` and every pull request. It uses smart scoping to skip expensive jobs when only unrelated areas changed. ## Job Overview -| Job | Purpose | When it runs | -| ----------------- | ------------------------------------------------------- | ------------------------------------------------- | -| `docs-scope` | Detect docs-only changes | Always | -| `changed-scope` | Detect which areas changed (node/macos/android/windows) | Non-docs PRs | -| `check` | TypeScript types, lint, format | Push to `main`, or PRs with Node-relevant changes | -| `check-docs` | Markdown lint + broken link check | Docs changed | -| `code-analysis` | LOC threshold check (1000 lines) | PRs only | -| `secrets` | Detect leaked secrets | Always | -| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes | -| `release-check` | Validate npm pack contents | After build | -| `checks` | Node/Bun tests + protocol check | Non-docs, node changes | -| `checks-windows` | Windows-specific tests | Non-docs, windows-relevant changes | -| `macos` | Swift lint/build/test + TS tests | PRs with macos changes | -| `android` | Gradle build + tests | Non-docs, android changes | +| Job | Purpose | When it runs | +| ----------------- | ------------------------------------------------------- | ---------------------------------- | +| `docs-scope` | Detect docs-only changes | Always | +| `changed-scope` | Detect which areas changed (node/macos/android/windows) | Non-doc changes | +| `check` | TypeScript types, lint, format | Non-docs, node changes | +| `check-docs` | Markdown lint + broken link check | Docs changed | +| `code-analysis` | LOC threshold check (1000 lines) | PRs only | +| `secrets` | Detect leaked secrets | Always | +| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes | +| `release-check` | Validate npm pack contents | After build | +| `checks` | Node/Bun tests + protocol check | Non-docs, node changes | +| `checks-windows` | Windows-specific tests | Non-docs, windows-relevant changes | +| `macos` | Swift lint/build/test + TS tests | PRs with macos changes | +| `android` | Gradle build + tests | Non-docs, android changes | ## Fail-Fast Order Jobs are ordered so cheap checks fail before expensive ones run: -1. `docs-scope` + `code-analysis` + `check` (parallel, ~1-2 min) -2. `build-artifacts` (blocked on above) -3. `checks`, `checks-windows`, `macos`, `android` (blocked on build) +1. `docs-scope` + `changed-scope` + `check` + `secrets` (parallel, cheap gates first) +2. `build-artifacts` + `release-check` +3. `checks` (Linux Node test split into 2 shards), `checks-windows`, `macos`, `android` Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index a4018b30a2c..c5ed28319b1 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -5,6 +5,7 @@ import { appendFileSync } from "node:fs"; const DOCS_PATH_RE = /^(docs\/|.*\.mdx?$)/; const SKILLS_PYTHON_SCOPE_RE = /^skills\//; +const CI_WORKFLOW_SCOPE_RE = /^\.github\/workflows\/ci\.yml$/; const MACOS_PROTOCOL_GEN_RE = /^(apps\/macos\/Sources\/OpenClawProtocol\/|apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\/)/; const MACOS_NATIVE_RE = /^(apps\/macos\/|apps\/ios\/|apps\/shared\/|Swabble\/)/; @@ -55,6 +56,12 @@ export function detectChangedScope(changedPaths) { runSkillsPython = true; } + if (CI_WORKFLOW_SCOPE_RE.test(path)) { + runMacos = true; + runAndroid = true; + runSkillsPython = true; + } + if (!MACOS_PROTOCOL_GEN_RE.test(path) && MACOS_NATIVE_RE.test(path)) { runMacos = true; } diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index 358dbfc472c..682cfb8d9b3 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -124,6 +124,16 @@ describe("detectChangedScope", () => { }); }); + it("runs platform lanes when the CI workflow changes", () => { + expect(detectChangedScope([".github/workflows/ci.yml"])).toEqual({ + runNode: true, + runMacos: true, + runAndroid: true, + runWindows: true, + runSkillsPython: true, + }); + }); + it("treats base and head as literal git args", () => { const markerPath = path.join( os.tmpdir(),