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
808 lines
22 KiB
JavaScript
808 lines
22 KiB
JavaScript
import { spawn, spawnSync } from "node:child_process";
|
|
import {
|
|
createWriteStream,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
writeFileSync
|
|
} from "node:fs";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
buildLaneInputFingerprint,
|
|
laneFingerprintVersion,
|
|
LaneCacheError,
|
|
mergeLaneResults,
|
|
resolveRetryPushReady,
|
|
selectFailedOnlyLanes
|
|
} from "./run-check-changed-cache.mjs";
|
|
import {
|
|
buildGoLintLane,
|
|
buildGoTestLane,
|
|
discoverGoModuleRoots,
|
|
buildPackageTestCommand,
|
|
isBuiltinGenerateRequired,
|
|
isGlobalGoValidationRelevant,
|
|
isGoValidationRelevant,
|
|
resolveGoModuleRoot,
|
|
resolveGoValidationTargets,
|
|
selectGoLintModuleRoots
|
|
} from "./run-check-changed-targets.mjs";
|
|
import {
|
|
classifyChangedFiles,
|
|
createPackageManifestPackRelevance,
|
|
createRootManifestTestRelevance,
|
|
isPackagePackRelevantPath
|
|
} from "./change-classification.mjs";
|
|
import {
|
|
selectRepositoryCheckInputs,
|
|
selectRepositoryChecks
|
|
} from "./repository-checks.mjs";
|
|
import { formatFailureExcerpt } from "./run-validation-lanes.mjs";
|
|
import { resolveGolangciLintBinary } from "./golangci-lint-tool.mjs";
|
|
|
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
const workspaceRoot = join(scriptDirectory, "..", "..");
|
|
const golangciLintBinary = resolveGolangciLintBinary({ cwd: workspaceRoot });
|
|
const pnpmCommand = resolvePnpmCommand();
|
|
const pnpmShellCommand = formatCommand(pnpmCommand);
|
|
let maxParallel = 4;
|
|
let tailLines = 80;
|
|
let dryRun = false;
|
|
let failedOnly = false;
|
|
let pushReady = false;
|
|
let verbose = false;
|
|
let baseRef = resolveDefaultBaseRef();
|
|
const tmpRoot = join(workspaceRoot, ".tmp", "check-runs");
|
|
const latestSummaryPath = join(tmpRoot, "latest.json");
|
|
|
|
const packageInfos = loadPackageInfos();
|
|
|
|
export async function main() {
|
|
const previousSummary = failedOnly ? readLatestSummary() : null;
|
|
if (failedOnly && !previousSummary) {
|
|
console.log("check:changed found no previous run to reuse");
|
|
return;
|
|
}
|
|
pushReady = resolveRetryPushReady(pushReady, previousSummary);
|
|
|
|
const plannedLanes = buildChangedLanes();
|
|
|
|
if (plannedLanes.length === 0) {
|
|
console.log("check:changed found no changed files to validate");
|
|
return;
|
|
}
|
|
|
|
if (dryRun && !failedOnly) {
|
|
printPlan(plannedLanes);
|
|
return;
|
|
}
|
|
|
|
const currentLanes = plannedLanes.map((lane) => ({
|
|
...lane,
|
|
inputFingerprint: buildLaneInputFingerprint({
|
|
baseRef,
|
|
lane,
|
|
workspaceRoot
|
|
})
|
|
}));
|
|
|
|
const failedOnlySelection = previousSummary
|
|
? selectFailedOnlyLanes(currentLanes, previousSummary)
|
|
: null;
|
|
const lanesToRun = failedOnlySelection?.lanesToRun ?? currentLanes;
|
|
const reusedResults = failedOnlySelection?.reusedResults ?? [];
|
|
|
|
if (dryRun) {
|
|
printPlan(lanesToRun, reusedResults);
|
|
return;
|
|
}
|
|
|
|
if (lanesToRun.length === 0) {
|
|
console.log(
|
|
`check:changed found no failed or changed lanes; reused ${reusedResults.length} passed lane(s)`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const runId = new Date().toISOString().replace(/[:.]/g, "-");
|
|
const runDirectory = join(tmpRoot, runId);
|
|
mkdirSync(runDirectory, { recursive: true });
|
|
|
|
if (verbose) {
|
|
console.log(`check:changed running ${lanesToRun.length} lane(s)`);
|
|
if (reusedResults.length > 0) {
|
|
console.log(
|
|
`check:changed reusing ${reusedResults.length} passed lane(s)`
|
|
);
|
|
}
|
|
console.log(`logs: ${relative(workspaceRoot, runDirectory)}`);
|
|
}
|
|
|
|
const startedAt = Date.now();
|
|
const executedResults = await runLanes(lanesToRun, runDirectory);
|
|
const results = mergeLaneResults(
|
|
currentLanes,
|
|
executedResults,
|
|
reusedResults
|
|
);
|
|
const durationMs = Date.now() - startedAt;
|
|
const summary = {
|
|
baseRef,
|
|
durationMs,
|
|
executedLaneCount: executedResults.length,
|
|
failedOnly,
|
|
laneFingerprintVersion,
|
|
pushReady,
|
|
reusedLaneCount: reusedResults.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(latestSummaryPath, `${JSON.stringify(summary, null, 2)}\n`);
|
|
|
|
const failures = results.filter((result) => result.exitCode !== 0);
|
|
printSummary(results, failures, durationMs, runDirectory);
|
|
|
|
if (failures.length > 0) {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
function buildChangedLanes() {
|
|
const changedFiles = listChangedFiles(baseRef);
|
|
const isPackageManifestPackRelevant = createPackageManifestPackRelevance({
|
|
baseRef,
|
|
root: workspaceRoot
|
|
});
|
|
const classification = classifyChangedFiles(changedFiles, {
|
|
isPackageManifestPackRelevant,
|
|
isRootManifestTestRelevant: createRootManifestTestRelevance({
|
|
baseRef,
|
|
root: workspaceRoot
|
|
})
|
|
});
|
|
const lanesByKey = new Map();
|
|
const addLane = (lane) => {
|
|
const normalizedInputs = Array.from(new Set(lane.inputFiles)).sort();
|
|
const existing = lanesByKey.get(lane.key);
|
|
if (existing) {
|
|
existing.inputFiles = Array.from(
|
|
new Set([...existing.inputFiles, ...normalizedInputs])
|
|
).sort();
|
|
} else {
|
|
lanesByKey.set(lane.key, {
|
|
...lane,
|
|
inputFiles: normalizedInputs
|
|
});
|
|
}
|
|
};
|
|
|
|
if (changedFiles.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
addLane({
|
|
key: "diff-check",
|
|
label: "diff-check",
|
|
command: [
|
|
"bash",
|
|
"-lc",
|
|
`git diff --check ${shellQuote(baseRef)}...HEAD && git diff --check && git diff --cached --check`
|
|
],
|
|
inputFiles: changedFiles
|
|
});
|
|
|
|
const lintFiles = classification.runTs
|
|
? selectExistingLintFiles(changedFiles)
|
|
: [];
|
|
if (lintFiles.length > 0) {
|
|
addLane({
|
|
key: "lint:changed",
|
|
label: "lint:changed",
|
|
command: [
|
|
...pnpmCommand,
|
|
"exec",
|
|
"oxlint",
|
|
"--deny-warnings",
|
|
"--no-error-on-unmatched-pattern",
|
|
...lintFiles
|
|
],
|
|
inputFiles: lintFiles
|
|
});
|
|
}
|
|
|
|
for (const check of selectRepositoryChecks(changedFiles)) {
|
|
addLane({
|
|
key: check.key,
|
|
label: check.label,
|
|
command: [...pnpmCommand, "run", check.script],
|
|
inputFiles: selectRepositoryCheckInputs(check, changedFiles)
|
|
});
|
|
}
|
|
|
|
const goModuleRoots = classification.runGo
|
|
? discoverGoModuleRoots({ root: workspaceRoot })
|
|
: [];
|
|
const goValidationTargets = classification.runGo
|
|
? resolveGoValidationTargets(changedFiles, {
|
|
lintModuleRoots: selectGoLintModuleRoots(goModuleRoots),
|
|
moduleRoots: goModuleRoots
|
|
})
|
|
: null;
|
|
const forceBuiltinGenerate = isBuiltinGenerateRequired(changedFiles);
|
|
if (goValidationTargets) {
|
|
for (const [moduleRoot, targets] of goValidationTargets.lintByModule) {
|
|
const inputFiles = selectGoLaneInputs(
|
|
changedFiles,
|
|
moduleRoot,
|
|
forceBuiltinGenerate,
|
|
goModuleRoots
|
|
);
|
|
addLane({
|
|
...buildGoLintLane({
|
|
forceBuiltinGenerate,
|
|
golangciLintBinary,
|
|
moduleRoot,
|
|
pnpmCommand: pnpmShellCommand,
|
|
targets,
|
|
shellQuote,
|
|
workspaceRoot
|
|
}),
|
|
inputFiles
|
|
});
|
|
}
|
|
for (const [moduleRoot, targets] of goValidationTargets.testByModule) {
|
|
const inputFiles = selectGoLaneInputs(
|
|
changedFiles,
|
|
moduleRoot,
|
|
forceBuiltinGenerate,
|
|
goModuleRoots
|
|
);
|
|
addLane({
|
|
...buildGoTestLane({
|
|
forceBuiltinGenerate,
|
|
moduleRoot,
|
|
pnpmCommand: pnpmShellCommand,
|
|
shellQuote,
|
|
targets
|
|
}),
|
|
inputFiles
|
|
});
|
|
}
|
|
|
|
if (pushReady) {
|
|
addLane({
|
|
key: "build:go",
|
|
label: "build:go",
|
|
serialGroup: "tuttid-builtin-assets",
|
|
command: [...pnpmCommand, "run", "build:go"],
|
|
inputFiles: changedFiles.filter(isGoValidationRelevant)
|
|
});
|
|
}
|
|
}
|
|
|
|
const rootGlobalChange =
|
|
classification.runTs && changedFiles.some(isGlobalTypecheckRelevant);
|
|
if (rootGlobalChange) {
|
|
addLane({
|
|
key: "typecheck:all",
|
|
label: "typecheck:all",
|
|
command: [process.execPath, "./tools/scripts/run-typecheck.mjs"],
|
|
inputFiles: changedFiles.filter(isTypeScriptValidationInput)
|
|
});
|
|
}
|
|
|
|
for (const packageInfo of packageInfos) {
|
|
const packageFiles = changedFiles.filter((file) =>
|
|
file.startsWith(`${packageInfo.root}/`)
|
|
);
|
|
if (packageFiles.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const hasRelevantCode = packageFiles.some(isPackageValidationRelevant);
|
|
if (hasRelevantCode && packageInfo.scripts.typecheck && !rootGlobalChange) {
|
|
addLane({
|
|
key: `${packageInfo.name}:typecheck`,
|
|
label: `${packageInfo.name}:typecheck`,
|
|
command: [
|
|
process.execPath,
|
|
"./tools/scripts/run-tsgo-typecheck.mjs",
|
|
"--package-root",
|
|
packageInfo.root
|
|
],
|
|
inputFiles: packageFiles
|
|
});
|
|
}
|
|
|
|
if (hasRelevantCode && packageInfo.scripts.test) {
|
|
const command = buildPackageTestCommand({
|
|
baseRef,
|
|
packageFiles,
|
|
packageInfo,
|
|
pnpmCommand
|
|
});
|
|
if (command) {
|
|
addLane({
|
|
key: `${packageInfo.name}:test`,
|
|
label: `${packageInfo.name}:test`,
|
|
command,
|
|
inputFiles: packageFiles
|
|
});
|
|
}
|
|
}
|
|
|
|
if (
|
|
pushReady &&
|
|
!classification.packPackages.includes(packageInfo.name) &&
|
|
packageInfo.scripts.build &&
|
|
packageFiles.some((file) =>
|
|
isPackageBuildRelevant(file, isPackageManifestPackRelevant)
|
|
)
|
|
) {
|
|
addLane({
|
|
key: `${packageInfo.name}:build`,
|
|
label: `${packageInfo.name}:build`,
|
|
command: [...pnpmCommand, "--filter", packageInfo.name, "build"],
|
|
inputFiles: packageFiles
|
|
});
|
|
}
|
|
}
|
|
|
|
if (pushReady && classification.runPack) {
|
|
const packageArgs = classification.packAll
|
|
? []
|
|
: ["--", "--packages-json", JSON.stringify(classification.packPackages)];
|
|
addLane({
|
|
key: "pack:npm",
|
|
label: "npm package pack",
|
|
command: [...pnpmCommand, "run", "release:pack:check", ...packageArgs],
|
|
inputFiles: changedFiles
|
|
});
|
|
}
|
|
|
|
return Array.from(lanesByKey.values());
|
|
}
|
|
|
|
function readLatestSummary() {
|
|
if (!existsSync(latestSummaryPath)) {
|
|
return null;
|
|
}
|
|
return JSON.parse(readFileSync(latestSummaryPath, "utf8"));
|
|
}
|
|
|
|
export async function runLanes(inputLanes, runDirectory) {
|
|
const results = [];
|
|
const serialGroups = new Map();
|
|
let nextIndex = 0;
|
|
const workerCount = Math.max(1, Math.min(maxParallel, inputLanes.length));
|
|
|
|
await Promise.all(
|
|
Array.from({ length: workerCount }, async () => {
|
|
while (nextIndex < inputLanes.length) {
|
|
const laneIndex = nextIndex++;
|
|
const lane = inputLanes[laneIndex];
|
|
results.push(
|
|
await runLaneInSerialGroup(
|
|
lane,
|
|
laneIndex,
|
|
runDirectory,
|
|
serialGroups
|
|
)
|
|
);
|
|
}
|
|
})
|
|
);
|
|
|
|
return results.sort((left, right) => left.index - right.index);
|
|
}
|
|
|
|
function runLaneInSerialGroup(lane, index, runDirectory, serialGroups) {
|
|
if (!lane.serialGroup) {
|
|
return runLane(lane, index, runDirectory);
|
|
}
|
|
const previous = serialGroups.get(lane.serialGroup) ?? Promise.resolve();
|
|
const current = previous.then(() => runLane(lane, index, runDirectory));
|
|
serialGroups.set(
|
|
lane.serialGroup,
|
|
current.catch(() => undefined)
|
|
);
|
|
return current;
|
|
}
|
|
|
|
function runLane(lane, index, runDirectory) {
|
|
const logPath = join(runDirectory, `${sanitizeFileName(lane.key)}.log`);
|
|
const logStream = createWriteStream(logPath, { flags: "w" });
|
|
const startedAt = Date.now();
|
|
|
|
if (verbose) {
|
|
console.log(`[${lane.label}] started`);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const [command, ...args] = lane.command;
|
|
const useWindowsShellShim =
|
|
process.platform === "win32" && /\.(?:cmd|bat)$/iu.test(command);
|
|
const child = spawn(command, args, {
|
|
cwd: workspaceRoot,
|
|
env: process.env,
|
|
shell: useWindowsShellShim,
|
|
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) => {
|
|
logStream.write(`\n[runner] ${error.message}\n`);
|
|
logStream.end();
|
|
resolve(buildLaneResult(lane, index, logPath, startedAt, 1));
|
|
});
|
|
|
|
child.on("close", (code) => {
|
|
logStream.end();
|
|
const exitCode = typeof code === "number" ? code : 1;
|
|
const result = buildLaneResult(lane, index, logPath, startedAt, exitCode);
|
|
if (verbose) {
|
|
console.log(
|
|
`[${lane.label}] ${exitCode === 0 ? "passed" : "failed"} ${formatDuration(result.durationMs)}`
|
|
);
|
|
}
|
|
resolve(result);
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildLaneResult(lane, index, logPath, startedAt, exitCode) {
|
|
return {
|
|
command: lane.command,
|
|
durationMs: Date.now() - startedAt,
|
|
exitCode,
|
|
index,
|
|
inputFiles: lane.inputFiles,
|
|
inputFingerprint: lane.inputFingerprint,
|
|
key: lane.key,
|
|
label: lane.label,
|
|
logPath,
|
|
logPathRelative: relative(workspaceRoot, logPath),
|
|
reused: false
|
|
};
|
|
}
|
|
|
|
function printPlan(inputLanes, reusedResults = []) {
|
|
const reusedSuffix =
|
|
reusedResults.length > 0 ? `, ${reusedResults.length} reused` : "";
|
|
console.log(
|
|
`check:changed plan (${inputLanes.length} to run${reusedSuffix})`
|
|
);
|
|
for (const lane of inputLanes) {
|
|
console.log(`- ${lane.label}: ${formatCommand(lane.command)}`);
|
|
}
|
|
for (const result of reusedResults) {
|
|
console.log(`- ${result.label}: reuse passed result`);
|
|
}
|
|
}
|
|
|
|
export function printSummary(results, failures, durationMs, runDirectory) {
|
|
const reusedCount = results.filter((result) => result.reused).length;
|
|
const runCount = results.length - reusedCount;
|
|
const reuseSuffix =
|
|
reusedCount > 0 ? ` (${runCount} run, ${reusedCount} reused)` : "";
|
|
if (failures.length === 0) {
|
|
console.log(
|
|
`check:changed passed ${results.length} lane(s) in ${formatDuration(durationMs)}${reuseSuffix}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.error(
|
|
`check:changed failed ${failures.length}/${results.length} lane(s) in ${formatDuration(durationMs)}${reuseSuffix}`
|
|
);
|
|
for (const failure of failures) {
|
|
const output = failureExcerpt(failure.logPath, tailLines);
|
|
const header = output.truncated
|
|
? `${failure.label} ${output.label} (full log: ${failure.logPathRelative})`
|
|
: `${failure.label} ${output.label}`;
|
|
console.error(`\n--- ${header} ---`);
|
|
console.error(output.text);
|
|
}
|
|
console.error(
|
|
`\nfull logs: ${relative(workspaceRoot, runDirectory)}\nRerun failed lanes with: pnpm check:changed -- --failed-only`
|
|
);
|
|
}
|
|
|
|
function listChangedFiles(ref) {
|
|
const files = new Set();
|
|
for (const args of [
|
|
["diff", "--name-only", `${ref}...HEAD`],
|
|
["diff", "--name-only"],
|
|
["diff", "--cached", "--name-only"],
|
|
["ls-files", "--others", "--exclude-standard"]
|
|
]) {
|
|
for (const file of gitLines(args)) {
|
|
files.add(file);
|
|
}
|
|
}
|
|
return Array.from(files).sort();
|
|
}
|
|
|
|
function loadPackageInfos() {
|
|
return gitLines([
|
|
"ls-files",
|
|
"apps/*/package.json",
|
|
"packages/*/*/package.json",
|
|
"services/tuttid/builtin-apps/*/package.json",
|
|
"tools/fixtures/*/package.json"
|
|
])
|
|
.map((packageJsonPath) => {
|
|
const packageJson = JSON.parse(
|
|
readFileSync(join(workspaceRoot, packageJsonPath), "utf8")
|
|
);
|
|
return {
|
|
name: packageJson.name,
|
|
root: dirname(packageJsonPath).replaceAll("\\", "/"),
|
|
scripts: packageJson.scripts ?? {}
|
|
};
|
|
})
|
|
.filter((packageInfo) => packageInfo.name)
|
|
.sort((left, right) => right.root.length - left.root.length);
|
|
}
|
|
|
|
function gitLines(args) {
|
|
const result = spawnSync("git", args, {
|
|
cwd: workspaceRoot,
|
|
encoding: "utf8"
|
|
});
|
|
if (result.status !== 0) {
|
|
return [];
|
|
}
|
|
return result.stdout
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function resolveDefaultBaseRef() {
|
|
for (const candidate of ["origin/main", "main"]) {
|
|
const result = spawnSync("git", ["rev-parse", "--verify", candidate], {
|
|
cwd: workspaceRoot,
|
|
encoding: "utf8"
|
|
});
|
|
if (result.status === 0) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return "HEAD";
|
|
}
|
|
|
|
function resolvePnpmCommand() {
|
|
const fallback = [process.platform === "win32" ? "pnpm.cmd" : "pnpm"];
|
|
try {
|
|
const packageJson = JSON.parse(
|
|
readFileSync(join(workspaceRoot, "package.json"), "utf8")
|
|
);
|
|
const match = /^pnpm@(.+)$/u.exec(String(packageJson.packageManager ?? ""));
|
|
if (!match) {
|
|
return fallback;
|
|
}
|
|
return [
|
|
process.platform === "win32" ? "corepack.cmd" : "corepack",
|
|
`pnpm@${match[1]}`
|
|
];
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
export function parseCliArgs(inputArgs) {
|
|
const options = {
|
|
baseRef: null,
|
|
dryRun: false,
|
|
failedOnly: false,
|
|
maxParallel: 4,
|
|
pushReady: false,
|
|
tailLines: 80,
|
|
verbose: false
|
|
};
|
|
const args = inputArgs.filter((arg) => arg !== "--");
|
|
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
switch (arg) {
|
|
case "--dry-run":
|
|
options.dryRun = true;
|
|
break;
|
|
case "--failed-only":
|
|
options.failedOnly = true;
|
|
break;
|
|
case "--push-ready":
|
|
options.pushReady = true;
|
|
break;
|
|
case "--verbose":
|
|
options.verbose = true;
|
|
break;
|
|
case "--base":
|
|
options.baseRef = readCliValue(args, ++index, arg);
|
|
break;
|
|
case "--max-parallel":
|
|
options.maxParallel = readPositiveIntegerCliValue(args, ++index, arg);
|
|
break;
|
|
case "--tail-lines":
|
|
options.tailLines = readPositiveIntegerCliValue(args, ++index, arg);
|
|
break;
|
|
default:
|
|
throw new UserFacingError(`unknown option: ${arg}`);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function readCliValue(args, index, option) {
|
|
const value = args[index];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new UserFacingError(`${option} requires a value`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function readPositiveIntegerCliValue(args, index, option) {
|
|
const value = readCliValue(args, index, option);
|
|
if (!/^[1-9]\d*$/u.test(value)) {
|
|
throw new UserFacingError(`${option} requires a positive integer`);
|
|
}
|
|
return Number.parseInt(value, 10);
|
|
}
|
|
|
|
function applyCliOptions(options) {
|
|
maxParallel = options.maxParallel;
|
|
tailLines = options.tailLines;
|
|
dryRun = options.dryRun;
|
|
failedOnly = options.failedOnly;
|
|
pushReady = options.pushReady;
|
|
verbose = options.verbose;
|
|
baseRef = options.baseRef ?? resolveDefaultBaseRef();
|
|
}
|
|
|
|
function isLintableCodeFile(file) {
|
|
return /\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx)$/u.test(file);
|
|
}
|
|
|
|
export function selectExistingLintFiles(
|
|
changedFiles,
|
|
fileExists = fileExistsWithinWorkspace
|
|
) {
|
|
return changedFiles.filter(
|
|
(file) => isLintableCodeFile(file) && fileExists(file)
|
|
);
|
|
}
|
|
|
|
function isTestFile(file) {
|
|
return /\.(?:test|spec)\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/u.test(file);
|
|
}
|
|
|
|
function isPackageValidationRelevant(file) {
|
|
return (
|
|
isLintableCodeFile(file) ||
|
|
isTestFile(file) ||
|
|
/(?:^|\/)(package\.json|tsconfig[^/]*\.json|vitest\.config\.ts|tsup\.config\.ts)$/u.test(
|
|
file
|
|
)
|
|
);
|
|
}
|
|
|
|
export function isPackageBuildRelevant(file, isPackageManifestPackRelevant) {
|
|
if (!isPackagePackRelevantPath(file)) {
|
|
return false;
|
|
}
|
|
if (
|
|
/(?:^|\/)package\.json$/u.test(file) &&
|
|
!isPackageManifestPackRelevant(file)
|
|
) {
|
|
return false;
|
|
}
|
|
return (
|
|
isPackageValidationRelevant(file) ||
|
|
/(?:^|\/)(assets|public|style|styles)\//u.test(file) ||
|
|
/(?:electron\.vite\.config\.ts|vite\.web\.config\.mjs)$/u.test(file)
|
|
);
|
|
}
|
|
|
|
function isGlobalTypecheckRelevant(file) {
|
|
return [
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"pnpm-workspace.yaml",
|
|
"tsconfig.json"
|
|
].includes(file);
|
|
}
|
|
|
|
function isTypeScriptValidationInput(file) {
|
|
return (
|
|
isPackageValidationRelevant(file) ||
|
|
["pnpm-lock.yaml", "pnpm-workspace.yaml"].includes(file) ||
|
|
file.startsWith("packages/configs/")
|
|
);
|
|
}
|
|
|
|
function selectGoLaneInputs(
|
|
changedFiles,
|
|
moduleRoot,
|
|
forceBuiltinGenerate,
|
|
moduleRoots
|
|
) {
|
|
return changedFiles.filter(
|
|
(file) =>
|
|
(isGoValidationRelevant(file) &&
|
|
(isGlobalGoValidationRelevant(file) ||
|
|
resolveGoModuleRoot(file, moduleRoots) === moduleRoot)) ||
|
|
(forceBuiltinGenerate &&
|
|
moduleRoot === "services/tuttid" &&
|
|
file.startsWith("services/tuttid/builtin-apps/tutti-onboarding/"))
|
|
);
|
|
}
|
|
|
|
function fileExistsWithinWorkspace(file) {
|
|
return existsSync(join(workspaceRoot, file));
|
|
}
|
|
|
|
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`;
|
|
}
|
|
|
|
function failureExcerpt(path, lineCount) {
|
|
if (!existsSync(path)) {
|
|
return { label: "full log", text: "", truncated: false };
|
|
}
|
|
|
|
const excerpt = formatFailureExcerpt(readFileSync(path, "utf8"), lineCount);
|
|
return {
|
|
...excerpt,
|
|
label: excerpt.truncated
|
|
? `failure excerpt last ${lineCount} lines`
|
|
: "failure output"
|
|
};
|
|
}
|
|
|
|
class UserFacingError extends Error {}
|
|
|
|
const currentPath = fileURLToPath(import.meta.url);
|
|
if (process.argv[1] && resolve(process.argv[1]) === currentPath) {
|
|
Promise.resolve()
|
|
.then(() => applyCliOptions(parseCliArgs(process.argv.slice(2))))
|
|
.then(() => main())
|
|
.catch((error) => {
|
|
console.error(
|
|
error instanceof UserFacingError || error instanceof LaneCacheError
|
|
? `check:changed: ${error.message}`
|
|
: error instanceof Error
|
|
? (error.stack ?? error.message)
|
|
: error
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|