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.
96 lines
3.8 KiB
JavaScript
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)}`;
|
|
}
|