@intentius/chant 0.124.33 → 0.124.35
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/cli/handlers/graph.js +2 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +147 -51
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +284 -81
- package/dist/cli/handlers/release.d.ts +12 -0
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +125 -10
- package/dist/cli/handlers/run.d.ts +3 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +12 -2
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +37 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -0
- package/dist/cli/registry.d.ts +17 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +8 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +3 -2
- package/dist/components/green-cover.d.ts +77 -0
- package/dist/components/green-cover.d.ts.map +1 -0
- package/dist/components/green-cover.js +153 -0
- package/dist/components/pr-pipeline.d.ts +34 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +39 -5
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/promote.d.ts +4 -1
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +6 -3
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +9 -0
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +15 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +11 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/graph-declared.d.ts +9 -1
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +18 -3
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +30 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +13 -2
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +5 -1
- package/dist/lifecycle/compose-refs.d.ts +52 -0
- package/dist/lifecycle/compose-refs.d.ts.map +1 -0
- package/dist/lifecycle/compose-refs.js +115 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.js +3 -2
- package/dist/lifecycle/green-apply.d.ts +39 -0
- package/dist/lifecycle/green-apply.d.ts.map +1 -0
- package/dist/lifecycle/green-apply.js +51 -0
- package/dist/lifecycle/publish-declaration.d.ts +4 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +15 -0
- package/dist/lifecycle/publish-npm.d.ts +7 -4
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +60 -13
- package/dist/lifecycle/release-compose.d.ts +91 -0
- package/dist/lifecycle/release-compose.d.ts.map +1 -0
- package/dist/lifecycle/release-compose.js +235 -0
- package/dist/lifecycle/release-cut.d.ts +8 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +2 -0
- package/dist/lifecycle/release-publish.d.ts +2 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +2 -1
- package/dist/lifecycle/release-store.d.ts +15 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +5 -1
- package/dist/wave-runtime.mjs +226 -224
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +20 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +130 -0
- package/dist/workspace/checks/security.d.ts +16 -1
- package/dist/workspace/checks/security.d.ts.map +1 -1
- package/dist/workspace/checks/security.js +4 -4
- package/dist/workspace/checks.d.ts +1 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +4 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +5 -3
- package/dist/workspace/ci-green-workflow.d.ts +18 -2
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +27 -5
- package/dist/workspace/ci-green.d.ts +31 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +78 -4
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-run.d.ts +6 -0
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +2 -1
- package/dist/workspace/ci-serve.d.ts +94 -13
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +248 -30
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +18 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +9 -2
- package/dist/workspace/declaration.schema.json +28 -0
- package/dist/workspace/delivery-metrics.d.ts +3 -1
- package/dist/workspace/delivery-metrics.d.ts.map +1 -1
- package/dist/workspace/delivery-metrics.js +12 -3
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +4 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +1 -0
- package/dist/workspace/reason-codes.d.ts +9 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +10 -0
- package/dist/workspace/release-candidates.d.ts +28 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -1
- package/dist/workspace/release-candidates.js +38 -2
- package/dist/workspace/release-candidates.schema.json +51 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/releases.schema.json +30 -0
- package/package.json +1 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/pr-green.test.ts +230 -0
- package/src/cli/handlers/pr.ts +148 -66
- package/src/cli/handlers/promote.test.ts +34 -0
- package/src/cli/handlers/promote.ts +302 -91
- package/src/cli/handlers/release-compose.e2e.test.ts +133 -0
- package/src/cli/handlers/release.ts +112 -10
- package/src/cli/handlers/run.ts +12 -1
- package/src/cli/main.ts +37 -2
- package/src/cli/parse-args.ts +18 -0
- package/src/cli/registry.ts +17 -1
- package/src/codegen/release-wiring.test.ts +56 -9
- package/src/components/auto-release.test.ts +22 -0
- package/src/components/auto-release.ts +9 -2
- package/src/components/green-cover.test.ts +170 -0
- package/src/components/green-cover.ts +186 -0
- package/src/components/pr-pipeline.test.ts +14 -4
- package/src/components/pr-pipeline.ts +56 -5
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/promote.test.ts +7 -0
- package/src/components/promote.ts +9 -4
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +24 -1
- package/src/components/release-promote.ts +15 -0
- package/src/deep-observation.ts +8 -0
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.test.ts +18 -0
- package/src/graph-declared.ts +17 -2
- package/src/graph-detail.test.ts +68 -1
- package/src/graph-detail.ts +29 -1
- package/src/graph-ir.ts +12 -4
- package/src/lifecycle/__fixtures__/publish-fakes.ts +5 -1
- package/src/lifecycle/compose-refs.ts +125 -0
- package/src/lifecycle/deep-observe.test.ts +20 -0
- package/src/lifecycle/deep-observe.ts +4 -2
- package/src/lifecycle/green-apply.ts +71 -0
- package/src/lifecycle/publish-declaration.ts +15 -0
- package/src/lifecycle/publish-npm.ts +55 -13
- package/src/lifecycle/publish-targets.test.ts +45 -0
- package/src/lifecycle/release-compose.test.ts +125 -0
- package/src/lifecycle/release-compose.ts +312 -0
- package/src/lifecycle/release-cut.ts +6 -0
- package/src/lifecycle/release-publish.ts +4 -1
- package/src/lifecycle/release-store.ts +16 -1
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +20 -2
- package/src/workspace/checks/ci-workflow.test.ts +160 -0
- package/src/workspace/checks/ci-workflow.ts +135 -0
- package/src/workspace/checks/security.ts +4 -4
- package/src/workspace/checks.ts +4 -1
- package/src/workspace/ci-green-cli.ts +5 -3
- package/src/workspace/ci-green-workflow.test.ts +52 -0
- package/src/workspace/ci-green-workflow.ts +34 -5
- package/src/workspace/ci-green.test.ts +33 -0
- package/src/workspace/ci-green.ts +85 -4
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run.test.ts +18 -13
- package/src/workspace/ci-run.ts +8 -1
- package/src/workspace/ci-serve-land.e2e.test.ts +138 -0
- package/src/workspace/ci-serve-land.test.ts +176 -0
- package/src/workspace/ci-serve.test.ts +77 -43
- package/src/workspace/ci-serve.ts +268 -34
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +28 -0
- package/src/workspace/declaration.ts +32 -3
- package/src/workspace/delivery-metrics.ts +12 -3
- package/src/workspace/graph-contract.test.ts +3 -2
- package/src/workspace/init.ts +2 -2
- package/src/workspace/member-commands.test.ts +11 -4
- package/src/workspace/member-commands.ts +3 -0
- package/src/workspace/nested-graph.test.ts +4 -0
- package/src/workspace/nested-graph.ts +1 -0
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +10 -0
- package/src/workspace/release-candidates.schema.json +51 -0
- package/src/workspace/release-candidates.test.ts +30 -3
- package/src/workspace/release-candidates.ts +54 -2
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/releases.schema.json +30 -0
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* flags whose record files changed in its range, from the declared flag kinds
|
|
25
25
|
* (#4024).
|
|
26
26
|
*
|
|
27
|
+
* Each candidate also groups its changes (#4168 4.4) by work item, then by
|
|
28
|
+
* pull request, then by commit: a change lands under every work item its
|
|
29
|
+
* trailers name, or under no work item, and within that under its pull
|
|
30
|
+
* request, or under none for a commit that landed without one.
|
|
31
|
+
*
|
|
27
32
|
* It reads local refs and tags only and never fetches. Git is read through a
|
|
28
33
|
* local `git` subprocess.
|
|
29
34
|
*/
|
|
@@ -66,6 +71,34 @@ export function prFromSubject(subject) {
|
|
|
66
71
|
const m = PR_IN_SUBJECT.exec(subject);
|
|
67
72
|
return m ? Number(m[1]) : null;
|
|
68
73
|
}
|
|
74
|
+
/** The pull request a merge record names in its `source`, or null. */
|
|
75
|
+
export function mergeRecordPr(merge) {
|
|
76
|
+
const source = merge?.source;
|
|
77
|
+
return source?.kind === "pull-request" && typeof source.pr === "number" ? source.pr : null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Group changes, given newest first, by work item, then pull request, then
|
|
81
|
+
* commit. A change with work items lands in each one's group; the rest land
|
|
82
|
+
* in the `work: null` group, last. Within a group, pull requests come in the
|
|
83
|
+
* order their oldest commit landed, and commits without one last.
|
|
84
|
+
*/
|
|
85
|
+
export function groupChanges(changes) {
|
|
86
|
+
const oldestFirst = [...changes].reverse();
|
|
87
|
+
const groups = new Map();
|
|
88
|
+
const add = (work, pr, commit) => {
|
|
89
|
+
const byPr = groups.get(work) ?? new Map();
|
|
90
|
+
groups.set(work, byPr);
|
|
91
|
+
byPr.set(pr, [commit, ...(byPr.get(pr) ?? [])]);
|
|
92
|
+
};
|
|
93
|
+
for (const c of oldestFirst)
|
|
94
|
+
for (const work of c.work.length > 0 ? c.work : [null])
|
|
95
|
+
add(work, c.pr, c.commit);
|
|
96
|
+
const nullLast = (keys) => [...keys.filter((k) => k !== null), ...keys.filter((k) => k === null)];
|
|
97
|
+
return nullLast([...groups.keys()]).map((work) => {
|
|
98
|
+
const byPr = groups.get(work);
|
|
99
|
+
return { work, pullRequests: nullLast([...byPr.keys()]).map((pr) => ({ pr, commits: byPr.get(pr) })) };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
69
102
|
/** Read the release candidates of the member at `query.cwd`. */
|
|
70
103
|
export async function releaseCandidatesDocument(query) {
|
|
71
104
|
const head = { $schema: RELEASE_CANDIDATES_SCHEMA_ID, contract: RELEASE_CANDIDATES_CONTRACT_VERSION, chant: readerVersion() };
|
|
@@ -153,7 +186,8 @@ export async function releaseCandidatesDocument(query) {
|
|
|
153
186
|
const d = details.get(sha);
|
|
154
187
|
const { work, records } = recordTrailers(d?.trailers ?? {});
|
|
155
188
|
const merge = merges.get(sha) ?? null;
|
|
156
|
-
|
|
189
|
+
// The merge record names its pull request in `source` (../lifecycle/merge-record.ts).
|
|
190
|
+
const mergePr = mergeRecordPr(merge);
|
|
157
191
|
const subject = d?.subject ?? "";
|
|
158
192
|
return { commit: sha, subject, date: d?.date ?? "", work, records, pr: mergePr ?? prFromSubject(subject), merge };
|
|
159
193
|
};
|
|
@@ -178,13 +212,15 @@ export async function releaseCandidatesDocument(query) {
|
|
|
178
212
|
if (!tags.green.has(sha))
|
|
179
213
|
continue;
|
|
180
214
|
const own = change(sha);
|
|
215
|
+
const changes = open.map(change).reverse();
|
|
181
216
|
candidates.push({
|
|
182
217
|
commit: sha,
|
|
183
218
|
subject: own.subject,
|
|
184
219
|
date: own.date,
|
|
185
220
|
newFor: environments.filter((e) => i < newestRun.get(e.env)).map((e) => e.env),
|
|
186
221
|
cut: cutLabels.get(sha) ?? [],
|
|
187
|
-
changes
|
|
222
|
+
changes,
|
|
223
|
+
groups: groupChanges(changes),
|
|
188
224
|
flags: flagReader(open),
|
|
189
225
|
});
|
|
190
226
|
open = [];
|
|
@@ -212,6 +212,7 @@
|
|
|
212
212
|
"newFor",
|
|
213
213
|
"cut",
|
|
214
214
|
"changes",
|
|
215
|
+
"groups",
|
|
215
216
|
"flags"
|
|
216
217
|
],
|
|
217
218
|
"properties": {
|
|
@@ -245,6 +246,13 @@
|
|
|
245
246
|
},
|
|
246
247
|
"description": "The first-parent commits after the previous candidate, up to and including this one, newest first."
|
|
247
248
|
},
|
|
249
|
+
"groups": {
|
|
250
|
+
"type": "array",
|
|
251
|
+
"items": {
|
|
252
|
+
"$ref": "#/$defs/group"
|
|
253
|
+
},
|
|
254
|
+
"description": "The same changes by work item, then pull request, then commit (#4168 4.4). Work items come first, in the order the oldest change names them, then `work: null` for changes that name none. A change with two work items is under both."
|
|
255
|
+
},
|
|
248
256
|
"flags": {
|
|
249
257
|
"type": "array",
|
|
250
258
|
"items": {
|
|
@@ -254,6 +262,49 @@
|
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
},
|
|
265
|
+
"group": {
|
|
266
|
+
"type": "object",
|
|
267
|
+
"required": [
|
|
268
|
+
"work",
|
|
269
|
+
"pullRequests"
|
|
270
|
+
],
|
|
271
|
+
"properties": {
|
|
272
|
+
"work": {
|
|
273
|
+
"type": [
|
|
274
|
+
"string",
|
|
275
|
+
"null"
|
|
276
|
+
],
|
|
277
|
+
"description": "The work item id a `Chant-Record: work:<id>` trailer names, or null."
|
|
278
|
+
},
|
|
279
|
+
"pullRequests": {
|
|
280
|
+
"type": "array",
|
|
281
|
+
"items": {
|
|
282
|
+
"type": "object",
|
|
283
|
+
"required": [
|
|
284
|
+
"pr",
|
|
285
|
+
"commits"
|
|
286
|
+
],
|
|
287
|
+
"properties": {
|
|
288
|
+
"pr": {
|
|
289
|
+
"type": [
|
|
290
|
+
"integer",
|
|
291
|
+
"null"
|
|
292
|
+
],
|
|
293
|
+
"description": "The pull request, or null for commits that landed without one."
|
|
294
|
+
},
|
|
295
|
+
"commits": {
|
|
296
|
+
"type": "array",
|
|
297
|
+
"items": {
|
|
298
|
+
"type": "string"
|
|
299
|
+
},
|
|
300
|
+
"description": "Newest first."
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"description": "Pull requests in the order their oldest commit landed, then `pr: null`."
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
},
|
|
257
308
|
"change": {
|
|
258
309
|
"type": "object",
|
|
259
310
|
"required": [
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SecurityGroup } from "./security.js";
|
|
2
|
+
export declare const RELEASE_WORKFLOW_FORGES: readonly ["github", "forgejo", "gitlab"];
|
|
3
|
+
export type ReleaseWorkflowForge = (typeof RELEASE_WORKFLOW_FORGES)[number];
|
|
4
|
+
/** Where each forge's publish workflow is written, relative to the repository root. */
|
|
5
|
+
export declare const RELEASE_WORKFLOW_FILES: Record<ReleaseWorkflowForge, string>;
|
|
6
|
+
export interface ReleaseWorkflowInput {
|
|
7
|
+
forge: ReleaseWorkflowForge;
|
|
8
|
+
/** The security groups the workspace opts into at its root. */
|
|
9
|
+
groups: ReadonlySet<SecurityGroup>;
|
|
10
|
+
/** The command that installs what the build needs. Default `npm ci`. */
|
|
11
|
+
install?: string;
|
|
12
|
+
/** The command that builds what the targets upload, or none. Default `npm run build`. */
|
|
13
|
+
build?: string | null;
|
|
14
|
+
/** What runs chant. Default `npx chant`. */
|
|
15
|
+
chant?: string;
|
|
16
|
+
/** The workspace root relative to the repository root, `.` at the top. */
|
|
17
|
+
workspaceRoot?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The flags `chant release publish` gets under the groups. */
|
|
20
|
+
export declare function publishFlags(groups: ReadonlySet<SecurityGroup>): string[];
|
|
21
|
+
/** The workflow file's text, without the generated-by header. */
|
|
22
|
+
export declare function renderReleaseWorkflow(input: ReleaseWorkflowInput): string;
|
|
23
|
+
/** The header that marks the file generated, naming the command that regenerates it. */
|
|
24
|
+
export declare function releaseWorkflowHeader(command: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* `chant release workflow [--forge github|gitlab|forgejo] [--install <command>]
|
|
27
|
+
* [--build <command>|--no-build] [--chant <command>] [--output <file>]`: write
|
|
28
|
+
* the publish workflow for the workspace's forge, under its security groups.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runReleaseWorkflow(ctx: import("../cli/registry.js").CommandContext): Promise<number>;
|
|
31
|
+
//# sourceMappingURL=release-workflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-workflow.d.ts","sourceRoot":"","sources":["../../src/workspace/release-workflow.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,eAAO,MAAM,uBAAuB,YAAI,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAU,CAAC;AAChF,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,uFAAuF;AACvF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAKvE,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,oBAAoB,CAAC;IAC5B,+DAA+D;IAC/D,MAAM,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACnC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,aAAa,CAAC,GAAG,MAAM,EAAE,CAEzE;AAED,iEAAiE;AACjE,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAwDzE;AAED,wFAAwF;AACxF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,OAAO,iBAAiB,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8CvG"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant release workflow` (#4168 step 6): the publish workflow, generated
|
|
3
|
+
* the way `chant ci workflow` writes the tick, where the guides used to have
|
|
4
|
+
* one pasted (guide/release-from-workstation.mdx).
|
|
5
|
+
*
|
|
6
|
+
* The job runs on a `release/**` tag, checks the tag out, fetches the ci tags
|
|
7
|
+
* and `chant/lifecycle`, builds what the targets upload, and runs `chant
|
|
8
|
+
* release publish` for the label the tag names. The security groups decide
|
|
9
|
+
* the rest, and nothing is on without one (#4168 section 8):
|
|
10
|
+
*
|
|
11
|
+
* - `supply-chain` adds `--require-provenance`, so a run with no OIDC token
|
|
12
|
+
* fails rather than publishing without provenance.
|
|
13
|
+
* - `verified-releases` adds a `chant release verify` step before the publish.
|
|
14
|
+
*
|
|
15
|
+
* The OIDC permission (`id-token: write`, or GitLab's `id_tokens`) is always
|
|
16
|
+
* there: it costs nothing, and with it what the job uploads carries
|
|
17
|
+
* provenance whether or not the group requires it.
|
|
18
|
+
*/
|
|
19
|
+
import { GENERATED_MARKER } from "../discovery/files.js";
|
|
20
|
+
export const RELEASE_WORKFLOW_FORGES = ["github", "forgejo", "gitlab"];
|
|
21
|
+
/** Where each forge's publish workflow is written, relative to the repository root. */
|
|
22
|
+
export const RELEASE_WORKFLOW_FILES = {
|
|
23
|
+
github: ".github/workflows/chant-release.yml",
|
|
24
|
+
forgejo: ".forgejo/workflows/chant-release.yml",
|
|
25
|
+
// GitLab reads one .gitlab-ci.yml, which includes this file: `include: { local: .gitlab/chant-release.yml }`.
|
|
26
|
+
gitlab: ".gitlab/chant-release.yml",
|
|
27
|
+
};
|
|
28
|
+
/** The flags `chant release publish` gets under the groups. */
|
|
29
|
+
export function publishFlags(groups) {
|
|
30
|
+
return [...(groups.has("supply-chain") ? ["--require-provenance"] : []), "--yes"];
|
|
31
|
+
}
|
|
32
|
+
/** The workflow file's text, without the generated-by header. */
|
|
33
|
+
export function renderReleaseWorkflow(input) {
|
|
34
|
+
const chant = input.chant ?? "npx chant";
|
|
35
|
+
const install = input.install ?? "npm ci";
|
|
36
|
+
const build = input.build === undefined ? "npm run build" : input.build;
|
|
37
|
+
const root = input.workspaceRoot && input.workspaceRoot !== "." ? input.workspaceRoot : null;
|
|
38
|
+
const verify = input.groups.has("verified-releases");
|
|
39
|
+
const flags = publishFlags(input.groups).join(" ");
|
|
40
|
+
if (input.forge === "gitlab") {
|
|
41
|
+
const label = '"${CI_COMMIT_TAG#release/}"';
|
|
42
|
+
const lines = [
|
|
43
|
+
"chant-release-publish:",
|
|
44
|
+
" rules:",
|
|
45
|
+
" - if: $CI_COMMIT_TAG =~ /^release\\//",
|
|
46
|
+
" id_tokens:",
|
|
47
|
+
" SIGSTORE_ID_TOKEN:",
|
|
48
|
+
" aud: sigstore",
|
|
49
|
+
" script:",
|
|
50
|
+
...(root ? [` - cd ${root}`] : []),
|
|
51
|
+
` - ${install}`,
|
|
52
|
+
...(build ? [` - ${build}`] : []),
|
|
53
|
+
" - git fetch origin 'refs/tags/ci/*:refs/tags/ci/*'",
|
|
54
|
+
" - git fetch origin chant/lifecycle:chant/lifecycle",
|
|
55
|
+
...(verify ? [` - ${chant} release verify ${label}`] : []),
|
|
56
|
+
` - ${chant} release publish ${label} ${flags}`,
|
|
57
|
+
];
|
|
58
|
+
return `${lines.join("\n")}\n`;
|
|
59
|
+
}
|
|
60
|
+
const label = '"${GITHUB_REF_NAME#release/}"';
|
|
61
|
+
const lines = [
|
|
62
|
+
"name: chant release publish",
|
|
63
|
+
"on:",
|
|
64
|
+
" push:",
|
|
65
|
+
' tags: ["release/**"]',
|
|
66
|
+
"permissions:",
|
|
67
|
+
" contents: write",
|
|
68
|
+
" id-token: write",
|
|
69
|
+
"jobs:",
|
|
70
|
+
" publish:",
|
|
71
|
+
" runs-on: ubuntu-latest",
|
|
72
|
+
...(root ? [" defaults:", " run:", ` working-directory: ${root}`] : []),
|
|
73
|
+
" steps:",
|
|
74
|
+
" - uses: actions/checkout@v4",
|
|
75
|
+
" with:",
|
|
76
|
+
" fetch-depth: 0",
|
|
77
|
+
" - uses: actions/setup-node@v4",
|
|
78
|
+
" with:",
|
|
79
|
+
" node-version: 24",
|
|
80
|
+
" registry-url: https://registry.npmjs.org",
|
|
81
|
+
` - run: ${install}`,
|
|
82
|
+
...(build ? [` - run: ${build}`] : []),
|
|
83
|
+
" - run: git fetch origin 'refs/tags/ci/*:refs/tags/ci/*'",
|
|
84
|
+
" - run: git fetch origin chant/lifecycle:chant/lifecycle",
|
|
85
|
+
...(verify ? [` - run: ${chant} release verify ${label}`] : []),
|
|
86
|
+
` - run: ${chant} release publish ${label} ${flags}`,
|
|
87
|
+
];
|
|
88
|
+
return `${lines.join("\n")}\n`;
|
|
89
|
+
}
|
|
90
|
+
/** The header that marks the file generated, naming the command that regenerates it. */
|
|
91
|
+
export function releaseWorkflowHeader(command) {
|
|
92
|
+
return `# ${GENERATED_MARKER}. Regenerate with: ${command} (in the workspace root)\n`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `chant release workflow [--forge github|gitlab|forgejo] [--install <command>]
|
|
96
|
+
* [--build <command>|--no-build] [--chant <command>] [--output <file>]`: write
|
|
97
|
+
* the publish workflow for the workspace's forge, under its security groups.
|
|
98
|
+
*/
|
|
99
|
+
export async function runReleaseWorkflow(ctx) {
|
|
100
|
+
const { existsSync, mkdirSync, realpathSync, writeFileSync } = await import("node:fs");
|
|
101
|
+
const { dirname, join, relative, resolve, sep } = await import("node:path");
|
|
102
|
+
const { formatError, formatSuccess } = await import("../cli/format.js");
|
|
103
|
+
const { findWorkspaceRoot } = await import("../project-root.js");
|
|
104
|
+
const { gitTop } = await import("./tree.js");
|
|
105
|
+
const { securityGroupsAt } = await import("./security.js");
|
|
106
|
+
const { shellArg } = await import("./member-pipeline.js");
|
|
107
|
+
const usage = "chant release workflow [--forge github|gitlab|forgejo] [--install <command>] [--build <command>] [--chant <command>] [--output <file>]";
|
|
108
|
+
const { args } = ctx;
|
|
109
|
+
const found = findWorkspaceRoot(process.cwd());
|
|
110
|
+
if (!found) {
|
|
111
|
+
console.error(formatError({ message: "no chant.workspace.json between this directory and the git root", hint: usage }));
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const root = realpathSync(found.dir);
|
|
115
|
+
const repoRoot = gitTop(root) ?? root;
|
|
116
|
+
const forge = (args.forge ??
|
|
117
|
+
(existsSync(join(repoRoot, ".github", "workflows")) ? "github" : existsSync(join(repoRoot, ".forgejo", "workflows")) ? "forgejo" : existsSync(join(repoRoot, ".gitlab-ci.yml")) ? "gitlab" : "github"));
|
|
118
|
+
if (!RELEASE_WORKFLOW_FORGES.includes(forge)) {
|
|
119
|
+
console.error(formatError({ message: `--forge ${JSON.stringify(forge)} is not one of ${RELEASE_WORKFLOW_FORGES.join(", ")}`, hint: usage }));
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const groups = securityGroupsAt(root);
|
|
123
|
+
const workspaceRoot = relative(repoRoot, root).split(sep).join("/") || ".";
|
|
124
|
+
const text = renderReleaseWorkflow({
|
|
125
|
+
forge,
|
|
126
|
+
groups,
|
|
127
|
+
workspaceRoot,
|
|
128
|
+
...(args.ciInstall ? { install: args.ciInstall } : {}),
|
|
129
|
+
...(args.buildCommand ? { build: args.buildCommand } : {}),
|
|
130
|
+
...(args.ciChant ? { chant: args.ciChant } : {}),
|
|
131
|
+
});
|
|
132
|
+
const target = args.output ? resolve(process.cwd(), args.output) : join(repoRoot, RELEASE_WORKFLOW_FILES[forge]);
|
|
133
|
+
const parts = ["chant", "release", "workflow", "--forge", forge];
|
|
134
|
+
if (args.ciInstall)
|
|
135
|
+
parts.push("--install", args.ciInstall);
|
|
136
|
+
if (args.buildCommand)
|
|
137
|
+
parts.push("--build", args.buildCommand);
|
|
138
|
+
if (args.ciChant)
|
|
139
|
+
parts.push("--chant", args.ciChant);
|
|
140
|
+
if (args.output)
|
|
141
|
+
parts.push("--output", relative(root, target).split(sep).join("/"));
|
|
142
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
143
|
+
writeFileSync(target, releaseWorkflowHeader(parts.map(shellArg).join(" ")) + text);
|
|
144
|
+
const shown = relative(process.cwd(), target).split(sep).join("/") || target;
|
|
145
|
+
const extras = [groups.has("supply-chain") ? "requires OIDC provenance (supply-chain)" : null, groups.has("verified-releases") ? "verifies the release first (verified-releases)" : null].filter(Boolean);
|
|
146
|
+
console.log(formatSuccess(`Wrote ${shown}: publishes the release a release/** tag names${extras.length > 0 ? `, and ${extras.join(" and ")}` : ""}.`));
|
|
147
|
+
if (forge === "gitlab")
|
|
148
|
+
console.log(`Include it from .gitlab-ci.yml: include: [{ local: ${RELEASE_WORKFLOW_FILES.gitlab} }]`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
@@ -363,6 +363,36 @@
|
|
|
363
363
|
"type": "string"
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
|
+
},
|
|
367
|
+
"composed": {
|
|
368
|
+
"type": "object",
|
|
369
|
+
"required": [
|
|
370
|
+
"branch",
|
|
371
|
+
"base",
|
|
372
|
+
"sources"
|
|
373
|
+
],
|
|
374
|
+
"properties": {
|
|
375
|
+
"branch": {
|
|
376
|
+
"type": [
|
|
377
|
+
"string",
|
|
378
|
+
"null"
|
|
379
|
+
]
|
|
380
|
+
},
|
|
381
|
+
"base": {
|
|
382
|
+
"type": "string"
|
|
383
|
+
},
|
|
384
|
+
"sources": {
|
|
385
|
+
"type": "array",
|
|
386
|
+
"items": {
|
|
387
|
+
"type": "string"
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
"description": "Added by #4168 4.3, written by chant 0.124.35 and newer. On a cut of a composed commit only: its chant/compose/<name> branch, the trunk commit it was composed on, and the trunk commits picked onto that base, oldest first."
|
|
392
|
+
},
|
|
393
|
+
"channel": {
|
|
394
|
+
"type": "string",
|
|
395
|
+
"description": "Added by #4168 4.3, written by chant 0.124.35 and newer. The tag release publish moves in place of latest (an npm dist-tag, an image tag), from release cut --channel. Absent for latest."
|
|
366
396
|
}
|
|
367
397
|
}
|
|
368
398
|
},
|
package/package.json
CHANGED
|
@@ -432,7 +432,7 @@ async function runGraphLive(
|
|
|
432
432
|
// no stack carries a `src`.
|
|
433
433
|
const scopedStacks = stacks.filter((s) => s.src);
|
|
434
434
|
if (scopedStacks.length > 0) {
|
|
435
|
-
const declaredIr = await buildDeclaredPerStack(scopedStacks, projectPath);
|
|
435
|
+
const declaredIr = await buildDeclaredPerStack(scopedStacks, projectPath, (g, entities) => withLexiconEdges(g, plugins, entities));
|
|
436
436
|
ir =
|
|
437
437
|
args.overlayAnchor === "live"
|
|
438
438
|
? overlayGraphs(ir, declaredIr, overlayOpts)
|
|
@@ -455,7 +455,7 @@ async function runGraphLive(
|
|
|
455
455
|
console.error(formatWarning({ message: "overlay: a build root failed to render — showing the provisioned graph without the declared overlay" }));
|
|
456
456
|
} else {
|
|
457
457
|
await mergeGraphOps(declared.entities, declared.sourceFiles, projectPath);
|
|
458
|
-
const declaredIr = buildGraphIr(declared.entities, projectPath);
|
|
458
|
+
const declaredIr = withLexiconEdges(buildGraphIr(declared.entities, projectPath), plugins, declared.entities);
|
|
459
459
|
ir =
|
|
460
460
|
args.overlayAnchor === "live"
|
|
461
461
|
? overlayGraphs(ir, declaredIr, overlayOpts)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant components pr-apply --green` (#4168 step 9): apply on merge applies
|
|
3
|
+
* a green commit, under the approved plans of the merges since the last
|
|
4
|
+
* applied one when they cover it, and otherwise at a gate of its own on the
|
|
5
|
+
* combined digest. Git, the green target, the derivation and the gate ledger
|
|
6
|
+
* are fakes; the planning, the coverage and the apply are the real ones.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
13
|
+
import type { DriverComponent } from "../../components/driver";
|
|
14
|
+
import type { DeployContext } from "../../components/capability";
|
|
15
|
+
import type { ChangeSetDocument } from "../../change-set";
|
|
16
|
+
import type { GreenApplyRecord } from "../../lifecycle/green-apply";
|
|
17
|
+
import type { GreenTargetResult } from "../../components/green-cover";
|
|
18
|
+
import type { ParsedArgs } from "../registry";
|
|
19
|
+
|
|
20
|
+
const HEAD = "a".repeat(40);
|
|
21
|
+
const BASE = "b".repeat(40);
|
|
22
|
+
|
|
23
|
+
vi.mock("../../workspace/security", async (original) => ({
|
|
24
|
+
...(await original<typeof import("../../workspace/security")>()),
|
|
25
|
+
securityGroupsAt: () => new Set<string>(),
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("../../components/changed-files", () => ({
|
|
28
|
+
resolveCommit: async () => HEAD,
|
|
29
|
+
resolveMergeBase: async () => BASE,
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../../config", async () => {
|
|
32
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
33
|
+
return { ...actual, loadChantConfig: async () => ({ config: {} }) };
|
|
34
|
+
});
|
|
35
|
+
vi.mock("./fan-out", () => ({
|
|
36
|
+
resolveChangedUnits: async () => ({ units: { changed: ["net"], indeterminate: [] } }),
|
|
37
|
+
}));
|
|
38
|
+
// Nothing here may write or push the real repository's chant/lifecycle.
|
|
39
|
+
vi.mock("../../lifecycle/gate-change-set", () => ({
|
|
40
|
+
storeGateChangeSet: async () => ({ written: false }),
|
|
41
|
+
readGraphIds: async () => ({ ids: [] }),
|
|
42
|
+
setGraphIdReader: () => {},
|
|
43
|
+
gateGraphReader: () => async () => ({ ids: [] }),
|
|
44
|
+
}));
|
|
45
|
+
vi.mock("../../lifecycle/git", async () => {
|
|
46
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/git")>("../../lifecycle/git");
|
|
47
|
+
return { ...actual, pushLifecycleReport: async () => ({}) };
|
|
48
|
+
});
|
|
49
|
+
const applied: GreenApplyRecord[] = [];
|
|
50
|
+
vi.mock("../../lifecycle/green-apply", async () => {
|
|
51
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/green-apply")>("../../lifecycle/green-apply");
|
|
52
|
+
return { ...actual, writeGreenApply: async (r: GreenApplyRecord) => (applied.push(r), "ledger-commit") };
|
|
53
|
+
});
|
|
54
|
+
vi.mock("./run", () => ({
|
|
55
|
+
GATED_EXIT_CODE: 3,
|
|
56
|
+
autoReleaseRun: (id: string) => ({ runId: id, runOrigin: { forge: "local" } }),
|
|
57
|
+
recordAutoReleasesForRun: async () => [],
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
let gateLedger: ReturnType<typeof import("../../op/gate").memoryGateLedgerPort>;
|
|
61
|
+
vi.mock("../../op/gate", async () => {
|
|
62
|
+
const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
|
|
63
|
+
return { ...actual, gitGateLedgerPort: () => gateLedger, gateIsSealed: async () => false };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let target: GreenTargetResult;
|
|
67
|
+
let asked: Array<{ base?: string }> = [];
|
|
68
|
+
vi.mock("../../components/green-cover", async () => {
|
|
69
|
+
const actual = await vi.importActual<typeof import("../../components/green-cover")>("../../components/green-cover");
|
|
70
|
+
return { ...actual, resolveGreenTarget: async (req: { base?: string }) => (asked.push(req), target) };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** What each root's plan writes, so a test can move it. */
|
|
74
|
+
const values = new Map<string, string>();
|
|
75
|
+
const ran: string[] = [];
|
|
76
|
+
vi.mock("../../components/fan-out-support", async () => {
|
|
77
|
+
const { CapabilityRegistry } = await import("../../components/capability");
|
|
78
|
+
const { planFanOut } = await import("../../components/fan-out");
|
|
79
|
+
const { computePlanDigest } = await import("../../lifecycle/plan-digest");
|
|
80
|
+
const registry = new CapabilityRegistry();
|
|
81
|
+
registry.register({
|
|
82
|
+
kind: "fake-root",
|
|
83
|
+
async plan(_ctx: DeployContext, input: { root: string }) {
|
|
84
|
+
const value = values.get(input.root) ?? "1";
|
|
85
|
+
const planDigest = computePlanDigest("fake-root", { root: input.root, value });
|
|
86
|
+
return {
|
|
87
|
+
member: input.root,
|
|
88
|
+
planDigest,
|
|
89
|
+
artifact: { planDigest },
|
|
90
|
+
changeSet: {
|
|
91
|
+
member: { member: input.root, lexicon: "terraform", planner: "tofu", status: "planned", planDigest, holes: [] },
|
|
92
|
+
entries: [{ member: input.root, lexicon: "terraform", planner: "tofu", address: `${input.root}.main`, type: "fake", action: "update", attributes: [{ path: "value", after: value }] }],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async outputs() {
|
|
97
|
+
return {};
|
|
98
|
+
},
|
|
99
|
+
async run(_ctx: DeployContext, input: { root: string }) {
|
|
100
|
+
ran.push(input.root);
|
|
101
|
+
return { outputs: {} };
|
|
102
|
+
},
|
|
103
|
+
} as never);
|
|
104
|
+
return {
|
|
105
|
+
fanOutRegistry: async () => registry,
|
|
106
|
+
deriveFanOut: async () => ({
|
|
107
|
+
success: true,
|
|
108
|
+
components: ESTATE,
|
|
109
|
+
plan: planFanOut({ components: ESTATE, changed: ["net"] }),
|
|
110
|
+
signal: { changed: ["net"], indeterminate: [], unclaimed: [] },
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const root = (name: string, dependsOn: string[] = []): DriverComponent => ({
|
|
116
|
+
name,
|
|
117
|
+
dependsOn,
|
|
118
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "fake-root", root: name }] }],
|
|
119
|
+
});
|
|
120
|
+
const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"])];
|
|
121
|
+
|
|
122
|
+
const { memoryGateLedgerPort } = await import("../../op/gate");
|
|
123
|
+
const { runComponentsPrApply, runComponentsPrPlan } = await import("./pr");
|
|
124
|
+
const { composeChangeSet } = await import("../../change-set");
|
|
125
|
+
|
|
126
|
+
let dir: string;
|
|
127
|
+
let errors: string[];
|
|
128
|
+
|
|
129
|
+
const ctx = (a: Partial<ParsedArgs>) =>
|
|
130
|
+
({ args: { command: "components", path: "pr-apply", format: "", fix: false, watch: false, verbose: false, help: false, live: false, base: "main", env: "prod", output: dir, ...a } as ParsedArgs, plugins: [] }) as never;
|
|
131
|
+
|
|
132
|
+
beforeEach(async () => {
|
|
133
|
+
dir = await mkdtemp(join(tmpdir(), "pr-green-"));
|
|
134
|
+
errors = [];
|
|
135
|
+
applied.length = 0;
|
|
136
|
+
ran.length = 0;
|
|
137
|
+
values.clear();
|
|
138
|
+
asked = [];
|
|
139
|
+
gateLedger = memoryGateLedgerPort();
|
|
140
|
+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
141
|
+
vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.map(String).join(" ")); });
|
|
142
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterEach(async () => {
|
|
146
|
+
vi.restoreAllMocks();
|
|
147
|
+
await rm(dir, { recursive: true, force: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** The change set a pull request's plan made with the values set now. */
|
|
151
|
+
async function planned(): Promise<ChangeSetDocument> {
|
|
152
|
+
await runComponentsPrPlan(ctx({ pr: 12, path: "pr-plan" }));
|
|
153
|
+
return JSON.parse(await readFile(join(dir, "change-set.json"), "utf8")) as ChangeSetDocument;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const greenTarget = (approved: Array<{ doc: ChangeSetDocument; pr: number; approvedBy: string[] }>): GreenTargetResult => ({
|
|
157
|
+
status: "target",
|
|
158
|
+
target: {
|
|
159
|
+
commit: HEAD,
|
|
160
|
+
base: BASE,
|
|
161
|
+
baseFrom: "applied",
|
|
162
|
+
merges: [BASE.replace(/b/g, "c"), HEAD],
|
|
163
|
+
prs: approved.map((m) => m.pr),
|
|
164
|
+
approved: approved.map((m, i) => ({ commit: i === approved.length - 1 ? HEAD : BASE.replace(/b/g, "c"), ...m })),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const OP = `green-${HEAD.slice(0, 12)}`;
|
|
168
|
+
|
|
169
|
+
describe("pr-apply --green (#4168 step 9)", () => {
|
|
170
|
+
test("the merges' approved plans cover the plan at the green commit: it applies under them, with no gate, and records the commit applied", async () => {
|
|
171
|
+
const doc = await planned();
|
|
172
|
+
target = greenTarget([{ doc, pr: 12, approvedBy: ["github:alice"] }]);
|
|
173
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(0);
|
|
174
|
+
expect(ran).toEqual(["net", "app"]);
|
|
175
|
+
expect(gateLedger.appended).toEqual([]);
|
|
176
|
+
expect(applied).toEqual([expect.objectContaining({ env: "prod", commit: HEAD, base: BASE, approval: "merges", approvedBy: ["github:alice"], digest: doc.digest })]);
|
|
177
|
+
const report = JSON.parse(await readFile(join(dir, "pr-apply.json"), "utf8"));
|
|
178
|
+
expect(report).toMatchObject({ op: OP, pr: null, status: "applied", approval: { status: "approved", approvedBy: ["github:alice"] } });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("two merges, each approving one member, cover the plan together (the case one pull request's apply stopped at plan-changed)", async () => {
|
|
182
|
+
values.set("app", "1");
|
|
183
|
+
const first = await planned();
|
|
184
|
+
values.set("app", "2");
|
|
185
|
+
const both = await planned();
|
|
186
|
+
// #13 touched app alone, so its plan holds app alone.
|
|
187
|
+
const second = composeChangeSet([{ member: both.members.find((m) => m.member === "app")!, entries: both.entries.filter((e) => e.member === "app") }]);
|
|
188
|
+
// #12 approved net and app at 1; #13 approved app at 2; the green commit plans net at 1 and app at 2.
|
|
189
|
+
target = greenTarget([
|
|
190
|
+
{ doc: first, pr: 12, approvedBy: ["github:alice"] },
|
|
191
|
+
{ doc: second, pr: 13, approvedBy: ["github:bob"] },
|
|
192
|
+
]);
|
|
193
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(0);
|
|
194
|
+
expect(ran).toEqual(["net", "app"]);
|
|
195
|
+
expect(applied[0]).toMatchObject({ approval: "merges", approvedBy: ["github:alice", "github:bob"] });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("a plan the merges did not approve waits at the green commit's own gate on the combined digest, then applies once that is approved", async () => {
|
|
199
|
+
const doc = await planned();
|
|
200
|
+
values.set("net", "9");
|
|
201
|
+
target = greenTarget([{ doc, pr: 12, approvedBy: ["github:alice"] }]);
|
|
202
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(3);
|
|
203
|
+
expect(ran).toEqual([]);
|
|
204
|
+
expect(applied).toEqual([]);
|
|
205
|
+
const refused = JSON.parse(await readFile(join(dir, "pr-apply.json"), "utf8"));
|
|
206
|
+
expect(refused).toMatchObject({ op: OP, status: "refused", refusal: "not-approved" });
|
|
207
|
+
expect(refused.message).toContain("do not cover it: net now writes net.main.value to a different value than the approved plan (checked against #12)");
|
|
208
|
+
expect(refused.message).toContain(`chant approve ${OP} pr-apply --plan ${refused.digest}`);
|
|
209
|
+
|
|
210
|
+
gateLedger = memoryGateLedgerPort({
|
|
211
|
+
resolutions: [{ version: 1, kind: "resolution", op: OP, gate: "pr-apply", resolvedBy: "github:carol", timestamp: new Date().toISOString(), planDigest: refused.digest }],
|
|
212
|
+
});
|
|
213
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(0);
|
|
214
|
+
expect(ran).toEqual(["net", "app"]);
|
|
215
|
+
expect(applied[0]).toMatchObject({ approval: "gate", approvedBy: ["github:carol"], digest: refused.digest });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("the green commit last applied is nothing to do, and --green takes no --pr", async () => {
|
|
219
|
+
target = { status: "applied", commit: HEAD };
|
|
220
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(0);
|
|
221
|
+
expect(ran).toEqual([]);
|
|
222
|
+
expect(errors.join("\n")).toContain("is the green commit last applied to prod");
|
|
223
|
+
expect(await runComponentsPrApply(ctx({ green: true, pr: 12 }))).toBe(1);
|
|
224
|
+
target = { status: "error", message: "abc is not a green commit", hint: "check it out" };
|
|
225
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(1);
|
|
226
|
+
expect(errors.join("\n")).toContain("abc is not a green commit");
|
|
227
|
+
// --base reaches the target as the first apply's fallback.
|
|
228
|
+
expect(asked.at(-1)).toMatchObject({ base: "main" });
|
|
229
|
+
});
|
|
230
|
+
});
|