@intentius/chant 0.124.23 → 0.124.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ci-jobs.d.ts +41 -1
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +48 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +14 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +9 -0
- package/dist/cli/handlers/emulator.d.ts +13 -2
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.js +63 -3
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +7 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.js +5 -4
- package/dist/cli/handlers/run.d.ts +4 -3
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +73 -9
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +17 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -0
- package/dist/cli/registry.d.ts +12 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +45 -6
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +97 -22
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +25 -10
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -2
- package/dist/lifecycle/git.d.ts +11 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +62 -6
- package/dist/lifecycle/no-record.d.ts +59 -0
- package/dist/lifecycle/no-record.d.ts.map +1 -0
- package/dist/lifecycle/no-record.js +88 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +30 -9
- package/dist/op/emulator-lifecycle.d.ts +23 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.js +39 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +10 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +7 -2
- package/dist/op/op-waves-notify.d.ts +2 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -1
- package/dist/op/op-waves-notify.js +9 -3
- package/dist/op/op-waves-run.d.ts +8 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +36 -11
- package/dist/op/op-waves.d.ts +85 -1
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +153 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +6 -0
- package/dist/op/step-console.d.ts +65 -0
- package/dist/op/step-console.d.ts.map +1 -0
- package/dist/op/step-console.js +140 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +238 -238
- package/dist/workspace/compose-graph.d.ts +22 -3
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +18 -5
- package/dist/workspace/graph.schema.json +68 -1
- package/dist/workspace/nested-graph.d.ts +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +5 -1
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/package.json +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- package/src/cli/handlers/approve.ts +9 -0
- package/src/cli/handlers/emulator.test.ts +61 -1
- package/src/cli/handlers/emulator.ts +65 -4
- package/src/cli/handlers/promote.test.ts +7 -0
- package/src/cli/handlers/promote.ts +7 -1
- package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
- package/src/cli/handlers/run-wave.ts +5 -4
- package/src/cli/handlers/run.test.ts +126 -0
- package/src/cli/handlers/run.ts +69 -9
- package/src/cli/main.test.ts +24 -0
- package/src/cli/main.ts +17 -4
- package/src/cli/parse-args.ts +21 -0
- package/src/cli/registry.ts +12 -0
- package/src/components/cli-support.test.ts +75 -0
- package/src/components/cli-support.ts +130 -25
- package/src/components/driver.test.ts +28 -0
- package/src/components/driver.ts +23 -10
- package/src/components/generate-disabled.test.ts +56 -0
- package/src/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -2
- package/src/lifecycle/git.ts +61 -6
- package/src/lifecycle/no-record.test.ts +175 -0
- package/src/lifecycle/no-record.ts +108 -0
- package/src/op/activities/shell.ts +28 -6
- package/src/op/emulator-lifecycle.test.ts +24 -1
- package/src/op/emulator-lifecycle.ts +58 -2
- package/src/op/gate.ts +3 -0
- package/src/op/index.ts +1 -1
- package/src/op/local-executor.ts +20 -3
- package/src/op/op-waves-jobs.test.ts +213 -0
- package/src/op/op-waves-notify.ts +11 -5
- package/src/op/op-waves-run.ts +42 -11
- package/src/op/op-waves.ts +202 -2
- package/src/op/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +28 -6
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +68 -1
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/runs.ts +4 -2
|
@@ -104,6 +104,61 @@ describe("composeWorkspaceGraph", () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
describe("CI edges, pipelines and dangling references (#3922)", () => {
|
|
108
|
+
/** A github member as `chant graph` reports it after #3910. */
|
|
109
|
+
const ci = (workflow: string, job: string): GraphIR => ({
|
|
110
|
+
version: 1,
|
|
111
|
+
nodes: [
|
|
112
|
+
{ id: workflow, kind: "Workflow", lexicon: "github", attrs: {} },
|
|
113
|
+
{ id: job, kind: "Job", lexicon: "github", attrs: {} },
|
|
114
|
+
{ id: "deploy", kind: "Job", lexicon: "github", attrs: {} },
|
|
115
|
+
],
|
|
116
|
+
edges: [
|
|
117
|
+
{ from: "deploy", to: job, kind: "needs", viaAttr: "needs" },
|
|
118
|
+
{ from: job, to: workflow, kind: "calls", viaAttr: "uses", crossPipeline: true },
|
|
119
|
+
],
|
|
120
|
+
groups: { byLexicon: { github: [job, "deploy", workflow] }, byPipeline: { ci: [job, "deploy", workflow] } },
|
|
121
|
+
dangling: [
|
|
122
|
+
{ from: "deploy", kind: "needs", path: "needs", value: "lint" },
|
|
123
|
+
{ from: job, kind: "calls", path: "uses", value: "other/repo/.github/workflows/x.yml@main", crossPipeline: true },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
const doc = composeWorkspaceGraph({ name: "acme", root: "." }, [
|
|
127
|
+
{ member: member("svc", "svc"), ir: ci("ciWorkflow", "build") },
|
|
128
|
+
{ member: member("app", "app"), ir: ci("ciWorkflow", "test") },
|
|
129
|
+
{ member: member("db", "db"), ir: db },
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
test("edges keep their kind and crossPipeline", () => {
|
|
133
|
+
expect(doc.edges.filter((e) => e.member === "svc")).toEqual([
|
|
134
|
+
{ from: "svc/build", to: "svc/ciWorkflow", kind: "calls", viaAttr: "uses", crossPipeline: true, member: "svc" },
|
|
135
|
+
{ from: "svc/deploy", to: "svc/build", kind: "needs", viaAttr: "needs", member: "svc" },
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("byPipeline keys and ids are prefixed with the member", () => {
|
|
140
|
+
expect(doc.groups.byPipeline).toEqual({
|
|
141
|
+
"app/ci": ["app/ciWorkflow", "app/deploy", "app/test"],
|
|
142
|
+
"svc/ci": ["svc/build", "svc/ciWorkflow", "svc/deploy"],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("dangling references keep their fields, with from prefixed and the member named, sorted", () => {
|
|
147
|
+
expect(doc.dangling).toEqual([
|
|
148
|
+
{ from: "app/deploy", kind: "needs", path: "needs", value: "lint", member: "app" },
|
|
149
|
+
{ from: "app/test", kind: "calls", path: "uses", value: "other/repo/.github/workflows/x.yml@main", crossPipeline: true, member: "app" },
|
|
150
|
+
{ from: "svc/build", kind: "calls", path: "uses", value: "other/repo/.github/workflows/x.yml@main", crossPipeline: true, member: "svc" },
|
|
151
|
+
{ from: "svc/deploy", kind: "needs", path: "needs", value: "lint", member: "svc" },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("no dangling section and no byPipeline when no member reports either", () => {
|
|
156
|
+
const plain = composeWorkspaceGraph({ name: "acme", root: "." }, [{ member: member("db", "db"), ir: db }]);
|
|
157
|
+
expect("dangling" in plain).toBe(false);
|
|
158
|
+
expect(plain.groups.byPipeline).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
107
162
|
describe("the links section (#2539)", () => {
|
|
108
163
|
test("inferred joins carry their label, and a declared link suppresses the inferred edge it covers", () => {
|
|
109
164
|
const consumer: GraphIR = {
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* `runtimeOwner` and export and import nodes are rewritten the same way.
|
|
12
12
|
* - `groups.byMember` maps each member to its node ids. `byLexicon` and
|
|
13
13
|
* `byComposite` keep their keys (a lexicon or composite type is the same
|
|
14
|
-
* thing in every member) and merge the ids. `byStack`, `byContainer
|
|
15
|
-
* `byWave` name things inside one member, so their keys
|
|
14
|
+
* thing in every member) and merge the ids. `byStack`, `byContainer`,
|
|
15
|
+
* `byWave` and `byPipeline` name things inside one member, so their keys
|
|
16
|
+
* are prefixed too.
|
|
17
|
+
* - Edges keep their `kind` and `crossPipeline` (#3910). A member's dangling
|
|
18
|
+
* references (`ir.dangling`) land on the top-level `dangling` list with
|
|
19
|
+
* `from` prefixed and a `member` field, like edges (#3922).
|
|
16
20
|
* - `links` holds the member links (#2524 D6, #2539): the declared links and
|
|
17
21
|
* the joins inferred from members' `imports` and `exports` with the core
|
|
18
22
|
* `joinKey()`, labelled `exact` or `folded` (`./links.ts`). A declared link
|
|
@@ -36,7 +40,7 @@ import { graphLinks, type LinkTableRow } from "./links";
|
|
|
36
40
|
import type { KindLinkRead } from "./kind-link-readers";
|
|
37
41
|
import type { RecordLinkRow } from "./record-assets";
|
|
38
42
|
import type { ReasonCode } from "./reason-codes";
|
|
39
|
-
import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
|
|
43
|
+
import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRDanglingRef, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
|
|
40
44
|
|
|
41
45
|
/** The version of the composed document `chant workspace graph` writes. */
|
|
42
46
|
export const WORKSPACE_GRAPH_VERSION = 1;
|
|
@@ -165,6 +169,8 @@ export const COLLECTOR_META_KEY = "collector";
|
|
|
165
169
|
/** `nested` names the nested workspace's member a node was read through (#2551); absent for the outer workspace's own nodes. */
|
|
166
170
|
export type ComposedNode = IRNode & { member: string; nested?: string };
|
|
167
171
|
export type ComposedEdge = IREdge & { member: string };
|
|
172
|
+
/** A member's dangling reference (#3910, #3922): `from` is prefixed like a node id. `nested` as on {@link ComposedNode}. */
|
|
173
|
+
export type ComposedDangling = IRDanglingRef & { member: string; nested?: string };
|
|
168
174
|
|
|
169
175
|
export interface WorkspaceGraph {
|
|
170
176
|
version: number;
|
|
@@ -176,6 +182,12 @@ export interface WorkspaceGraph {
|
|
|
176
182
|
exports: (IRExport & { member: string })[];
|
|
177
183
|
imports: (IRImport & { member: string })[];
|
|
178
184
|
derivedAttrs?: Record<string, string[]>;
|
|
185
|
+
/**
|
|
186
|
+
* The members' dangling CI references (#3910, #3922): each `ir.dangling`
|
|
187
|
+
* entry with `from` prefixed by its member and a `member` field, sorted by
|
|
188
|
+
* `from`, `path` and `value`. Absent when no member has one.
|
|
189
|
+
*/
|
|
190
|
+
dangling?: ComposedDangling[];
|
|
179
191
|
/**
|
|
180
192
|
* Member links (#2524 D6, #2539), declared and inferred, then the links of
|
|
181
193
|
* the records read with `--kind` (#2549). Empty when no declaration is given.
|
|
@@ -289,6 +301,13 @@ function prefixRefs(value: unknown, member: string): unknown {
|
|
|
289
301
|
return out;
|
|
290
302
|
}
|
|
291
303
|
|
|
304
|
+
/** The groups whose keys name something inside one member, so are prefixed with it. */
|
|
305
|
+
export const SCOPED_GROUPS = ["byStack", "byContainer", "byWave", "byPipeline"] as const;
|
|
306
|
+
type ScopedGroup = (typeof SCOPED_GROUPS)[number];
|
|
307
|
+
|
|
308
|
+
/** The order the composed `dangling` list is sorted in. */
|
|
309
|
+
export const danglingKey = (d: IRDanglingRef): string => `${d.from}\u0000${d.path}\u0000${d.value}`;
|
|
310
|
+
|
|
292
311
|
function mergeInto(target: Record<string, string[]>, key: string, ids: string[]): void {
|
|
293
312
|
(target[key] ??= []).push(...ids);
|
|
294
313
|
}
|
|
@@ -317,7 +336,8 @@ export function composeWorkspaceGraph(
|
|
|
317
336
|
const imports: WorkspaceGraph["imports"] = [];
|
|
318
337
|
const byMember: Record<string, string[]> = {};
|
|
319
338
|
const shared: Record<"byLexicon" | "byComposite", Record<string, string[]>> = { byLexicon: {}, byComposite: {} };
|
|
320
|
-
const scoped: Record<
|
|
339
|
+
const scoped: Record<ScopedGroup, Record<string, string[]>> = { byStack: {}, byContainer: {}, byWave: {}, byPipeline: {} };
|
|
340
|
+
const dangling: ComposedDangling[] = [];
|
|
321
341
|
const derivedAttrs: Record<string, Set<string>> = {};
|
|
322
342
|
const collectors: MemberCollector[] = [];
|
|
323
343
|
|
|
@@ -337,9 +357,10 @@ export function composeWorkspaceGraph(
|
|
|
337
357
|
for (const key of ["byLexicon", "byComposite"] as const) {
|
|
338
358
|
for (const [k, v] of Object.entries(ir.groups[key] ?? {})) mergeInto(shared[key], k, v.map((id) => prefix(m, id)));
|
|
339
359
|
}
|
|
340
|
-
for (const key of
|
|
360
|
+
for (const key of SCOPED_GROUPS) {
|
|
341
361
|
for (const [k, v] of Object.entries(ir.groups[key] ?? {})) mergeInto(scoped[key], prefix(m, k), v.map((id) => prefix(m, id)));
|
|
342
362
|
}
|
|
363
|
+
for (const d of ir.dangling ?? []) dangling.push({ ...d, from: prefix(m, d.from), member: m });
|
|
343
364
|
for (const e of ir.exports ?? []) exports.push({ ...e, ...(e.node ? { node: prefix(m, e.node) } : {}), member: m });
|
|
344
365
|
for (const i of ir.imports ?? []) imports.push({ ...i, node: prefix(m, i.node), member: m });
|
|
345
366
|
for (const [kind, attrs] of Object.entries(ir.derivedAttrs ?? {})) for (const a of attrs) (derivedAttrs[kind] ??= new Set()).add(a);
|
|
@@ -360,7 +381,7 @@ export function composeWorkspaceGraph(
|
|
|
360
381
|
|
|
361
382
|
const groups: WorkspaceGraph["groups"] = { byMember };
|
|
362
383
|
for (const key of ["byLexicon", "byComposite"] as const) if (Object.keys(shared[key]).length) groups[key] = sorted(shared[key]);
|
|
363
|
-
for (const key of
|
|
384
|
+
for (const key of SCOPED_GROUPS) if (Object.keys(scoped[key]).length) groups[key] = sorted(scoped[key]);
|
|
364
385
|
|
|
365
386
|
const doc: WorkspaceGraph = {
|
|
366
387
|
version: WORKSPACE_GRAPH_VERSION,
|
|
@@ -377,6 +398,7 @@ export function composeWorkspaceGraph(
|
|
|
377
398
|
collectors,
|
|
378
399
|
records: [],
|
|
379
400
|
};
|
|
401
|
+
if (dangling.length) doc.dangling = dangling.sort((a, b) => danglingKey(a).localeCompare(danglingKey(b)));
|
|
380
402
|
if (Object.keys(derivedAttrs).length) {
|
|
381
403
|
doc.derivedAttrs = Object.fromEntries(Object.keys(derivedAttrs).sort().map((k) => [k, [...derivedAttrs[k]].sort()]));
|
|
382
404
|
}
|
|
@@ -15,7 +15,7 @@ import { existsSync, readdirSync, readFileSync, rmSync, utimesSync, writeFileSyn
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { afterAll, describe, expect, test } from "vitest";
|
|
17
17
|
import { cleanScratch, commitAll, contract, declaration, declaration as declaration_, FAKE_FILE_GRAPH_CHANT, FAKE_GRAPH_CHANT, git, REPO, repo, scratchDir, validSchema } from "./__fixtures__/contract-repo";
|
|
18
|
-
import type { GraphIR } from "../graph-ir";
|
|
18
|
+
import type { GraphIR, IREdgeKind } from "../graph-ir";
|
|
19
19
|
import { composeWorkspaceGraph, MEMBER_RUN_REASON_CODES } from "./compose-graph";
|
|
20
20
|
import { parseDeclaration, WORKSPACE_ERROR_CODES } from "./declaration";
|
|
21
21
|
import type { LinkTableRow } from "./links";
|
|
@@ -106,6 +106,31 @@ describe("the links section (#2539)", () => {
|
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
describe("CI edges, pipelines and dangling references (#3922)", () => {
|
|
110
|
+
test("the edge kinds match IREdgeKind, and a composed CI member validates", () => {
|
|
111
|
+
const kinds: IREdgeKind[] = ["ref", "needs", "calls", "triggers", "includes", "extends"];
|
|
112
|
+
expect(schema.$defs.edge.properties.kind.enum).toEqual(kinds);
|
|
113
|
+
const ir: GraphIR = {
|
|
114
|
+
version: 1,
|
|
115
|
+
nodes: [
|
|
116
|
+
{ id: "ci", kind: "Workflow", lexicon: "github", attrs: {} },
|
|
117
|
+
{ id: "build", kind: "Job", lexicon: "github", attrs: {} },
|
|
118
|
+
{ id: "deploy", kind: "Job", lexicon: "github", attrs: {} },
|
|
119
|
+
],
|
|
120
|
+
edges: [
|
|
121
|
+
{ from: "deploy", to: "build", kind: "needs", viaAttr: "needs" },
|
|
122
|
+
{ from: "build", to: "ci", kind: "calls", viaAttr: "uses", crossPipeline: true },
|
|
123
|
+
],
|
|
124
|
+
groups: { byPipeline: { ci: ["build", "ci", "deploy"] } },
|
|
125
|
+
dangling: [{ from: "deploy", kind: "needs", path: "needs", value: "lint" }, { from: "build", kind: "calls", path: "uses", value: "o/r/.github/workflows/x.yml@v1", crossPipeline: true }],
|
|
126
|
+
};
|
|
127
|
+
const m = { name: "svc", dir: "svc", kind: "chant", status: "composed" as const, reason: null, chant: "0.124.20", irVersion: 1, live: false };
|
|
128
|
+
const graph = composeWorkspaceGraph({ name: "acme", root: "." }, [{ member: m, ir }]);
|
|
129
|
+
expect(graph.dangling).toHaveLength(2);
|
|
130
|
+
expectValid({ $schema: GRAPH_OUTPUT_SCHEMA_ID, contract: 1, chant: "0.124.20", at: null, ...graph });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
109
134
|
describe("the collectors section (#2559)", () => {
|
|
110
135
|
const topology = {
|
|
111
136
|
pipelines: [{ id: "traces", signal: "traces", receivers: ["otlp"], processors: [], exporters: ["otlphttp"] }],
|
|
@@ -93,6 +93,13 @@
|
|
|
93
93
|
"$ref": "#/$defs/edge"
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
|
+
"dangling": {
|
|
97
|
+
"description": "Added in contract 1 by #3922. The members' references whose target is no node of their graph (#3910): a job `needs` a job no workflow declares, a `uses:` names another repository's workflow, a GitLab `include:` names a file chant did not generate. Each member's `ir.dangling` entry with `from` prefixed by its member and a `member` field, sorted by `from`, `path` and `value`. Absent when no member has one.",
|
|
98
|
+
"type": "array",
|
|
99
|
+
"items": {
|
|
100
|
+
"$ref": "#/$defs/dangling"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
96
103
|
"groups": {
|
|
97
104
|
"type": "object",
|
|
98
105
|
"required": [
|
|
@@ -158,6 +165,16 @@
|
|
|
158
165
|
}
|
|
159
166
|
},
|
|
160
167
|
"description": "Keys prefixed with the member."
|
|
168
|
+
},
|
|
169
|
+
"byPipeline": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"additionalProperties": {
|
|
172
|
+
"type": "array",
|
|
173
|
+
"items": {
|
|
174
|
+
"type": "string"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"description": "Added in contract 1 by #3922. CI pipeline membership (#3910): a workflow or pipeline the member's CI lexicon reports, with the node ids it runs. Keys prefixed with the member, like byStack. Edges with `crossPipeline` run between these groups."
|
|
161
178
|
}
|
|
162
179
|
}
|
|
163
180
|
},
|
|
@@ -523,7 +540,15 @@
|
|
|
523
540
|
"type": "string"
|
|
524
541
|
},
|
|
525
542
|
"kind": {
|
|
526
|
-
"
|
|
543
|
+
"enum": [
|
|
544
|
+
"ref",
|
|
545
|
+
"needs",
|
|
546
|
+
"calls",
|
|
547
|
+
"triggers",
|
|
548
|
+
"includes",
|
|
549
|
+
"extends"
|
|
550
|
+
],
|
|
551
|
+
"description": "`ref` for an edge from a typed reference. The CI kinds (#3910): `needs` (a job waits on another), `calls` (a reusable workflow or action is used), `triggers` (a pipeline starts another), `includes` (a file is pulled in), `extends` (a GitLab job inherits another). The list grows within contract 1; a reader ignores a kind it does not know."
|
|
527
552
|
},
|
|
528
553
|
"viaAttr": {
|
|
529
554
|
"type": "string"
|
|
@@ -531,11 +556,53 @@
|
|
|
531
556
|
"toAttr": {
|
|
532
557
|
"type": "string"
|
|
533
558
|
},
|
|
559
|
+
"crossPipeline": {
|
|
560
|
+
"type": "boolean",
|
|
561
|
+
"description": "Added in contract 1 by #3922. True when the edge leaves the pipeline `from` belongs to (#3910): a reusable-workflow call, a `workflow_run` trigger, a GitLab child or multi-project trigger, an include. Absent on every in-pipeline edge."
|
|
562
|
+
},
|
|
534
563
|
"member": {
|
|
535
564
|
"type": "string"
|
|
536
565
|
}
|
|
537
566
|
}
|
|
538
567
|
},
|
|
568
|
+
"dangling": {
|
|
569
|
+
"type": "object",
|
|
570
|
+
"required": [
|
|
571
|
+
"from",
|
|
572
|
+
"kind",
|
|
573
|
+
"path",
|
|
574
|
+
"value",
|
|
575
|
+
"member"
|
|
576
|
+
],
|
|
577
|
+
"properties": {
|
|
578
|
+
"from": {
|
|
579
|
+
"type": "string",
|
|
580
|
+
"description": "The node holding the reference, prefixed with its member."
|
|
581
|
+
},
|
|
582
|
+
"kind": {
|
|
583
|
+
"$ref": "#/$defs/edge/properties/kind"
|
|
584
|
+
},
|
|
585
|
+
"path": {
|
|
586
|
+
"type": "string",
|
|
587
|
+
"description": "Where on `from` it was written, such as `needs`, `uses` or `trigger.include`."
|
|
588
|
+
},
|
|
589
|
+
"value": {
|
|
590
|
+
"type": "string",
|
|
591
|
+
"description": "The reference exactly as written."
|
|
592
|
+
},
|
|
593
|
+
"crossPipeline": {
|
|
594
|
+
"type": "boolean",
|
|
595
|
+
"description": "As on an edge."
|
|
596
|
+
},
|
|
597
|
+
"member": {
|
|
598
|
+
"type": "string"
|
|
599
|
+
},
|
|
600
|
+
"nested": {
|
|
601
|
+
"type": "string",
|
|
602
|
+
"description": "For a member of kind `workspace`: the nested workspace's member the reference was read through, as on a node."
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
},
|
|
539
606
|
"failure": {
|
|
540
607
|
"description": "The declaration could not be read, and nothing was composed. Exit code 1.",
|
|
541
608
|
"type": "object",
|
|
@@ -97,6 +97,20 @@ describe("foldNested", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
describe("foldNested with CI pipelines and dangling references (#3922)", () => {
|
|
101
|
+
test("prefixes byPipeline and dangling with the outer member, and keeps the nested member on each reference", () => {
|
|
102
|
+
const outer = composeWorkspaceGraph({ name: "outer", root: "." }, [
|
|
103
|
+
{ member: member("platform", { kind: "workspace", status: "skipped", reason: { code: "kind-not-run", message: "nested" }, chant: null, irVersion: null }) },
|
|
104
|
+
]);
|
|
105
|
+
const inner = innerGraph();
|
|
106
|
+
inner.groups.byPipeline = { "api/ci": ["api/Queue"] };
|
|
107
|
+
inner.dangling = [{ from: "api/Queue", kind: "needs", path: "needs", value: "lint", member: "api" }];
|
|
108
|
+
foldNested(outer, outer.members[0], inner);
|
|
109
|
+
expect(outer.groups.byPipeline).toEqual({ "platform/api/ci": ["platform/api/Queue"] });
|
|
110
|
+
expect(outer.dangling).toEqual([{ from: "platform/api/Queue", kind: "needs", path: "needs", value: "lint", member: "platform", nested: "api" }]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
100
114
|
describe("the outer workspace never writes inside a nested one", () => {
|
|
101
115
|
let root: string;
|
|
102
116
|
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
import type { ParsedArgs } from "../cli/registry";
|
|
25
|
-
import type
|
|
25
|
+
import { danglingKey, SCOPED_GROUPS, type ComposedMember, type ComposedNode, type WorkspaceGraph } from "./compose-graph";
|
|
26
26
|
|
|
27
27
|
export type { NestedWorkspace } from "./compose-graph";
|
|
28
28
|
import { readerToolchain, type Toolchain } from "./member-commands";
|
|
@@ -148,12 +148,17 @@ export function foldNested(graph: WorkspaceGraph, member: ComposedMember, inner:
|
|
|
148
148
|
into[k] = [...(into[k] ?? []), ...v.map((id) => prefixed(outer, id))].sort();
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
for (const key of
|
|
151
|
+
for (const key of SCOPED_GROUPS) {
|
|
152
152
|
for (const [k, v] of Object.entries(innerGroups[key] ?? {})) {
|
|
153
153
|
const into = (groups[key] ??= {});
|
|
154
154
|
into[prefixed(outer, k)] = v.map((id) => prefixed(outer, id)).sort();
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
if (inner.dangling?.length) {
|
|
158
|
+
graph.dangling = [...(graph.dangling ?? []), ...inner.dangling.map((d) => ({ ...d, from: prefixed(outer, d.from), member: outer, nested: d.member }))].sort((a, b) =>
|
|
159
|
+
danglingKey(a).localeCompare(danglingKey(b)),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
157
162
|
for (const c of inner.collectors ?? []) graph.collectors.push({ ...c, member: prefixed(outer, c.member) });
|
|
158
163
|
for (const [kind, attrs] of Object.entries(inner.derivedAttrs ?? {})) {
|
|
159
164
|
const all = new Set([...(graph.derivedAttrs?.[kind] ?? []), ...attrs]);
|
package/src/workspace/runs.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
55
55
|
import { readFileSync, statSync } from "node:fs";
|
|
56
56
|
import { resolve } from "node:path";
|
|
57
57
|
import { z } from "zod";
|
|
58
|
-
import { describeLifecyclePush, fetchLifecycleStatus, type LifecyclePushStatus, pushLifecycleStatus, readBlobBySha, RefCASConflictError, writeLedgerFiles } from "../lifecycle/git";
|
|
58
|
+
import { describeLifecyclePush, fetchLifecycleStatus, ledgerRevision, type LifecyclePushStatus, pushLifecycleStatus, readBlobBySha, RefCASConflictError, writeLedgerFiles } from "../lifecycle/git";
|
|
59
59
|
import { resolveMemberLedger } from "../lifecycle/member-ledger";
|
|
60
60
|
import { sortedJsonReplacer } from "../utils";
|
|
61
61
|
import { followSquashes, type SquashFollow } from "./squash";
|
|
@@ -509,7 +509,9 @@ async function appendRun(top: string, rootOnDisk: string, id: string, make: (exi
|
|
|
509
509
|
let written: RunLine[] = [];
|
|
510
510
|
let lastErr: unknown;
|
|
511
511
|
for (let attempt = 1; attempt <= APPEND_RETRY_ATTEMPTS && commit === undefined; attempt++) {
|
|
512
|
-
|
|
512
|
+
// #3955: in a run that records nothing, the run's own unrecorded tip.
|
|
513
|
+
const rev = await ledgerRevision(top);
|
|
514
|
+
const prior = git(top, ["rev-parse", "--verify", "--quiet", `${rev === LEDGER_BRANCH ? `refs/heads/${LEDGER_BRANCH}` : rev}:${path}`])?.trim() || null;
|
|
513
515
|
const text = prior ? ((await readBlobBySha(prior, { cwd: top })) ?? "") : "";
|
|
514
516
|
const existing = parseRunFile(text, id).lines;
|
|
515
517
|
const lines = make(existing);
|