diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ad49f5704c6..f2937b95b2df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -540,16 +540,18 @@ jobs: console.warn(`Changed Node test planning failed; using compact full suite: ${error}`); } } - // Targeted (narrow-diff) PRs skip the heavy packaging lanes unless the - // diff touches surfaces those lanes exist to prove: test-only diffs - // cannot change dist bytes, and QA smoke re-proves packaging/scenario - // surfaces that unit shards plus build smokes already cover elsewhere. + // Heavy packaging lanes run only when the diff touches surfaces they + // exist to prove: test-only diffs cannot change dist bytes, and QA + // smoke only sees changes on its scenario surface or inside the + // packaged CLI's import graph. QA gating is diff-based, so it also + // applies when test targeting fell back to the full compact suite. const changedScopeHasBuildImpact = changedNodeTestShards === null || typeof changedNodeTestPlan.hasBuildArtifactAffectingChange !== "function" || changedNodeTestPlan.hasBuildArtifactAffectingChange(changedPaths); const changedScopeHasQaImpact = - changedNodeTestShards === null || + changedPaths === null || + eventName !== "pull_request" || typeof changedNodeTestPlan.hasQaSmokeAffectingChange !== "function" || changedNodeTestPlan.hasQaSmokeAffectingChange(changedPaths); const runBuildArtifacts = runNodeFull && changedScopeHasBuildImpact; @@ -1851,13 +1853,16 @@ jobs: - check_name: check-additional-boundaries-a group: boundaries boundary_shard: 1/4 - # This shard runs ~4min on 8 vCPU and regularly owns the PR wall - # clock; extra cores shorten it for similar billed core-minutes. - runner: blacksmith-16vcpu-ubuntu-2404 + runner: blacksmith-8vcpu-ubuntu-2404 - check_name: check-additional-boundaries-bcd group: boundaries boundary_shard: 2/4,3/4,4/4 runner: blacksmith-8vcpu-ubuntu-2404 + # Prompt snapshot regeneration runs real embedded-agent turns (~2min) + # and used to own the boundaries-a wall clock; keep it in its own lane. + - check_name: check-prompt-snapshots + group: prompt-snapshots + runner: blacksmith-8vcpu-ubuntu-2404 - check_name: check-session-accessor-boundary group: session-accessor-boundary runner: blacksmith-4vcpu-ubuntu-2404 @@ -1970,6 +1975,12 @@ jobs: boundaries) node scripts/run-additional-boundary-checks.mjs ;; + prompt-snapshots) + # No presence fallback: the boundary runner previously invoked + # this unconditionally, and silent success would drop snapshot + # drift coverage. + run_check "prompt:snapshots:check" pnpm prompt:snapshots:check + ;; session-accessor-boundary) if [ ! -f scripts/check-session-accessor-boundary.mjs ]; then echo "[skip] session accessor boundary check is not present in this checkout" diff --git a/scripts/lib/ci-changed-node-test-plan.mjs b/scripts/lib/ci-changed-node-test-plan.mjs index 6172d3d6b5e9..26c330a6c885 100644 --- a/scripts/lib/ci-changed-node-test-plan.mjs +++ b/scripts/lib/ci-changed-node-test-plan.mjs @@ -44,19 +44,41 @@ export function hasBuildArtifactAffectingChange(changedPaths) { return changedPaths.some((changedPath) => !isTestOnlyPath(changedPath)); } -const QA_SMOKE_CRITICAL_RE = - /^(?:extensions\/qa-lab|qa)\/|^scripts\/(?:build-all\.mjs|package-openclaw-for-docker\.mjs)$|^(?:package\.json|pnpm-lock\.yaml|npm-shrinkwrap\.json)$|^ui\//u; +// Surfaces the CI smoke scenarios exercise outside the core runtime import +// graph: the qa-lab harness and scenario data, the packaged-CLI build inputs, +// the control UI (playwright scenario), the two channels the smoke profile +// drives (matrix, telegram), and workspace packages whose package-specifier +// imports the relative import graph cannot see. The QA lane's own +// orchestration (this planner, the CI workflow, composite actions) is also +// QA-impacting: changes to the gate must not be able to skip the gated lane. +const QA_SMOKE_SURFACE_RE = + /^(?:extensions\/(?:matrix|qa-lab|telegram)|packages|qa|ui)\/|^scripts\/(?:build-all\.mjs|package-openclaw-for-docker\.mjs)$|^scripts\/lib\/ci-changed-node-test-plan\.mjs$|^\.github\/(?:workflows\/ci\.yml$|actions\/)|^(?:openclaw\.mjs|package\.json|pnpm-lock\.yaml|npm-shrinkwrap\.json|pnpm-workspace\.yaml|tsdown\.config\.ts)$/u; +// The smoke profile runs the packaged CLI end to end, so its runtime blast +// radius is exactly the CLI entry's import graph (dynamic imports included). +const QA_SMOKE_RUNTIME_ENTRY = "src/index.ts"; /** - * True when a changed path touches the QA smoke packaging/scenario surface. - * Deliberate product tradeoff: targeted PRs outside this surface skip QA smoke - * and rely on import-selected shards plus build-artifact CLI smokes. Targeting - * only fires for narrow non-SDK-impacting diffs, and QA smoke still runs on - * every node-scoped main push, so a missed regression breaks main visibly - * instead of shipping silently. + * True when a changed path can influence the QA smoke scenarios: it touches + * the smoke surface directly, or the packaged CLI's import graph reaches it. + * Diffs outside both are invisible to the smoke profile, so the manifest may + * skip that lane regardless of whether test targeting fired. */ -export function hasQaSmokeAffectingChange(changedPaths) { - return changedPaths.some((changedPath) => QA_SMOKE_CRITICAL_RE.test(changedPath)); +export function hasQaSmokeAffectingChange(changedPaths, options = {}) { + const cwd = options.cwd ?? process.cwd(); + if (changedPaths.some((changedPath) => QA_SMOKE_SURFACE_RE.test(changedPath))) { + return true; + } + const sourcePaths = changedPaths.filter( + (changedPath) => changedPath.startsWith("src/") && !isTestFileTarget(changedPath), + ); + if (sourcePaths.length === 0) { + return false; + } + // Deleted sources cannot be graphed; fail safe to running the smoke lane. + if (sourcePaths.some((changedPath) => !existsSync(path.join(cwd, changedPath)))) { + return true; + } + return hasImportGraphImpactOnTargets(sourcePaths, [QA_SMOKE_RUNTIME_ENTRY], cwd); } function createBoundaryShard() { diff --git a/scripts/run-additional-boundary-checks.mjs b/scripts/run-additional-boundary-checks.mjs index bd903c253e4f..08093be1b45f 100644 --- a/scripts/run-additional-boundary-checks.mjs +++ b/scripts/run-additional-boundary-checks.mjs @@ -15,8 +15,10 @@ const POST_FORCE_KILL_WAIT_MS = 250; const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; /** Ordered list of supplemental boundary checks used by CI sharding. */ +// prompt:snapshots:check is intentionally absent: it regenerates snapshots by +// running real embedded-agent turns (~2min) and owns a dedicated CI lane +// (check-prompt-snapshots) so no boundary shard carries that wall clock. export const BOUNDARY_CHECKS = [ - ["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]], ["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]], ["lint:docker-e2e", "pnpm", ["run", "lint:docker-e2e"]], ["lint:tmp:no-random-messaging", "pnpm", ["run", "lint:tmp:no-random-messaging"]], diff --git a/test/scripts/ci-changed-node-test-plan.test.ts b/test/scripts/ci-changed-node-test-plan.test.ts index 14830f67e1f9..e3e6cb5e2c89 100644 --- a/test/scripts/ci-changed-node-test-plan.test.ts +++ b/test/scripts/ci-changed-node-test-plan.test.ts @@ -56,7 +56,20 @@ describe("CI changed Node test plan", () => { expect(hasBuildArtifactAffectingChange(["src/agents/foo.ts"])).toBe(true); expect(hasQaSmokeAffectingChange(["extensions/qa-lab/src/ci-smoke-plan.ts"])).toBe(true); expect(hasQaSmokeAffectingChange(["ui/src/app.ts"])).toBe(true); - expect(hasQaSmokeAffectingChange(["src/infra/retry.ts"])).toBe(false); + // Inside the packaged CLI's import graph -> the smoke scenarios can see it. + expect(hasQaSmokeAffectingChange(["src/infra/retry.ts"])).toBe(true); + // Smoke drives matrix + telegram; other channel plugins are invisible to it. + expect(hasQaSmokeAffectingChange(["extensions/telegram/src/index.ts"])).toBe(true); + expect(hasQaSmokeAffectingChange(["extensions/discord/src/index.ts"])).toBe(false); + expect(hasQaSmokeAffectingChange(["scripts/run-vitest.mjs"])).toBe(false); + expect(hasQaSmokeAffectingChange(["test/scripts/ci-node-test-plan.test.ts"])).toBe(false); + // The QA lane's own orchestration must not be able to skip the lane. + expect(hasQaSmokeAffectingChange([".github/workflows/ci.yml"])).toBe(true); + expect(hasQaSmokeAffectingChange([".github/actions/setup-node-env/action.yml"])).toBe(true); + expect(hasQaSmokeAffectingChange(["scripts/lib/ci-changed-node-test-plan.mjs"])).toBe(true); + expect(hasQaSmokeAffectingChange([".github/workflows/labeler.yml"])).toBe(false); + // Deleted source files cannot be graphed; fail safe to running QA smoke. + expect(hasQaSmokeAffectingChange(["src/infra/definitely-deleted-module.ts"])).toBe(true); }); it("fails safe to the full plan for broad changes", () => { diff --git a/test/scripts/run-additional-boundary-checks.test.ts b/test/scripts/run-additional-boundary-checks.test.ts index ef1621650842..133ab152d90e 100644 --- a/test/scripts/run-additional-boundary-checks.test.ts +++ b/test/scripts/run-additional-boundary-checks.test.ts @@ -109,12 +109,14 @@ async function waitForChildClose( } describe("run-additional-boundary-checks", () => { - it("runs prompt snapshot drift checks in CI", () => { - expect(BOUNDARY_CHECKS[0]).toEqual({ - label: "prompt:snapshots:check", - command: "pnpm", - args: ["prompt:snapshots:check"], - }); + it("keeps prompt snapshot drift checks in their dedicated CI lane", () => { + // The snapshot check regenerates prompts with real embedded-agent turns + // (~2min); packing it into a boundary shard makes that shard the PR wall + // clock, so it owns the check-prompt-snapshots lane instead. + expect(BOUNDARY_CHECKS.some((check) => check.label === "prompt:snapshots:check")).toBe(false); + const workflow = fs.readFileSync(".github/workflows/ci.yml", "utf8"); + expect(workflow).toContain("check_name: check-prompt-snapshots"); + expect(workflow).toContain('run_check "prompt:snapshots:check" pnpm prompt:snapshots:check'); }); it("normalizes concurrency input", () => {