Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| A run opens on its summary: what triggered it, its status, duration and artifacts, a graph of its jobs (what needs what, matrices folded, called workflows boxed, deploys with their address), annotations, and each job's summary; the job list groups the same way | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { | |
| 5 | GRAPH, | |
| 6 | type GraphJob, | |
| 7 | baseName, | |
| 8 | columns, | |
| 9 | connector, | |
| 10 | crossings, | |
| 11 | groupJobs, | |
| 12 | layoutRun, | |
| 13 | orderColumns, | |
| 14 | standingOf, | |
| 15 | } from "./run-graph.ts"; | |
| 16 | ||
| 17 | let next = 0; | |
| 18 | function job(key: string, needs: string[] = [], more: Partial<GraphJob> = {}): GraphJob { | |
| 19 | return { | |
| 20 | id: `job_${next++}`, | |
| 21 | key, | |
| 22 | name: key, | |
| 23 | needs, | |
| 24 | status: "completed", | |
| 25 | conclusion: "success", | |
| 26 | startedAt: null, | |
| 27 | finishedAt: null, | |
| 28 | ...more, | |
| 29 | }; | |
| 30 | } | |
| 31 | ||
| 32 | test("columns go by the longest path of needs", () => { | |
| 33 | const units = groupJobs([job("lint"), job("build"), job("test", ["build"]), job("deploy", ["build", "test"])]); | |
| 34 | const column = columns(units); | |
| 35 | assert.deepEqual(Object.fromEntries(column), { lint: 0, build: 0, test: 1, deploy: 2 }); | |
| 36 | }); | |
| 37 | ||
| 38 | test("a need of a job not in the run is left out", () => { | |
| 39 | const column = columns(groupJobs([job("a", ["gone"]), job("b", ["a"])])); | |
| 40 | assert.deepEqual(Object.fromEntries(column), { a: 0, b: 1 }); | |
| 41 | }); | |
| 42 | ||
| 43 | test("a cycle cannot loop the layout", () => { | |
| 44 | const jobs = [job("a", ["b"]), job("b", ["a"]), job("c", ["c"])]; | |
| 45 | const column = columns(groupJobs(jobs)); | |
| 46 | for (const value of column.values()) assert.ok(Number.isFinite(value) && value >= 0); | |
| 47 | const layout = layoutRun(jobs); | |
| 48 | // Never a connector going back a column. | |
| 49 | assert.ok(layout.edges.length <= 1); | |
| 50 | for (const edge of layout.edges) assert.ok(column.get(edge.from)! < column.get(edge.to)!); | |
| 51 | }); | |
| 52 | ||
| 53 | test("ordering a column by its needs takes out a crossing", () => { | |
| 54 | // As listed: a, b over x, y, with x needing b and y needing a: they cross. | |
| 55 | const units = groupJobs([job("a"), job("b"), job("x", ["b"]), job("y", ["a"])]); | |
| 56 | const column = columns(units); | |
| 57 | const edges: [string, string][] = [ | |
| 58 | ["b", "x"], | |
| 59 | ["a", "y"], | |
| 60 | ]; | |
| 61 | assert.equal(crossings([["a", "b"], ["x", "y"]], edges), 1); | |
| 62 | const order = orderColumns(units, column); | |
| 63 | assert.deepEqual(order, [ | |
| 64 | ["a", "b"], | |
| 65 | ["y", "x"], | |
| 66 | ]); | |
| 67 | assert.equal(crossings(order, edges), 0); | |
| 68 | }); | |
| 69 | ||
| 70 | test("ordering keeps the order as listed when nothing crosses", () => { | |
| 71 | const units = groupJobs([job("a"), job("b"), job("x", ["a"]), job("y", ["b"])]); | |
| 72 | assert.deepEqual(orderColumns(units, columns(units)), [ | |
| 73 | ["a", "b"], | |
| 74 | ["x", "y"], | |
| 75 | ]); | |
| 76 | }); | |
| 77 | ||
| 78 | test("a matrix's jobs are one node, named without their combination", () => { | |
| 79 | const units = groupJobs([ | |
| 80 | job("build"), | |
| 81 | job("test", ["build"], { name: "test (ubuntu-latest, 20)" }), | |
| 82 | job("test", ["build"], { name: "test (ubuntu-latest, 22)" }), | |
| 83 | job("test", ["build"], { name: "test (macos-latest, 22)" }), | |
| 84 | ]); | |
| 85 | assert.equal(units.length, 2); | |
| 86 | const matrix = units[1]!; | |
| 87 | assert.equal(matrix.kind, "matrix"); | |
| 88 | assert.equal(matrix.label, "test"); | |
| 89 | assert.equal(matrix.kind === "matrix" && matrix.jobs.length, 3); | |
| 90 | assert.equal(baseName("test (a, b)"), "test"); | |
| 91 | assert.equal(baseName("plain"), "plain"); | |
| 92 | }); | |
| 93 | ||
| 94 | test("an expanded matrix is taller, and its connectors still meet its first line", () => { | |
| 95 | const jobs = [job("build"), job("test", ["build"], { name: "test (1)" }), job("test", ["build"], { name: "test (2)" })]; | |
| 96 | const closed = layoutRun(jobs); | |
| 97 | const open = layoutRun(jobs, new Set(["test"])); | |
| 98 | const node = (layout: typeof closed) => layout.nodes.find((n) => n.unit.key === "test")!; | |
| 99 | assert.equal(node(closed).h, GRAPH.nodeHeight); | |
| 100 | assert.equal(node(open).h, GRAPH.nodeHeight + 2 * GRAPH.rowHeight + 6); | |
| 101 | assert.ok(node(open).expanded); | |
| 102 | assert.equal(open.edges.length, 1); | |
| 103 | assert.equal(open.edges[0]!.path, closed.edges[0]!.path); | |
| 104 | }); | |
| 105 | ||
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 106 | test("a matrix's calls share the caller's box, each call's jobs named by its combination", () => { |
| 107 | const uses = "./.g1t/workflows/build.yml"; | |
| 108 | const jobs = [ | |
| 109 | job("build", [], { name: "Build (linux)", uses }), | |
| 110 | job("build", [], { name: "Build (mac)", uses }), | |
| 111 | job("build[0]/test", [], { name: "Build (linux) / test" }), | |
| 112 | job("build[1]/test", [], { name: "Build (mac) / test" }), | |
| 113 | job("deploy", ["build"]), | |
| 114 | ]; | |
| 115 | const units = groupJobs(jobs); | |
| 116 | assert.deepEqual( | |
| 117 | units.map((unit) => [unit.kind, unit.key]), | |
| 118 | [ | |
| 119 | ["call", "build"], | |
| 120 | ["job", "deploy"], | |
| 121 | ], | |
| 122 | ); | |
| 123 | const call = units[0]!; | |
| 124 | assert.ok(call.kind === "call"); | |
| 125 | assert.equal(call.callers.length, 2); | |
| 126 | assert.deepEqual( | |
| 127 | call.units.map((unit) => [unit.key, unit.label]), | |
| 128 | [ | |
| 129 | ["build[0]/test", "Build (linux) / test"], | |
| 130 | ["build[1]/test", "Build (mac) / test"], | |
| 131 | ], | |
| 132 | ); | |
| 133 | }); | |
| 134 | ||
| A run opens on its summary: what triggered it, its status, duration and artifacts, a graph of its jobs (what needs what, matrices folded, called workflows boxed, deploys with their address), annotations, and each job's summary; the job list groups the same way | 135 | test("a called workflow's jobs sit in a box under the job calling it", () => { |
| 136 | const jobs = [ | |
| 137 | job("build", [], { name: "Build", uses: "./.g1t/workflows/build.yml" }), | |
| 138 | job("build/compile", [], { name: "Build / compile" }), | |
| 139 | job("build/package", ["build/compile"], { name: "Build / package" }), | |
| 140 | job("deploy", ["build"]), | |
| 141 | ]; | |
| 142 | const units = groupJobs(jobs); | |
| 143 | assert.deepEqual( | |
| 144 | units.map((unit) => [unit.kind, unit.key]), | |
| 145 | [ | |
| 146 | ["call", "build"], | |
| 147 | ["job", "deploy"], | |
| 148 | ], | |
| 149 | ); | |
| 150 | const call = units[0]!; | |
| 151 | assert.ok(call.kind === "call"); | |
| 152 | assert.equal(call.uses, "./.g1t/workflows/build.yml"); | |
| 153 | assert.deepEqual( | |
| 154 | call.units.map((unit) => unit.label), | |
| 155 | ["compile", "package"], | |
| 156 | ); | |
| 157 | ||
| 158 | const layout = layoutRun(jobs); | |
| 159 | const box = layout.groups[0]!; | |
| 160 | const compile = layout.nodes.find((n) => n.unit.key === "build/compile")!; | |
| 161 | const pack = layout.nodes.find((n) => n.unit.key === "build/package")!; | |
| 162 | const deploy = layout.nodes.find((n) => n.unit.key === "deploy")!; | |
| 163 | // Inside the box, one after the other; the job needing the caller after the box. | |
| 164 | assert.ok(compile.x >= box.x && compile.x + compile.w <= box.x + box.w); | |
| 165 | assert.ok(pack.x > compile.x && pack.x + pack.w <= box.x + box.w); | |
| 166 | assert.ok(compile.y >= box.y + GRAPH.groupHead); | |
| 167 | assert.ok(deploy.x >= box.x + box.w + GRAPH.columnGap); | |
| 168 | assert.deepEqual( | |
| 169 | layout.edges.map((edge) => [edge.from, edge.to]).sort(), | |
| 170 | [ | |
| 171 | ["build", "deploy"], | |
| 172 | ["build/compile", "build/package"], | |
| 173 | ], | |
| 174 | ); | |
| 175 | assert.equal(layout.width, deploy.x + deploy.w + GRAPH.margin); | |
| 176 | }); | |
| 177 | ||
| 178 | test("a connector's colour follows the run: red from a failure, moving into a running job", () => { | |
| 179 | const layout = layoutRun([ | |
| 180 | job("a", [], { conclusion: "failure" }), | |
| 181 | job("b", ["a"], { conclusion: "skipped" }), | |
| 182 | job("c"), | |
| 183 | job("d", ["c"], { status: "in_progress", conclusion: null }), | |
| 184 | job("e", ["c"], { status: "waiting", conclusion: null }), | |
| 185 | ]); | |
| 186 | const state = Object.fromEntries(layout.edges.map((edge) => [`${edge.from}>${edge.to}`, edge.state])); | |
| 187 | assert.deepEqual(state, { "a>b": "failed", "c>d": "active", "c>e": "idle" }); | |
| 188 | }); | |
| 189 | ||
| 190 | test("a deployment's address makes its node taller", () => { | |
| 191 | const layout = layoutRun([job("deploy", [], { environment: "production", environmentUrl: "https://g1t.page" })]); | |
| 192 | assert.equal(layout.nodes[0]!.h, GRAPH.nodeHeight + GRAPH.urlHeight); | |
| 193 | }); | |
| 194 | ||
| 195 | test("several jobs stand together as running, then as the worst ending", () => { | |
| 196 | assert.deepEqual(standingOf([job("a"), job("b", [], { status: "calling", conclusion: null })]), { status: "in_progress", conclusion: null }); | |
| 197 | assert.deepEqual(standingOf([job("a"), job("b", [], { conclusion: "failure" }), job("c", [], { conclusion: "cancelled" })]), { | |
| 198 | status: "completed", | |
| 199 | conclusion: "failure", | |
| 200 | }); | |
| 201 | assert.deepEqual(standingOf([job("a", [], { conclusion: "skipped" })]), { status: "completed", conclusion: "skipped" }); | |
| 202 | }); | |
| 203 | ||
| 204 | test("connectors run straight on one row, and turn with rounded corners between rows", () => { | |
| 205 | assert.equal(connector(0, 10, 50, 10), "M0 10H50"); | |
| 206 | const turn = connector(0, 10, 100, 70); | |
| 207 | assert.match(turn, /^M0 10H\d+Q/); | |
| 208 | assert.ok(turn.endsWith("H100")); | |
| 209 | }); | |
| 210 | ||
| 211 | test("an empty run lays out as nothing", () => { | |
| 212 | const layout = layoutRun([]); | |
| 213 | assert.equal(layout.nodes.length, 0); | |
| 214 | assert.equal(layout.edges.length, 0); | |
| 215 | }); | |
| 216 | ||
| 217 | test("a matrix whose jobs deploy to one address shows it once", () => { | |
| 218 | const url = "https://g1t.sh"; | |
| 219 | const shared = layoutRun([ | |
| 220 | job("core", [], { name: "core (a)", environmentUrl: url }), | |
| 221 | job("core", [], { name: "core (b)", environmentUrl: url }), | |
| 222 | ]); | |
| 223 | assert.equal(shared.nodes[0]!.h, GRAPH.nodeHeight + GRAPH.urlHeight); | |
| 224 | const mixed = layoutRun([ | |
| 225 | job("core", [], { name: "core (a)", environmentUrl: url }), | |
| 226 | job("core", [], { name: "core (b)", environmentUrl: null }), | |
| 227 | ]); | |
| 228 | assert.equal(mixed.nodes[0]!.h, GRAPH.nodeHeight); | |
| 229 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.