Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

73 lines · 2.3 KBRaw History
1import type { Workflow, WorkflowError } from "./model.js";
2
3/**
4 * Groups jobs into topological levels from `needs`: every job in level N only
5 * needs jobs in levels < N. Jobs keep file order within a level. Unknown
6 * `needs` and dependency cycles are reported as errors; jobs involved in a
7 * cycle (or depending on one) are left out of `order`.
8 */
9export function jobOrder(wf: Workflow): { order: string[][]; errors: WorkflowError[] } {
10 const errors: WorkflowError[] = [];
11 const keys = Object.keys(wf.jobs);
12 const known = new Set(keys);
13 const deps = new Map<string, string[]>();
14 for (const key of keys) {
15 const needs = wf.jobs[key]!.needs;
16 for (const n of needs) {
17 if (!known.has(n)) {
18 errors.push({ path: `jobs.${key}.needs`, message: `Job '${key}' depends on unknown job '${n}'` });
19 }
20 }
21 deps.set(key, needs.filter((n) => known.has(n)));
22 }
23
24 const order: string[][] = [];
25 const placed = new Set<string>();
26 let remaining = keys.slice();
27 while (remaining.length > 0) {
28 const level = remaining.filter((k) => deps.get(k)!.every((d) => placed.has(d)));
29 if (level.length === 0) break;
30 order.push(level);
31 for (const k of level) placed.add(k);
32 remaining = remaining.filter((k) => !placed.has(k));
33 }
34
35 if (remaining.length > 0) {
36 const cycle = findCycle(remaining, deps);
37 const where = cycle ? cycle[0]! : remaining[0]!;
38 errors.push({
39 path: `jobs.${where}.needs`,
40 message: cycle
41 ? `Dependency cycle between jobs: ${cycle.join(" -> ")}`
42 : `Jobs ${remaining.map((r) => `'${r}'`).join(", ")} can never run because of a dependency cycle`,
43 });
44 }
45 return { order, errors };
46}
47
48function findCycle(nodes: string[], deps: Map<string, string[]>): string[] | undefined {
49 const state = new Map<string, 1 | 2>();
50 const stack: string[] = [];
51 const visit = (n: string): string[] | undefined => {
52 state.set(n, 1);
53 stack.push(n);
54 for (const d of deps.get(n) ?? []) {
55 const s = state.get(d);
56 if (s === 1) return [...stack.slice(stack.indexOf(d)), d];
57 if (s === undefined) {
58 const found = visit(d);
59 if (found) return found;
60 }
61 }
62 stack.pop();
63 state.set(n, 2);
64 return undefined;
65 };
66 for (const n of nodes) {
67 if (!state.has(n)) {
68 const found = visit(n);
69 if (found) return found;
70 }
71 }
72 return undefined;
73}