mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
275 lines
7.0 KiB
JavaScript
275 lines
7.0 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { performance } from "node:perf_hooks";
|
|
import {
|
|
detectChangedLanes,
|
|
listChangedPathsFromGit,
|
|
listStagedChangedPaths,
|
|
normalizeChangedPath,
|
|
} from "./changed-lanes.mjs";
|
|
|
|
const ROUTABLE_TEST_PATH_RE = /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u;
|
|
|
|
export function createChangedCheckPlan(result) {
|
|
const commands = [];
|
|
const add = (name, args) => {
|
|
if (!commands.some((command) => command.name === name && sameArgs(command.args, args))) {
|
|
commands.push({ name, args });
|
|
}
|
|
};
|
|
|
|
add("conflict markers", ["check:no-conflict-markers"]);
|
|
|
|
if (result.docsOnly) {
|
|
return {
|
|
commands,
|
|
testTargets: [],
|
|
runFullTests: false,
|
|
runExtensionTests: false,
|
|
summary: "docs-only",
|
|
};
|
|
}
|
|
|
|
const lanes = result.lanes;
|
|
const runAll = lanes.all;
|
|
|
|
if (runAll) {
|
|
add("typecheck all", ["tsgo:all"]);
|
|
add("lint", ["lint"]);
|
|
add("runtime import cycles", ["check:import-cycles"]);
|
|
return {
|
|
commands,
|
|
testTargets: [],
|
|
runFullTests: true,
|
|
runExtensionTests: false,
|
|
summary: "all",
|
|
};
|
|
}
|
|
|
|
if (lanes.core) {
|
|
add("typecheck core", ["tsgo:core"]);
|
|
}
|
|
if (lanes.coreTests) {
|
|
add("typecheck core tests", ["tsgo:core:test"]);
|
|
}
|
|
if (lanes.extensions) {
|
|
add("typecheck extensions", ["tsgo:extensions"]);
|
|
}
|
|
if (lanes.extensionTests) {
|
|
add("typecheck extension tests", ["tsgo:extensions:test"]);
|
|
}
|
|
|
|
if (lanes.core || lanes.coreTests) {
|
|
add("lint core", ["lint:core"]);
|
|
}
|
|
if (lanes.extensions || lanes.extensionTests) {
|
|
add("lint extensions", ["lint:extensions"]);
|
|
}
|
|
if (lanes.tooling) {
|
|
add("lint scripts", ["lint:scripts"]);
|
|
}
|
|
if (lanes.apps) {
|
|
add("lint apps", ["lint:apps"]);
|
|
}
|
|
|
|
if (lanes.core || lanes.extensions) {
|
|
add("runtime import cycles", ["check:import-cycles"]);
|
|
}
|
|
if (lanes.core) {
|
|
add("webhook body guard", ["lint:webhook:no-low-level-body-read"]);
|
|
add("pairing store guard", ["lint:auth:no-pairing-store-group"]);
|
|
add("pairing account guard", ["lint:auth:pairing-account-scope"]);
|
|
}
|
|
|
|
const testTargets = result.paths.filter((changedPath) => ROUTABLE_TEST_PATH_RE.test(changedPath));
|
|
return {
|
|
commands,
|
|
testTargets,
|
|
runFullTests: false,
|
|
runExtensionTests: result.extensionImpactFromCore,
|
|
summary: Object.entries(lanes)
|
|
.filter(([, enabled]) => enabled)
|
|
.map(([lane]) => lane)
|
|
.join(", "),
|
|
};
|
|
}
|
|
|
|
export async function runChangedCheck(result, options = {}) {
|
|
const plan = createChangedCheckPlan(result);
|
|
printPlan(result, plan, options);
|
|
|
|
if (options.dryRun) {
|
|
return 0;
|
|
}
|
|
|
|
const timings = [];
|
|
for (const command of plan.commands) {
|
|
const status = await runPnpm(command, timings);
|
|
if (status !== 0) {
|
|
printSummary(timings, options);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (plan.runFullTests) {
|
|
const status = await runPnpm({ name: "tests all", args: ["test"] }, timings);
|
|
if (status !== 0) {
|
|
printSummary(timings, options);
|
|
return status;
|
|
}
|
|
} else if (plan.testTargets.length > 0) {
|
|
const status = await runNode(
|
|
{
|
|
name: "tests changed",
|
|
args: ["scripts/test-projects.mjs", ...plan.testTargets],
|
|
},
|
|
timings,
|
|
);
|
|
if (status !== 0) {
|
|
printSummary(timings, options);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (plan.runExtensionTests) {
|
|
const status = await runPnpm({ name: "tests extensions", args: ["test:extensions"] }, timings);
|
|
if (status !== 0) {
|
|
printSummary(timings, options);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
printSummary(timings, options);
|
|
return 0;
|
|
}
|
|
|
|
function sameArgs(left, right) {
|
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
}
|
|
|
|
function printPlan(result, plan, options) {
|
|
const prefix = options.dryRun ? "[check:changed:dry-run]" : "[check:changed]";
|
|
console.error(`${prefix} lanes=${plan.summary || "none"}`);
|
|
if (result.extensionImpactFromCore) {
|
|
console.error(`${prefix} core contract changed; extension tests included`);
|
|
}
|
|
for (const reason of result.reasons) {
|
|
console.error(`${prefix} ${reason}`);
|
|
}
|
|
if (plan.testTargets.length > 0) {
|
|
console.error(`${prefix} test targets=${plan.testTargets.length}`);
|
|
}
|
|
}
|
|
|
|
async function runPnpm(command, timings) {
|
|
return await runCommand({ ...command, bin: "pnpm" }, timings);
|
|
}
|
|
|
|
async function runNode(command, timings) {
|
|
return await runCommand({ ...command, bin: process.execPath }, timings);
|
|
}
|
|
|
|
async function runCommand(command, timings) {
|
|
const startedAt = performance.now();
|
|
console.error(`\n[check:changed] ${command.name}`);
|
|
const child = spawn(command.bin, command.args, {
|
|
stdio: "inherit",
|
|
shell: process.platform === "win32",
|
|
});
|
|
|
|
return await new Promise((resolve) => {
|
|
child.once("error", (error) => {
|
|
console.error(error);
|
|
timings.push({
|
|
name: command.name,
|
|
durationMs: performance.now() - startedAt,
|
|
status: 1,
|
|
});
|
|
resolve(1);
|
|
});
|
|
child.once("close", (status) => {
|
|
const resolvedStatus = status ?? 1;
|
|
timings.push({
|
|
name: command.name,
|
|
durationMs: performance.now() - startedAt,
|
|
status: resolvedStatus,
|
|
});
|
|
resolve(resolvedStatus);
|
|
});
|
|
});
|
|
}
|
|
|
|
function printSummary(timings, options) {
|
|
if (!options.timed && timings.every((timing) => timing.status === 0)) {
|
|
return;
|
|
}
|
|
console.error("\n[check:changed] summary");
|
|
for (const timing of timings) {
|
|
const status = timing.status === 0 ? "ok" : `failed:${timing.status}`;
|
|
console.error(
|
|
`${formatMs(timing.durationMs).padStart(8)} ${status.padEnd(9)} ${timing.name}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function formatMs(durationMs) {
|
|
if (durationMs < 1000) {
|
|
return `${Math.round(durationMs)}ms`;
|
|
}
|
|
return `${(durationMs / 1000).toFixed(2)}s`;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
base: "origin/main",
|
|
head: "HEAD",
|
|
staged: false,
|
|
dryRun: false,
|
|
timed: false,
|
|
paths: [],
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--base") {
|
|
args.base = argv[index + 1] ?? args.base;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--head") {
|
|
args.head = argv[index + 1] ?? args.head;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--staged") {
|
|
args.staged = true;
|
|
continue;
|
|
}
|
|
if (arg === "--dry-run") {
|
|
args.dryRun = true;
|
|
continue;
|
|
}
|
|
if (arg === "--timed") {
|
|
args.timed = true;
|
|
continue;
|
|
}
|
|
args.paths.push(normalizeChangedPath(arg));
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function isDirectRun() {
|
|
const direct = process.argv[1];
|
|
return Boolean(direct && import.meta.url.endsWith(direct));
|
|
}
|
|
|
|
if (isDirectRun()) {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const paths =
|
|
args.paths.length > 0
|
|
? args.paths
|
|
: args.staged
|
|
? listStagedChangedPaths()
|
|
: listChangedPathsFromGit({ base: args.base, head: args.head });
|
|
const result = detectChangedLanes(paths);
|
|
process.exitCode = await runChangedCheck(result, args);
|
|
}
|