diff --git a/docs/ci.md b/docs/ci.md index 84fbb21e4b3..10c4d0869d4 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -47,7 +47,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 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. A separate `docker-e2e-fast` job runs the bounded bundled-plugin Docker profile under a 120-second command timeout: setup-entry dependency repair plus synthetic bundled-loader failure isolation. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes. +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 runs for install, packaging, container-relevant changes, bundled extension production changes, and the core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Test-only and docs-only edits do not reserve Docker workers. 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. A separate `docker-e2e-fast` job runs the bounded bundled-plugin Docker profile under a 120-second command timeout: setup-entry dependency repair plus synthetic bundled-loader failure isolation. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes. 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 c7ef627c973..2ca94198a03 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -44,7 +44,9 @@ const CONTROL_UI_I18N_SCOPE_RE = const NATIVE_ONLY_RE = /^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|Swabble\/|appcast\.xml$)/; const CHANGED_SMOKE_SCOPE_RE = - /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/install\.sh$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/Dockerfile$|scripts\/e2e\/(?:Dockerfile\.qr-import|bundled-channel-runtime-deps-docker|qr-import-docker|gateway-network-docker)\.sh$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/package\.json$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; + /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/install\.sh$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|.*\.sh)$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/package\.json$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; +const CHANGED_SMOKE_RUNTIME_SCOPE_RE = + /^(src\/(?:channels|gateway|plugin-sdk|plugins)\/|extensions\/)/; /** * @param {string[]} changedPaths @@ -112,7 +114,10 @@ export function detectChangedScope(changedPaths) { runWindows = true; } - if (CHANGED_SMOKE_SCOPE_RE.test(path)) { + if ( + CHANGED_SMOKE_SCOPE_RE.test(path) || + (CHANGED_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path)) + ) { runChangedSmoke = true; } diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index 83942291938..b7613b362cc 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -74,7 +74,7 @@ describe("detectChangedScope", () => { }); it("enables node lane for node-relevant files", () => { - expect(detectChangedScope(["src/plugins/runtime/index.ts"])).toEqual({ + expect(detectChangedScope(["src/config/defaults.ts"])).toEqual({ runNode: true, runMacos: false, runAndroid: false, @@ -313,6 +313,15 @@ describe("detectChangedScope", () => { runChangedSmoke: true, runControlUiI18n: false, }); + expect(detectChangedScope(["scripts/e2e/plugin-update-unchanged-docker.sh"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); expect(detectChangedScope(["scripts/postinstall-bundled-plugins.mjs"])).toEqual({ runNode: true, runMacos: false, @@ -333,6 +342,75 @@ describe("detectChangedScope", () => { }); }); + it("runs changed-smoke for Docker-covered core and extension runtime surfaces", () => { + expect(detectChangedScope(["src/plugins/loader.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); + expect(detectChangedScope(["src/plugin-sdk/provider-entry.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); + expect(detectChangedScope(["src/gateway/protocol/messages.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); + expect(detectChangedScope(["src/channels/plugins/catalog.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); + expect(detectChangedScope([bundledPluginFile("matrix", "index.ts")])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); + }); + + it("keeps changed-smoke off for runtime-surface tests", () => { + expect(detectChangedScope(["src/plugins/loader.test.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); + expect(detectChangedScope([bundledPluginFile("matrix", "index.test.ts")])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); + }); + it("runs control-ui locale check only for control-ui i18n surfaces", () => { expect(detectChangedScope(["ui/src/i18n/locales/en.ts"])).toEqual({ runNode: true,