Files
pleno-vue/scripts/test-graph/workflow-generator.mjs
T
Jeppe B 12a2d73b32 Replace monolithic E2E matrices with dependency-aware component CI
Generate small reusable component partitions with explicit provider dependencies, GitHub-hosted concurrency, fail-closed evidence, and exact mobile store gates. Include the literal-i18n fix from #206 so validation covers the exact post-merge tree.
2026-07-20 23:52:15 +02:00

96 lines
3.8 KiB
JavaScript

export const workflowJobId = (nodeId, lane) => `component_${nodeId}_${lane}`.replace(/-/gu, "_");
export const GENERATED_JOBS_BEGIN = " # BEGIN GENERATED DEPENDENCY-AWARE COMPONENT JOBS";
export const GENERATED_JOBS_END = " # END GENERATED DEPENDENCY-AWARE COMPONENT JOBS";
function runnablePrerequisiteJobs(graph, nodeId, lane, visited = new Set()) {
if (visited.has(nodeId)) {
return [];
}
visited.add(nodeId);
const prerequisite = graph.nodes.get(nodeId);
if (prerequisite.lanes.includes(lane)) {
return [workflowJobId(nodeId, lane)];
}
if (prerequisite.lanes.includes("contract")) {
return [workflowJobId(nodeId, "contract")];
}
return [...graph.dependencies.get(nodeId)].flatMap((dependency) =>
runnablePrerequisiteJobs(graph, dependency, lane, visited)
);
}
export function workflowDependencyJobIds(graph, nodeId, lane) {
const node = graph.nodes.get(nodeId);
const dependencies = new Set();
if (lane !== "contract" && node.lanes.includes("contract")) {
dependencies.add(workflowJobId(nodeId, "contract"));
}
for (const prerequisiteId of graph.dependencies.get(nodeId)) {
for (const prerequisiteJob of runnablePrerequisiteJobs(graph, prerequisiteId, lane)) {
dependencies.add(prerequisiteJob);
}
}
return [...dependencies].sort();
}
export function generateStaticComponentJobs(
graph,
{ planJobId = "plan_e2e", reusableWorkflow = "./.github/workflows/test-graph-node.yml", includeJobsKey = true } = {}
) {
const lines = includeJobsKey
? ["# Generated by scripts/test-graph/workflow-generator.mjs. Do not edit by hand.", "jobs:"]
: [];
for (const nodeId of graph.topologicalOrder) {
const node = graph.nodes.get(nodeId);
for (const lane of node.lanes) {
const jobId = workflowJobId(nodeId, lane);
const dependencies = workflowDependencyJobIds(graph, nodeId, lane);
lines.push(` ${jobId}:`);
lines.push(` name: ${JSON.stringify(`Component / ${node.label} / ${lane}`)}`);
lines.push(" needs:");
lines.push(` - ${planJobId}`);
if (lane !== "contract") {
lines.push(" - build");
}
for (const dependency of dependencies) {
lines.push(` - ${dependency}`);
}
lines.push(` if: \${{ contains(fromJSON(needs.${planJobId}.outputs.selected_jobs), '${jobId}') }}`);
lines.push(` uses: ${reusableWorkflow}`);
lines.push(" with:");
lines.push(` node: ${nodeId}`);
lines.push(` lane: ${lane}`);
const partitionCount = node.partitions[lane] || 1;
lines.push(
` partitions: "${JSON.stringify(
Array.from({ length: partitionCount }, (_, index) => index + 1)
).replaceAll('"', '\\"')}"`
);
lines.push(" permissions:");
lines.push(" contents: read");
}
}
return `${lines.join("\n")}\n`;
}
export function replaceGeneratedWorkflowSection(workflowSource, graph, options = {}) {
const begin = workflowSource.indexOf(GENERATED_JOBS_BEGIN);
const end = workflowSource.indexOf(GENERATED_JOBS_END);
if (begin === -1 || end === -1 || end < begin) {
throw new Error(
`Workflow must contain one ordered ${GENERATED_JOBS_BEGIN.trim()} / ${GENERATED_JOBS_END.trim()} marker pair.`
);
}
if (
workflowSource.indexOf(GENERATED_JOBS_BEGIN, begin + GENERATED_JOBS_BEGIN.length) !== -1 ||
workflowSource.indexOf(GENERATED_JOBS_END, end + GENERATED_JOBS_END.length) !== -1
) {
throw new Error("Workflow contains duplicate generated component job markers.");
}
const entries = generateStaticComponentJobs(graph, { ...options, includeJobsKey: false });
return `${workflowSource.slice(
0,
begin
)}${GENERATED_JOBS_BEGIN}\n${entries}${GENERATED_JOBS_END}${workflowSource.slice(end + GENERATED_JOBS_END.length)}`;
}