Files
tutti/tools/scripts/run-validation-lanes.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

299 lines
7.9 KiB
JavaScript

import { spawn } from "node:child_process";
import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
writeFileSync
} from "node:fs";
import { join, relative } from "node:path";
import { stripVTControlCharacters } from "node:util";
import { createGitDiscoveryEnvironment } from "./git-environment.mjs";
export async function runValidationLanes({
lanes,
maxParallel,
summaryLabel,
tailLines,
tmpDirectoryName,
workspaceRoot
}) {
if (lanes.length === 0) {
console.log(`${summaryLabel} found no lanes to validate`);
return { exitCode: 0, results: [] };
}
const tmpRoot = join(workspaceRoot, ".tmp", tmpDirectoryName);
const runId = new Date().toISOString().replace(/[:.]/gu, "-");
const runDirectory = join(tmpRoot, runId);
mkdirSync(runDirectory, { recursive: true });
const startedAt = Date.now();
const results = await runLanes({
lanes,
maxParallel,
runDirectory,
tailLines,
workspaceRoot
});
const durationMs = Date.now() - startedAt;
const failures = results.filter((result) => result.exitCode !== 0);
const summary = {
durationMs,
laneCount: lanes.length,
runDirectory,
startedAt: new Date(startedAt).toISOString(),
tailLines,
results
};
writeFileSync(
join(runDirectory, "summary.json"),
`${JSON.stringify(summary, null, 2)}\n`
);
mkdirSync(tmpRoot, { recursive: true });
writeFileSync(
join(tmpRoot, "latest.json"),
`${JSON.stringify(summary, null, 2)}\n`
);
if (failures.length === 0) {
console.log(
`${summaryLabel} passed ${lanes.length} lane(s) in ${formatDuration(durationMs)}`
);
printLaneTimingSummary(results, workspaceRoot, tmpRoot);
return { exitCode: 0, results };
}
console.error(
`${summaryLabel} failed ${failures.length}/${lanes.length} lane(s) in ${formatDuration(durationMs)}`
);
printLaneTimingSummary(results, workspaceRoot, tmpRoot, console.error);
console.error(
`failed lanes: ${failures.map((failure) => failure.label).join(", ")}`
);
console.error(`\nfull logs: ${relative(workspaceRoot, runDirectory)}`);
return { exitCode: 1, results };
}
async function runLanes({
lanes,
maxParallel,
runDirectory,
tailLines,
workspaceRoot
}) {
const results = [];
let nextIndex = 0;
const workerCount = Math.max(1, Math.min(maxParallel, lanes.length));
await Promise.all(
Array.from({ length: workerCount }, async () => {
while (nextIndex < lanes.length) {
const index = nextIndex++;
results.push(
await runLane({
index,
lane: lanes[index],
runDirectory,
tailLines,
workspaceRoot
})
);
}
})
);
return results.sort((left, right) => left.index - right.index);
}
function runLane({ index, lane, runDirectory, tailLines, workspaceRoot }) {
const logPath = join(runDirectory, `${sanitizeFileName(lane.key)}.log`);
const logStream = createWriteStream(logPath, { flags: "w" });
const startedAt = Date.now();
return new Promise((resolve) => {
let settled = false;
const child = spawn(lane.command[0], lane.command.slice(1), {
cwd: lane.cwd ?? workspaceRoot,
env: createGitDiscoveryEnvironment(),
stdio: ["ignore", "pipe", "pipe"]
});
logStream.write(`$ ${formatCommand(lane.command)}\n\n`);
child.stdout.on("data", (chunk) => logStream.write(chunk));
child.stderr.on("data", (chunk) => logStream.write(chunk));
child.on("error", (error) => {
if (settled) {
return;
}
settled = true;
logStream.write(`\n[runner] ${error.message}\n`);
finish(1);
});
child.on("close", (code) => {
if (settled) {
return;
}
settled = true;
finish(typeof code === "number" ? code : 1);
});
function finish(exitCode) {
logStream.end(() => {
const result = {
durationMs: Date.now() - startedAt,
exitCode,
index,
key: lane.key,
label: lane.label,
logPath,
logPathRelative: relative(workspaceRoot, logPath)
};
if (exitCode !== 0) {
printFailure(result, tailLines);
}
resolve(result);
});
}
});
}
export function readPositiveIntegerOption(name, defaultValue) {
const index = process.argv.indexOf(name);
if (index === -1) {
return defaultValue;
}
const parsed = Number.parseInt(process.argv[index + 1] ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
}
function formatCommand(command) {
return command.map(shellQuote).join(" ");
}
function shellQuote(value) {
if (/^[A-Za-z0-9_./:=@+-]+$/u.test(value)) {
return value;
}
return `'${value.replaceAll("'", "'\\''")}'`;
}
function sanitizeFileName(value) {
return value.replace(/[^A-Za-z0-9_.-]+/gu, "-");
}
function formatDuration(durationMs) {
return `${(durationMs / 1000).toFixed(1)}s`;
}
export function formatSlowestLanes(results, limit = 3) {
return [...results]
.filter((result) => Number.isFinite(result.durationMs))
.sort(
(left, right) =>
right.durationMs - left.durationMs ||
left.label.localeCompare(right.label)
)
.slice(0, Math.max(0, limit))
.map((result) => `${result.label} ${formatDuration(result.durationMs)}`)
.join(", ");
}
function printLaneTimingSummary(
results,
workspaceRoot,
tmpRoot,
write = console.log
) {
const slowest = formatSlowestLanes(results);
if (slowest === "") {
return;
}
const latestPath = relative(workspaceRoot, join(tmpRoot, "latest.json"));
write(`slowest lanes: ${slowest} (details: ${latestPath})`);
}
function printFailure(failure, lineCount) {
const excerpt = failureExcerptFromFile(failure.logPath, lineCount);
const header = excerpt.truncated
? `${failure.label} failure excerpt last ${lineCount} lines (full log: ${failure.logPathRelative})`
: `${failure.label} failure output`;
console.error(`\n--- ${header} ---`);
console.error(excerpt.text);
}
function failureExcerptFromFile(path, lineCount) {
if (!existsSync(path)) {
return { text: "", truncated: false };
}
return formatFailureExcerpt(readFileSync(path, "utf8"), lineCount);
}
export function formatFailureExcerpt(content, lineCount) {
const lines = splitLines(content).map((line) =>
stripVTControlCharacters(line)
);
const failureMarkerIndex = lines.findIndex((line) =>
line.includes("✖ failing tests:")
);
const relevantLines =
failureMarkerIndex === -1
? lines[0]?.startsWith("$ ") && lines[1] === ""
? lines.slice(2)
: lines
: lines.slice(failureMarkerIndex);
const filteredLines = collapseRepeatedLines(
relevantLines.filter((line) => !isFailureBoilerplate(line))
);
const limit = Math.max(
0,
Number.isFinite(lineCount) ? Math.floor(lineCount) : 0
);
return {
text: limit === 0 ? "" : filteredLines.slice(-limit).join("\n"),
truncated: filteredLines.length > limit
};
}
function isFailureBoilerplate(line) {
return (
line.includes("ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL") ||
line.startsWith("Exit status ") ||
/^\s*ELIFECYCLE\s+Command failed/u.test(line) ||
/^>\s+\S+@\S+\s+\S+\s+(?:\/|[A-Za-z]:\\)/u.test(line) ||
/^>\s+(?:go|jest|node|npm|pnpm|tsx|vitest|yarn)\b/u.test(line)
);
}
function collapseRepeatedLines(lines) {
const collapsed = [];
for (let index = 0; index < lines.length; ) {
const line = lines[index];
let nextIndex = index + 1;
while (nextIndex < lines.length && lines[nextIndex] === line) {
nextIndex += 1;
}
const count = nextIndex - index;
collapsed.push(
count > 1 && line !== "" ? `${line} (repeated ${count} times)` : line
);
index = nextIndex;
}
return collapsed;
}
function splitLines(content) {
if (content.length === 0) {
return [];
}
return content.endsWith("\n")
? content.slice(0, -1).split("\n")
: content.split("\n");
}