mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
Retain complete core graph validation in the existing required parallel boundary row for canonical extension-only changes. Discover all four noncore compiler consumers while preserving full fallback for aliases, missing inputs, mixed changes and incomplete inventories.
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
#!/usr/bin/env node
|
|
// Materialize compiler and lint selections only after the check-planning job installs dependencies.
|
|
import { appendFileSync, existsSync } from "node:fs";
|
|
import { detectChangedLanes } from "./changed-lanes.mts";
|
|
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
|
import { runWithFailedTrailer } from "./lib/failed-trailer.mts";
|
|
import { isRecord } from "./lib/record-shared.mjs";
|
|
import {
|
|
resolveChangedCiTsgoInputs,
|
|
selectTsgoCoreTestStripe,
|
|
} from "./lib/tsgo-core-test-shards.mts";
|
|
|
|
type CheckRow = {
|
|
check_name: string;
|
|
task: string;
|
|
runner: string;
|
|
type_graph_names_json?: string;
|
|
core_type_graph_names_json?: string;
|
|
core_type_concurrency?: number;
|
|
};
|
|
type StripeRow = {
|
|
stripe: number;
|
|
lint_selection_json?: string;
|
|
type_graph_names_json?: string;
|
|
root_type_stripe?: string;
|
|
};
|
|
type Matrix<Row> = { include: Row[] };
|
|
|
|
export type CiCheckPlanInput = {
|
|
typeGraphBoundaryOwner: "" | "check-plan" | "additional-checks";
|
|
changedPaths: string[];
|
|
changedCoreTestPaths: string[] | null;
|
|
runnerProfile: string;
|
|
checkMatrix: Matrix<CheckRow>;
|
|
coreTypeMatrix: Matrix<StripeRow>;
|
|
lintCoreMatrix: Matrix<StripeRow>;
|
|
lintExtensionMatrix: Matrix<StripeRow>;
|
|
};
|
|
|
|
/** Narrow checks and pack complete extension fallback within the existing runner budget. */
|
|
export async function createCiCheckPlan(input: CiCheckPlanInput) {
|
|
const runs = (task: string) => input.checkMatrix.include.some((row) => row.task === task);
|
|
const lintPlan = runs("lint")
|
|
? await (
|
|
await import("./check-changed.mts")
|
|
).createChangedCiLintPlan(detectChangedLanes(input.changedPaths), {
|
|
runnerProfile: input.runnerProfile,
|
|
})
|
|
: null;
|
|
const started = performance.now();
|
|
const typePlan =
|
|
runs("prod-types") || runs("test-types")
|
|
? await (
|
|
await import("./run-tsgo-core-test-shards.mts")
|
|
).createChangedCiTypeCheckPlan(input.changedPaths, {
|
|
cwd: process.cwd(),
|
|
coreBoundaryOwner:
|
|
input.typeGraphBoundaryOwner === "additional-checks" ? "additional-checks" : undefined,
|
|
})
|
|
: null;
|
|
// Full selection needs no discovery, but a boundary without another admitted owner stays here.
|
|
if (
|
|
typePlan?.mode === "full" &&
|
|
input.typeGraphBoundaryOwner === "check-plan" &&
|
|
!resolveChangedCiTsgoInputs(input.changedPaths, existsSync)
|
|
) {
|
|
const { checkCoreTsgoGraphBoundary } = await import("./check-tsgo-core-boundary.mts");
|
|
await checkCoreTsgoGraphBoundary();
|
|
}
|
|
const graphs = typePlan?.graphs ?? [];
|
|
const production = graphs.filter(({ name }) => ["core", "ui", "extensions"].includes(name));
|
|
const coreTests = graphs.filter(({ name }) => name.startsWith("core-test-"));
|
|
const hosted = ["github", "hybrid"].includes(input.runnerProfile);
|
|
const otherOrder =
|
|
!hosted && !input.changedCoreTestPaths
|
|
? ["extensions-test", "test-root", "scripts"]
|
|
: ["extensions-test", "scripts", "test-root"];
|
|
let other = otherOrder.flatMap((name) => graphs.filter((graph) => graph.name === name));
|
|
if (production.length + coreTests.length + other.length !== graphs.length) {
|
|
throw new Error("Every selected compiler graph must have a CI execution owner");
|
|
}
|
|
const centralCore = hosted ? [] : coreTests;
|
|
if (
|
|
(production.length > 0 && !runs("prod-types")) ||
|
|
((centralCore.length > 0 || other.length > 0) && !runs("test-types"))
|
|
) {
|
|
throw new Error("Selected compiler graphs have no preflight check template");
|
|
}
|
|
const assignedCore = new Set<string>();
|
|
const coreRows: StripeRow[] =
|
|
typePlan && hosted
|
|
? input.coreTypeMatrix.include.flatMap((row) => {
|
|
const configs = new Set(
|
|
selectTsgoCoreTestStripe(`${row.stripe}/5`)?.map(({ config }) => config),
|
|
);
|
|
const selected = coreTests.filter(({ config }) => configs.has(config));
|
|
for (const graph of selected) {
|
|
assignedCore.add(graph.name);
|
|
}
|
|
return selected.length
|
|
? [{ ...row, type_graph_names_json: JSON.stringify(selected.map(({ name }) => name)) }]
|
|
: [];
|
|
})
|
|
: [];
|
|
if (hosted && assignedCore.size !== coreTests.length) {
|
|
throw new Error("Selected core compiler graphs have no preflight stripe template");
|
|
}
|
|
// Reuse admitted rows only; small selections keep their serial central owner.
|
|
// Each root partition runs after its row's concurrent core compilers settle.
|
|
if (coreRows.length >= 4 && other.some(({ name }) => name === "test-root")) {
|
|
for (const [index, row] of coreRows.slice(-4).entries()) {
|
|
row.root_type_stripe = `${index + 1}/4`;
|
|
}
|
|
other = other.filter(({ name }) => name !== "test-root");
|
|
}
|
|
const checkRows = input.checkMatrix.include.flatMap((row) => {
|
|
if (row.task === "prod-types") {
|
|
return production.length
|
|
? [{ ...row, type_graph_names_json: JSON.stringify(production.map(({ name }) => name)) }]
|
|
: [];
|
|
}
|
|
if (row.task === "test-types") {
|
|
return centralCore.length || other.length
|
|
? [
|
|
{
|
|
...row,
|
|
core_type_graph_names_json: JSON.stringify(centralCore.map(({ name }) => name)),
|
|
core_type_concurrency: !hosted && input.changedCoreTestPaths ? 2 : 1,
|
|
type_graph_names_json: JSON.stringify(other.map(({ name }) => name)),
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
return [row];
|
|
});
|
|
const retainLintTemplates = (templates: Matrix<StripeRow>, selected: readonly StripeRow[]) => {
|
|
if (
|
|
selected.some((row) => !templates.include.some((template) => template.stripe === row.stripe))
|
|
) {
|
|
throw new Error("Selected lint stripe has no preflight execution owner");
|
|
}
|
|
return templates.include.flatMap((template) => {
|
|
const row = selected.find((candidate) => candidate.stripe === template.stripe);
|
|
return row ? [{ ...template, lint_selection_json: row.lint_selection_json }] : [];
|
|
});
|
|
};
|
|
const coreLint = lintPlan
|
|
? retainLintTemplates(input.lintCoreMatrix, lintPlan.core)
|
|
: input.lintCoreMatrix.include;
|
|
const extensionTemplates = lintPlan
|
|
? retainLintTemplates(input.lintExtensionMatrix, lintPlan.extensions)
|
|
: input.lintExtensionMatrix.include;
|
|
// Complete hybrid fallback can share setup and SDK preparation without changing its chunks.
|
|
const compactExtensions =
|
|
input.runnerProfile === "hybrid" &&
|
|
runs("lint") &&
|
|
lintPlan === null &&
|
|
extensionTemplates.length === 6 &&
|
|
extensionTemplates.every((row, index) => row.stripe === index + 1 && !row.lint_selection_json);
|
|
const extensionLint: (StripeRow & { stripe_count?: number })[] = [];
|
|
for (const row of extensionTemplates) {
|
|
if (!compactExtensions) {
|
|
extensionLint.push(row);
|
|
} else if (row.stripe <= 3) {
|
|
extensionLint.push({ ...row, stripe_count: 3 });
|
|
}
|
|
}
|
|
const checkJobCount =
|
|
checkRows.length +
|
|
(hosted ? coreRows.length + coreLint.length : 0) +
|
|
(input.runnerProfile === "hybrid" ? extensionLint.length : 0);
|
|
if (checkJobCount > 400) {
|
|
throw new Error("Check planning exceeds the observer's 400-job inventory bound");
|
|
}
|
|
if (typePlan) {
|
|
console.log(
|
|
`CI type plan ${typePlan.mode} in ${((performance.now() - started) / 1000).toFixed(1)}s: ${graphs.map(({ name }) => name).join(", ")}`,
|
|
);
|
|
}
|
|
return {
|
|
check_job_count: checkJobCount,
|
|
check_matrix: { include: checkRows },
|
|
core_type_matrix: { include: coreRows },
|
|
lint_core_matrix: { include: coreLint },
|
|
lint_extension_matrix: { include: extensionLint },
|
|
central_lint_selection_json: lintPlan ? JSON.stringify(lintPlan.central) : "",
|
|
run_lint_core: coreLint.length > 0,
|
|
run_lint_extensions: extensionLint.length > 0,
|
|
run_changed_core_type_stripes: coreRows.length > 0,
|
|
};
|
|
}
|
|
|
|
function paths(value: unknown): string[] {
|
|
if (
|
|
!Array.isArray(value) ||
|
|
value.length === 0 ||
|
|
!value.every((file) => typeof file === "string")
|
|
) {
|
|
throw new Error("Check planning requires a complete changed-path array");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function matrix<Row>(value: unknown, parse: (row: unknown) => Row): Matrix<Row> {
|
|
if (!isRecord(value) || !Array.isArray(value.include)) {
|
|
throw new Error("Check planning requires a preflight matrix");
|
|
}
|
|
return { include: value.include.map(parse) };
|
|
}
|
|
|
|
function stripe(value: unknown): StripeRow {
|
|
if (
|
|
!isRecord(value) ||
|
|
typeof value.stripe !== "number" ||
|
|
!Number.isInteger(value.stripe) ||
|
|
value.stripe < 1 ||
|
|
value.stripe > 6
|
|
) {
|
|
throw new Error("Check planning requires canonical stripe templates");
|
|
}
|
|
return { stripe: value.stripe };
|
|
}
|
|
|
|
function parseInput(value: unknown): CiCheckPlanInput {
|
|
if (!isRecord(value) || typeof value.runnerProfile !== "string") {
|
|
throw new Error("Check planning requires its preflight input");
|
|
}
|
|
const boundaryOwner = value.typeGraphBoundaryOwner;
|
|
if (
|
|
boundaryOwner !== "" &&
|
|
boundaryOwner !== "check-plan" &&
|
|
boundaryOwner !== "additional-checks"
|
|
) {
|
|
throw new Error("Check planning requires its compiler boundary owner");
|
|
}
|
|
return {
|
|
typeGraphBoundaryOwner: boundaryOwner,
|
|
changedPaths: paths(value.changedPaths),
|
|
changedCoreTestPaths:
|
|
value.changedCoreTestPaths === null ? null : paths(value.changedCoreTestPaths),
|
|
runnerProfile: value.runnerProfile,
|
|
checkMatrix: matrix(value.checkMatrix, (row): CheckRow => {
|
|
if (
|
|
!isRecord(row) ||
|
|
typeof row.check_name !== "string" ||
|
|
typeof row.task !== "string" ||
|
|
typeof row.runner !== "string"
|
|
) {
|
|
throw new Error("Check planning requires canonical check templates");
|
|
}
|
|
return { check_name: row.check_name, task: row.task, runner: row.runner };
|
|
}),
|
|
coreTypeMatrix: matrix(value.coreTypeMatrix, stripe),
|
|
lintCoreMatrix: matrix(value.lintCoreMatrix, stripe),
|
|
lintExtensionMatrix: matrix(value.lintExtensionMatrix, stripe),
|
|
};
|
|
}
|
|
|
|
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
|
await runWithFailedTrailer("ci-check-plan", async () => {
|
|
const output = process.env.GITHUB_OUTPUT;
|
|
if (!output) {
|
|
throw new Error("Check planning requires GITHUB_OUTPUT");
|
|
}
|
|
const plan = await createCiCheckPlan(
|
|
parseInput(JSON.parse(process.env.OPENCLAW_CI_CHECK_PLAN_INPUT_JSON ?? "null")),
|
|
);
|
|
for (const [name, value] of Object.entries(plan)) {
|
|
appendFileSync(
|
|
output,
|
|
`${name}=${typeof value === "string" ? value : JSON.stringify(value)}\n`,
|
|
);
|
|
}
|
|
});
|
|
}
|