mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 01:21:40 +00:00
perf(ci): gate QA smoke by smoke-visible changes and isolate the prompt snapshot lane (#108202)
* perf(ci): diff-gate QA smoke by CLI import graph and isolate prompt snapshots lane * fix(ci): run prompt snapshot lane unconditionally * fix(ci): treat QA lane orchestration files as QA-impacting
This commit is contained in:
committed by
GitHub
parent
580938097f
commit
9c8a006d0c
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"]],
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user