From 7ff8f8cef8396905e264bce4f2aba82c26cb9de1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 20:13:18 +0100 Subject: [PATCH] ci: narrow windows check scope --- docs/ci.md | 2 +- scripts/ci-changed-scope.mjs | 9 +++-- src/scripts/ci-changed-scope.test.ts | 51 +++++++++++++++++++++------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/docs/ci.md b/docs/ci.md index dc221a61c07..f86caae3d14 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -46,7 +46,7 @@ Jobs are ordered so cheap checks fail before expensive ones run: Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. -Windows Node checks are scoped to runtime, package, config, and workflow surfaces; test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards. +Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards. The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It computes `run_install_smoke` from the narrower changed-smoke signal, so Docker/install smoke only runs for install, packaging, and container-relevant changes. Its QR package smoke forces the Docker `pnpm install` layer to rerun while preserving the BuildKit pnpm store cache, so it still exercises installation without redownloading dependencies on every run. Its gateway-network e2e reuses the runtime image built earlier in the job, so it adds real container-to-container WebSocket coverage without adding another Docker build. Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod typecheck plus core tests, core test-only changes run only core test typecheck/tests, extension production changes run extension prod typecheck plus extension tests, and extension test-only changes run only extension test typecheck/tests. Public Plugin SDK or plugin-contract changes expand to extension validation because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all lanes. diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 670b06b6c61..aa03ec836ad 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -14,7 +14,9 @@ const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/; const NODE_SCOPE_RE = /^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|\.github\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.oxlintrc\.json$|\.oxfmtrc\.jsonc$)/; const WINDOWS_SCOPE_RE = - /^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/; + /^(src\/process\/|src\/infra\/windows-install-roots\.ts$|scripts\/(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|js)$|test\/scripts\/(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/ci\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/; +const WINDOWS_TEST_SCOPE_RE = + /^(src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/windows-install-roots\.test\.ts$|test\/scripts\/(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/; const TEST_ONLY_PATH_RE = /(^test\/|\/test\/|\/tests\/|(?:^|\/)[^/]+\.(?:test|spec|test-utils|test-support|test-harness|e2e-harness)\.[cm]?[jt]sx?$)/; const CONTROL_UI_I18N_SCOPE_RE = @@ -83,7 +85,10 @@ export function detectChangedScope(changedPaths) { runNode = true; } - if (WINDOWS_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path)) { + if ( + WINDOWS_SCOPE_RE.test(path) && + (!TEST_ONLY_PATH_RE.test(path) || WINDOWS_TEST_SCOPE_RE.test(path)) + ) { runWindows = true; } diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index 17e99feeb64..9c9c35cffea 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -59,7 +59,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: false, runControlUiI18n: false, @@ -175,14 +175,14 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: false, + runWindows: true, runSkillsPython: false, runChangedSmoke: false, runControlUiI18n: false, }); }); - it("does not run Windows for test-only changes", () => { + it("runs Windows only for Windows-relevant changes", () => { expect(detectChangedScope(["extensions/memory-lancedb/index.test.ts"])).toEqual({ runNode: true, runMacos: false, @@ -192,7 +192,7 @@ describe("detectChangedScope", () => { runChangedSmoke: false, runControlUiI18n: false, }); - expect(detectChangedScope(["test/helpers/windows-paths.test-utils.ts"])).toEqual({ + expect(detectChangedScope(["src/auto-reply/reply/streaming-directives.ts"])).toEqual({ runNode: true, runMacos: false, runAndroid: false, @@ -201,6 +201,33 @@ describe("detectChangedScope", () => { runChangedSmoke: false, runControlUiI18n: false, }); + expect(detectChangedScope(["src/process/exec.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: true, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); + expect(detectChangedScope(["src/process/exec.windows.test.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: true, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); + expect(detectChangedScope(["scripts/npm-runner.mjs"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: true, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); }); it("runs changed-smoke for install and packaging surfaces", () => { @@ -208,7 +235,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: true, runControlUiI18n: false, @@ -217,7 +244,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: true, runControlUiI18n: false, @@ -235,7 +262,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: true, runControlUiI18n: false, @@ -244,7 +271,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: true, runControlUiI18n: false, @@ -253,7 +280,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: true, runControlUiI18n: false, @@ -262,7 +289,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: true, runControlUiI18n: false, @@ -274,7 +301,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: false, runControlUiI18n: true, @@ -284,7 +311,7 @@ describe("detectChangedScope", () => { runNode: true, runMacos: false, runAndroid: false, - runWindows: true, + runWindows: false, runSkillsPython: false, runChangedSmoke: false, runControlUiI18n: true,