@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
|
@@ -221,6 +221,12 @@ export interface CiGreen {
|
|
|
221
221
|
phases: CiPhase[];
|
|
222
222
|
/** The phases a commit must pass, by name, in file order. */
|
|
223
223
|
require: string[];
|
|
224
|
+
/**
|
|
225
|
+
* The phases that must pass before the branch moves (#4168 4.4), by name,
|
|
226
|
+
* in file order: the quick checks a pull request, a merge queue or a local
|
|
227
|
+
* run gives before a change lands. Absent or empty when the declaration names none.
|
|
228
|
+
*/
|
|
229
|
+
preMerge?: string[];
|
|
224
230
|
/**
|
|
225
231
|
* The full sha of the first commit on the branch whose ci tags need a
|
|
226
232
|
* statement signed by a runner key (#4031). A tag on a strict
|
|
@@ -244,9 +250,20 @@ export interface CiGreen {
|
|
|
244
250
|
/** Where `ci tick` reads phase results (#4044): forge check runs, or chant Op runs. */
|
|
245
251
|
export type CiSource = "checks" | "runs";
|
|
246
252
|
|
|
253
|
+
/**
|
|
254
|
+
* A prescribed delivery workflow (#4168 section 7). `trunk-curated`: a merge
|
|
255
|
+
* records the plan and deploys nothing, and releases are cut from green
|
|
256
|
+
* commits and promoted through the environments. `trunk-curated+apply-on-merge`
|
|
257
|
+
* adds the apply on merge as an opt-in. `chant workspace check` reports what a
|
|
258
|
+
* member under one still lacks (./checks/ci-workflow.ts, WSP151).
|
|
259
|
+
*/
|
|
260
|
+
export type CiWorkflow = "trunk-curated" | "trunk-curated+apply-on-merge";
|
|
261
|
+
|
|
247
262
|
/** The declaration's `ci` block (#3573). */
|
|
248
263
|
export interface CiDeclaration {
|
|
249
264
|
green: CiGreen | null;
|
|
265
|
+
/** `ci.workflow` (#4168 step 6): the workflow every member follows unless it names its own; absent when the declaration sets none. */
|
|
266
|
+
workflow?: CiWorkflow;
|
|
250
267
|
/**
|
|
251
268
|
* `ci.reportUrl` (#4160): where the pull-request jobs copy their reports
|
|
252
269
|
* and `chant ci run` keeps the run index, `<reportUrl>/runs/index.json`.
|
|
@@ -573,6 +590,8 @@ export interface Member {
|
|
|
573
590
|
travel: boolean;
|
|
574
591
|
/** The security groups this member adds to the declaration's top-level `security` (#4168 step 7), in file order; empty when it names none. */
|
|
575
592
|
security: SecurityGroup[];
|
|
593
|
+
/** The member's own `ci.workflow` (#4168 step 6), which wins over the top level's; null when it names none. */
|
|
594
|
+
ciWorkflow: CiWorkflow | null;
|
|
576
595
|
suppress: Suppression[];
|
|
577
596
|
/** The entry's JSON Pointer in the file, for messages. */
|
|
578
597
|
pointer: string;
|
|
@@ -869,6 +888,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
|
|
|
869
888
|
because: (e.because as string | undefined) ?? null,
|
|
870
889
|
travel: e.travel === true,
|
|
871
890
|
security: [...((e.security as SecurityGroup[] | undefined) ?? [])],
|
|
891
|
+
ciWorkflow: ((e.ci as { workflow?: CiWorkflow } | undefined)?.workflow) ?? null,
|
|
872
892
|
suppress,
|
|
873
893
|
pointer,
|
|
874
894
|
};
|
|
@@ -1116,7 +1136,8 @@ function retentionOf(raw: unknown): RetentionOverride | null {
|
|
|
1116
1136
|
|
|
1117
1137
|
function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocation): CiDeclaration | null {
|
|
1118
1138
|
if (raw === undefined) return null;
|
|
1119
|
-
const block = raw as { reportUrl?: string; green?: { branch: string; window?: string; phases: Record<string, string[] | { runs: string[]; skipped?: CiSkipped; build?: CiBuild }>; require: string[]; signedSince?: string; source?: CiSource } };
|
|
1139
|
+
const block = raw as { reportUrl?: string; workflow?: CiWorkflow; green?: { branch: string; window?: string; phases: Record<string, string[] | { runs: string[]; skipped?: CiSkipped; build?: CiBuild }>; require: string[]; preMerge?: string[]; signedSince?: string; source?: CiSource } };
|
|
1140
|
+
const workflow = block.workflow !== undefined ? { workflow: block.workflow } : {};
|
|
1120
1141
|
let reportUrl: string | null = null;
|
|
1121
1142
|
if (block.reportUrl !== undefined) {
|
|
1122
1143
|
// #4160: refused here as pr-plan and ci run refuse the flag, so a reader never gets a URL no job can write to.
|
|
@@ -1124,7 +1145,7 @@ function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocatio
|
|
|
1124
1145
|
if ("error" in target) throw new WorkspaceReadError("declaration-invalid", target.error, at("/ci/reportUrl"));
|
|
1125
1146
|
reportUrl = target.url;
|
|
1126
1147
|
}
|
|
1127
|
-
if (block.green === undefined) return { green: null, reportUrl };
|
|
1148
|
+
if (block.green === undefined) return { green: null, ...workflow, reportUrl };
|
|
1128
1149
|
const g = block.green;
|
|
1129
1150
|
const phases: CiPhase[] = Object.entries(g.phases).map(([name, p]) => ({
|
|
1130
1151
|
name,
|
|
@@ -1141,7 +1162,15 @@ function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocatio
|
|
|
1141
1162
|
at(`/ci/green/require/${i}`),
|
|
1142
1163
|
);
|
|
1143
1164
|
}
|
|
1144
|
-
|
|
1165
|
+
for (const [i, name] of (g.preMerge ?? []).entries()) {
|
|
1166
|
+
if (phases.some((p) => p.name === name)) continue;
|
|
1167
|
+
throw new WorkspaceReadError(
|
|
1168
|
+
"declaration-invalid",
|
|
1169
|
+
`ci.green.preMerge names the phase ${JSON.stringify(name)}, which ci.green.phases does not declare; declared phases: ${phases.map((p) => p.name).join(", ")}`,
|
|
1170
|
+
at(`/ci/green/preMerge/${i}`),
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return { green: { branch: g.branch, window: g.window ?? DEFAULT_CI_WINDOW, phases, require: [...g.require], ...(g.preMerge !== undefined ? { preMerge: [...g.preMerge] } : {}), signedSince: g.signedSince ?? null, source: g.source ?? "checks", pointer: "/ci/green" }, ...workflow, reportUrl };
|
|
1145
1174
|
}
|
|
1146
1175
|
|
|
1147
1176
|
function agentsOf(raw: unknown, members: Member[], at: (pointer: string, key?: boolean) => ErrorLocation): AgentDeclaration[] {
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* resolutions, for a deploy that names its release only through its
|
|
16
16
|
* admission (`releaseOfDeploy`, #4043);
|
|
17
17
|
* - the counted `ci/revoked/<sha>` tags, judged as `release cut` judges them
|
|
18
|
-
* (`readRevokedCommits`, #4033), and each commit's committer time from git
|
|
18
|
+
* (`readRevokedCommits`, #4033), and each commit's committer time from git;
|
|
19
|
+
* a composed commit (#4168 4.3) takes the earliest time of the trunk
|
|
20
|
+
* commits it carries.
|
|
19
21
|
*
|
|
20
22
|
* Outside a workspace the project's own ledger is read as one member, `null`.
|
|
21
23
|
* It reads the local branch and the local tags, and never fetches.
|
|
@@ -139,11 +141,18 @@ export async function deliveryMetrics(query: MetricsQuery): Promise<DeliveryMetr
|
|
|
139
141
|
const { readRevokedCommits } = await import("../lifecycle/release-revocation");
|
|
140
142
|
const revoked = await readRevokedCommits(root);
|
|
141
143
|
|
|
144
|
+
const { compositionOf } = await import("../lifecycle/compose-refs");
|
|
142
145
|
const commitTimes = new Map<string, number | null>();
|
|
146
|
+
const ownTime = (sha: string): number | null => {
|
|
147
|
+
const iso = tryGit(top, ["show", "-s", "--format=%cI", `${sha}^{commit}`]);
|
|
148
|
+
return iso ? Date.parse(iso) : null;
|
|
149
|
+
};
|
|
143
150
|
const commitTime = (sha: string): number | null => {
|
|
144
151
|
if (!commitTimes.has(sha)) {
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
// #4168 4.3: a composed commit's lead time runs from the earliest trunk commit it carries.
|
|
153
|
+
const sources = compositionOf(top, sha)?.sources ?? [];
|
|
154
|
+
const times = sources.map(ownTime).filter((t): t is number => t !== null);
|
|
155
|
+
commitTimes.set(sha, times.length > 0 ? Math.min(...times) : ownTime(sha));
|
|
147
156
|
}
|
|
148
157
|
return commitTimes.get(sha)!;
|
|
149
158
|
};
|
|
@@ -221,10 +221,11 @@ describe("chant workspace graph on the chant repo (#2557)", () => {
|
|
|
221
221
|
// reference workspace runs its own chant workspace graph (#2551);
|
|
222
222
|
// read-contract.test.ts covers that. Here both are left out, so nothing runs.
|
|
223
223
|
const notRun = (JSON.parse(readFileSync(join(REPO, "chant.workspace.json"), "utf-8")) as { members: { name: string; kind: string }[] }).members
|
|
224
|
-
|
|
224
|
+
// npm (release/, #4040) is the one kind chant member; leaving it out keeps this a read with nothing to build.
|
|
225
|
+
.filter((m) => m.kind !== "terraform" && m.kind !== "choudoufu" && m.kind !== "workspace" && m.kind !== "chant")
|
|
225
226
|
.map((m) => m.name);
|
|
226
227
|
|
|
227
|
-
test("validates, and lists every member but the terraform one as skipped:
|
|
228
|
+
test("validates, and lists every member but the terraform one and npm as skipped: no other is kind chant", async () => {
|
|
228
229
|
const { doc, failed } = await workspaceGraph({ cwd: join(REPO, "packages", "core"), members: notRun });
|
|
229
230
|
const g = result(doc);
|
|
230
231
|
expectValid(g);
|
package/src/workspace/init.ts
CHANGED
|
@@ -559,7 +559,7 @@ export async function runWorkspaceInit(ctx: CommandContext): Promise<number> {
|
|
|
559
559
|
console.error(
|
|
560
560
|
formatError({
|
|
561
561
|
message: `${join(root, existing[0])} already exists`,
|
|
562
|
-
hint: "Edit it by hand, or run chant workspace ls to see what it declares.",
|
|
562
|
+
hint: "Edit it by hand, or run chant workspace ls to see what it declares. To set it up for a delivery workflow, run chant ci init --workflow trunk-curated.",
|
|
563
563
|
}),
|
|
564
564
|
);
|
|
565
565
|
return 1;
|
|
@@ -612,7 +612,7 @@ export async function runWorkspaceInit(ctx: CommandContext): Promise<number> {
|
|
|
612
612
|
return 0;
|
|
613
613
|
}
|
|
614
614
|
writeFileSync(target, text, { flag: "wx" });
|
|
615
|
-
console.log(formatSuccess(`Wrote ${target}. chant workspace ls lists what it declares.`));
|
|
615
|
+
console.log(formatSuccess(`Wrote ${target}. chant workspace ls lists what it declares, and chant ci init --workflow trunk-curated sets up merging and releases.`));
|
|
616
616
|
return 0;
|
|
617
617
|
}
|
|
618
618
|
|
|
@@ -112,22 +112,22 @@ describe("resolveToolchain", () => {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
describe("the chant repo's own declaration (#2557)", () => {
|
|
115
|
-
test("
|
|
115
|
+
test("build runs the one chant member, npm (release/, #4040), and the example groups under one toolchain", () => {
|
|
116
116
|
const plan = planMembers("build", REPO);
|
|
117
117
|
expect(plan.workspace.name).toBe("chant");
|
|
118
118
|
expect(plan.unreadable).toEqual([]);
|
|
119
119
|
expect(plan.groups).toHaveLength(1);
|
|
120
120
|
const units = plan.groups[0].units;
|
|
121
|
-
expect(units.
|
|
121
|
+
expect(units.filter((u) => !u.group).map((u) => u.id)).toEqual(["npm"]);
|
|
122
122
|
expect(units.map((u) => u.id)).toContain("fixtures:test/leftness");
|
|
123
123
|
expect(plan.skipped.every((s) => s.reason.code === "kind-not-run")).toBe(true);
|
|
124
124
|
expect(plan.skipped.map((s) => s.name)).toContain("core");
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
test("graph and audit run
|
|
127
|
+
test("graph and audit run only npm there: every other member is kind other", () => {
|
|
128
128
|
for (const verb of ["graph", "audit"] as const) {
|
|
129
129
|
const plan = planMembers(verb, REPO);
|
|
130
|
-
expect(plan.groups, verb).toEqual([]);
|
|
130
|
+
expect(plan.groups.flatMap((g) => g.units.map((u) => u.id)), verb).toEqual(["npm"]);
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
@@ -164,6 +164,13 @@ describe("member command lines", () => {
|
|
|
164
164
|
expect(memberArgv("graph", unit, args({ env: "prod" }))).toEqual(["graph", ".", "--format", "ir", "--env", "prod"]);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
test("graph hands each member the detail tier (#4228)", () => {
|
|
168
|
+
expect(memberArgv("graph", unit, args({ detail: 1, live: true, overlay: true, env: "local" }))).toEqual([
|
|
169
|
+
"graph", ".", "--format", "ir", "--env", "local", "--live", "--overlay", "--detail", "1",
|
|
170
|
+
]);
|
|
171
|
+
expect(memberArgv("graph", unit, args({ detail: 0 }))).toEqual(["graph", ".", "--format", "ir", "--detail", "0"]);
|
|
172
|
+
});
|
|
173
|
+
|
|
167
174
|
test("a reader unit takes --env only with --live, which needs one (#2874, #2875)", () => {
|
|
168
175
|
const reader = { ...unit, reader: { lexicon: "terraform", config: {}, packageDir: "/p" } } as typeof unit;
|
|
169
176
|
expect(memberArgv("graph", reader, args({ env: "prod" }))).toEqual(["graph", ".", "--format", "ir"]);
|
|
@@ -305,6 +305,9 @@ export function memberArgv(verb: WorkspaceVerb, unit: RunUnit, args: ParsedArgs)
|
|
|
305
305
|
if (args.live) argv.push("--live");
|
|
306
306
|
if (args.overlay) argv.push("--overlay");
|
|
307
307
|
if (args.traffic) argv.push("--traffic", args.traffic);
|
|
308
|
+
// The detail tier too, so `workspace graph --detail 1` composes collapsed
|
|
309
|
+
// composites with their rolled-up status (#4228).
|
|
310
|
+
if (args.detail !== undefined) argv.push("--detail", String(args.detail));
|
|
308
311
|
}
|
|
309
312
|
return argv;
|
|
310
313
|
}
|
|
@@ -69,6 +69,10 @@ describe("readNestedDocument", () => {
|
|
|
69
69
|
"workspace", "graph", "/w/inner", "--at", "abc", "--env", "dev", "--live", "--overlay", "--traffic", "10 rps", "--no-cache",
|
|
70
70
|
]);
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
test("passes the detail tier on (#4228)", () => {
|
|
74
|
+
expect(nestedArgv("/w/inner", { detail: 1 }, null)).toEqual(["workspace", "graph", "/w/inner", "--detail", "1"]);
|
|
75
|
+
});
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
describe("foldNested", () => {
|
|
@@ -43,6 +43,7 @@ export function nestedArgv(dir: string, args: Partial<ParsedArgs>, at: string |
|
|
|
43
43
|
...(args.live ? ["--live"] : []),
|
|
44
44
|
...(args.overlay ? ["--overlay"] : []),
|
|
45
45
|
...(args.traffic !== undefined ? ["--traffic", String(args.traffic)] : []),
|
|
46
|
+
...(args.detail !== undefined ? ["--detail", String(args.detail)] : []),
|
|
46
47
|
...(args.noCache ? ["--no-cache"] : []),
|
|
47
48
|
];
|
|
48
49
|
}
|
|
@@ -29,6 +29,7 @@ import { WORK_WARNING_CODES } from "./work";
|
|
|
29
29
|
import { BOX_FINDING_CODES } from "./checks/boxes";
|
|
30
30
|
import { DIAGRAM_FINDING_CODES } from "./checks/diagrams";
|
|
31
31
|
import { SECURITY_FINDING_CODES } from "./checks/security";
|
|
32
|
+
import { CI_WORKFLOW_FINDING_CODES } from "./ci-workflow";
|
|
32
33
|
import { WORK_ERROR_CODES, WORK_HISTORY_ERROR_CODES, WORK_LEASE_REFUSALS } from "./work-cli";
|
|
33
34
|
import { RUN_STATEMENT_ERROR_CODES, RUNS_ERROR_CODES, RUNS_REASON_CODES, RUNS_WRITE_ERROR_CODES } from "./runs-cli";
|
|
34
35
|
import { RUN_STATEMENT_FAILURE_CODES } from "./trust/run-statement";
|
|
@@ -68,6 +69,7 @@ const PER_COMMAND: Record<string, readonly string[]> = {
|
|
|
68
69
|
BOX_FINDING_CODES,
|
|
69
70
|
DIAGRAM_FINDING_CODES,
|
|
70
71
|
SECURITY_FINDING_CODES,
|
|
72
|
+
CI_WORKFLOW_FINDING_CODES,
|
|
71
73
|
STATUS_REASON_CODES,
|
|
72
74
|
STATUS_ERROR_CODES,
|
|
73
75
|
STATUS_GATE_REASON_CODES,
|
|
@@ -247,6 +247,16 @@ export const REASONS = {
|
|
|
247
247
|
"security-runner-start-no-key": "Under isolated-runners, .chant/trust.json lists no key under runners for a provisioner to sign runner-start statements with.",
|
|
248
248
|
"security-runner-start-no-role": "Under isolated-runners, no principal under runners in .chant/trust.json holds the runner role.",
|
|
249
249
|
"security-trust-unreadable": "Under isolated-runners, .chant/trust.json can't be read, so no runner-start statement can be checked.",
|
|
250
|
+
// The ci.workflow requirements (check, #4168 step 6). Each is also a WSP151 finding.
|
|
251
|
+
"ci-workflow-green-undeclared": "Under ci.workflow, the declaration has no ci.green block, so no commit gets a verdict and no release can be cut.",
|
|
252
|
+
"ci-workflow-no-pre-merge": "Under ci.workflow, ci.green.preMerge names no phase, so nothing says which checks run before the branch moves.",
|
|
253
|
+
"ci-workflow-apply-in-green": "Under ci.workflow, a phase in ci.green.require matches an apply job, which would wait on its own verdict.",
|
|
254
|
+
"ci-workflow-no-components": "Under ci.workflow, a member declares no component, so nothing of it can be cut or promoted.",
|
|
255
|
+
"ci-workflow-no-environments": "Under ci.workflow, a member's chant.config.ts declares no environments, so a release has nowhere to be promoted to.",
|
|
256
|
+
"ci-workflow-ungated-env": "Under ci.workflow, a component has no gate, so a promote to the last environment waits on no approval.",
|
|
257
|
+
"ci-workflow-pipeline-missing": "Under ci.workflow, no pipeline file runs the plan job, the merge job or chant ci tick.",
|
|
258
|
+
"ci-workflow-pipeline-differs": "Under ci.workflow, the merge job does not match the workflow: it applies under trunk-curated, or records under trunk-curated+apply-on-merge.",
|
|
259
|
+
"ci-workflow-publish-missing": "Under ci.workflow, no pipeline file runs chant release publish.",
|
|
250
260
|
// A box that holds a credential (check, #2726). Each is also a WSP finding.
|
|
251
261
|
"box-credential-declared": "A file in a box member's directory carries a literal secret: a credential's shape, or a literal where a credential goes. A variable or secret-manager reference is not one.",
|
|
252
262
|
"box-capability-unbrokered": "A capability in a member's box block names no broker, so the box would hold its credential.",
|
|
@@ -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": [
|
|
@@ -11,7 +11,7 @@ import { writeFileSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { contract } from "./__fixtures__/contract-repo";
|
|
13
13
|
import candidatesSchema from "./release-candidates.schema.json";
|
|
14
|
-
import { prFromSubject, recordTrailers, releaseCandidatesDocument, RELEASE_CANDIDATES_SCHEMA_ID } from "./release-candidates";
|
|
14
|
+
import { groupChanges, mergeRecordPr, prFromSubject, recordTrailers, releaseCandidatesDocument, RELEASE_CANDIDATES_SCHEMA_ID, type CandidateChange } from "./release-candidates";
|
|
15
15
|
import { appendCut, makeRelease, persistRelease } from "../lifecycle/release-store";
|
|
16
16
|
import { appendReleaseRecord } from "../lifecycle/release-ledger";
|
|
17
17
|
import { writeLedgerFiles } from "../lifecycle/git";
|
|
@@ -46,6 +46,33 @@ describe("recordTrailers and prFromSubject", () => {
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
describe("groupChanges (#4168 4.4)", () => {
|
|
50
|
+
const change = (commit: string, work: string[], pr: number | null): CandidateChange => ({ commit, subject: commit, date: "", work, records: work.map((w) => `work:${w}`), pr, merge: null });
|
|
51
|
+
|
|
52
|
+
test("groups by work item, then pull request, then commit, with the ungrouped last", () => {
|
|
53
|
+
// Newest first, as a candidate's changes are.
|
|
54
|
+
const groups = groupChanges([
|
|
55
|
+
change("f", [], null),
|
|
56
|
+
change("e", ["w-2"], null),
|
|
57
|
+
change("d", ["w-1"], 4),
|
|
58
|
+
change("c", [], 3),
|
|
59
|
+
change("b", ["w-1", "w-2"], 2),
|
|
60
|
+
change("a", ["w-1"], 2),
|
|
61
|
+
]);
|
|
62
|
+
expect(groups).toEqual([
|
|
63
|
+
{ work: "w-1", pullRequests: [{ pr: 2, commits: ["b", "a"] }, { pr: 4, commits: ["d"] }] },
|
|
64
|
+
{ work: "w-2", pullRequests: [{ pr: 2, commits: ["b"] }, { pr: null, commits: ["e"] }] },
|
|
65
|
+
{ work: null, pullRequests: [{ pr: 3, commits: ["c"] }, { pr: null, commits: ["f"] }] },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a merge record names its pull request in source", () => {
|
|
70
|
+
expect(mergeRecordPr({ source: { kind: "pull-request", pr: 12 } })).toBe(12);
|
|
71
|
+
expect(mergeRecordPr({ source: { kind: "direct-push" } })).toBeNull();
|
|
72
|
+
expect(mergeRecordPr(null)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
49
76
|
describe("releaseCandidatesDocument", () => {
|
|
50
77
|
test("partitions the walk at green commits, skips revoked ones, and says which environments each is new for", async () => {
|
|
51
78
|
await withTestDir(async (dir) => {
|
|
@@ -63,7 +90,7 @@ describe("releaseCandidatesDocument", () => {
|
|
|
63
90
|
{ cwd: dir },
|
|
64
91
|
);
|
|
65
92
|
// A merge record for d names its pull request.
|
|
66
|
-
await writeLedgerFiles([{ path: `_merges/${d}.json`, content: JSON.stringify({ pr: 40, approvedDigest: "sha256:x" }) }], "merge record", { cwd: dir });
|
|
93
|
+
await writeLedgerFiles([{ path: `_merges/${d}.json`, content: JSON.stringify({ source: { kind: "pull-request", pr: 40 }, approvedDigest: "sha256:x" }) }], "merge record", { cwd: dir });
|
|
67
94
|
|
|
68
95
|
const doc = await releaseCandidatesDocument({ cwd: dir });
|
|
69
96
|
expectValid(doc);
|
|
@@ -78,7 +105,7 @@ describe("releaseCandidatesDocument", () => {
|
|
|
78
105
|
expect(doc.candidates.map((x) => x.commit)).toEqual([d, b, init]);
|
|
79
106
|
expect(doc.candidates[0]).toMatchObject({ newFor: ["staging", "prod"], cut: [] });
|
|
80
107
|
expect(doc.candidates[0].changes.map((x) => x.commit)).toEqual([d, c]);
|
|
81
|
-
expect(doc.candidates[0].changes[0]).toMatchObject({ pr: 40, merge: { pr: 40, approvedDigest: "sha256:x" } });
|
|
108
|
+
expect(doc.candidates[0].changes[0]).toMatchObject({ pr: 40, merge: { source: { kind: "pull-request", pr: 40 }, approvedDigest: "sha256:x" } });
|
|
82
109
|
expect(doc.candidates[1].changes.map((x) => x.commit)).toEqual([b, a]);
|
|
83
110
|
expect(doc.candidates[1].changes[0].work).toEqual(["w-7"]);
|
|
84
111
|
expect(doc.candidates[1].changes[1].pr).toBe(1);
|
|
@@ -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
|
*/
|
|
@@ -76,6 +81,20 @@ export interface CandidateChange {
|
|
|
76
81
|
merge: Record<string, unknown> | null;
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
/** A candidate's changes under one pull request, or under none (`pr: null`) for commits that landed without one. */
|
|
85
|
+
export interface CandidatePullRequestGroup {
|
|
86
|
+
pr: number | null;
|
|
87
|
+
/** Newest first. */
|
|
88
|
+
commits: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A candidate's changes under one work item, or under none (`work: null`). Work items come first, in the order the oldest change names them. */
|
|
92
|
+
export interface CandidateGroup {
|
|
93
|
+
work: string | null;
|
|
94
|
+
/** Pull requests in the order their oldest commit landed, then `pr: null`. */
|
|
95
|
+
pullRequests: CandidatePullRequestGroup[];
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
export interface CandidateFlag {
|
|
80
99
|
id: string;
|
|
81
100
|
member: string;
|
|
@@ -95,6 +114,8 @@ export interface Candidate {
|
|
|
95
114
|
/** The labels of releases already cut from it. */
|
|
96
115
|
cut: string[];
|
|
97
116
|
changes: CandidateChange[];
|
|
117
|
+
/** The same changes by work item, then pull request, then commit (#4168 4.4). */
|
|
118
|
+
groups: CandidateGroup[];
|
|
98
119
|
flags: CandidateFlag[];
|
|
99
120
|
}
|
|
100
121
|
|
|
@@ -156,6 +177,34 @@ export function prFromSubject(subject: string): number | null {
|
|
|
156
177
|
return m ? Number(m[1]) : null;
|
|
157
178
|
}
|
|
158
179
|
|
|
180
|
+
/** The pull request a merge record names in its `source`, or null. */
|
|
181
|
+
export function mergeRecordPr(merge: Record<string, unknown> | null): number | null {
|
|
182
|
+
const source = merge?.source as { kind?: unknown; pr?: unknown } | undefined;
|
|
183
|
+
return source?.kind === "pull-request" && typeof source.pr === "number" ? source.pr : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Group changes, given newest first, by work item, then pull request, then
|
|
188
|
+
* commit. A change with work items lands in each one's group; the rest land
|
|
189
|
+
* in the `work: null` group, last. Within a group, pull requests come in the
|
|
190
|
+
* order their oldest commit landed, and commits without one last.
|
|
191
|
+
*/
|
|
192
|
+
export function groupChanges(changes: readonly CandidateChange[]): CandidateGroup[] {
|
|
193
|
+
const oldestFirst = [...changes].reverse();
|
|
194
|
+
const groups = new Map<string | null, Map<number | null, string[]>>();
|
|
195
|
+
const add = (work: string | null, pr: number | null, commit: string): void => {
|
|
196
|
+
const byPr = groups.get(work) ?? new Map<number | null, string[]>();
|
|
197
|
+
groups.set(work, byPr);
|
|
198
|
+
byPr.set(pr, [commit, ...(byPr.get(pr) ?? [])]);
|
|
199
|
+
};
|
|
200
|
+
for (const c of oldestFirst) for (const work of c.work.length > 0 ? c.work : [null]) add(work, c.pr, c.commit);
|
|
201
|
+
const nullLast = <K>(keys: K[]): K[] => [...keys.filter((k) => k !== null), ...keys.filter((k) => k === null)];
|
|
202
|
+
return nullLast([...groups.keys()]).map((work) => {
|
|
203
|
+
const byPr = groups.get(work)!;
|
|
204
|
+
return { work, pullRequests: nullLast([...byPr.keys()]).map((pr) => ({ pr, commits: byPr.get(pr)! })) };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
159
208
|
/** Read the release candidates of the member at `query.cwd`. */
|
|
160
209
|
export async function releaseCandidatesDocument(query: ReleaseCandidatesQuery): Promise<ReleaseCandidatesDocument> {
|
|
161
210
|
const head = { $schema: RELEASE_CANDIDATES_SCHEMA_ID, contract: RELEASE_CANDIDATES_CONTRACT_VERSION, chant: readerVersion() };
|
|
@@ -240,7 +289,8 @@ export async function releaseCandidatesDocument(query: ReleaseCandidatesQuery):
|
|
|
240
289
|
const d = details.get(sha);
|
|
241
290
|
const { work, records } = recordTrailers(d?.trailers ?? {});
|
|
242
291
|
const merge = merges.get(sha) ?? null;
|
|
243
|
-
|
|
292
|
+
// The merge record names its pull request in `source` (../lifecycle/merge-record.ts).
|
|
293
|
+
const mergePr = mergeRecordPr(merge);
|
|
244
294
|
const subject = d?.subject ?? "";
|
|
245
295
|
return { commit: sha, subject, date: d?.date ?? "", work, records, pr: mergePr ?? prFromSubject(subject), merge };
|
|
246
296
|
};
|
|
@@ -265,13 +315,15 @@ export async function releaseCandidatesDocument(query: ReleaseCandidatesQuery):
|
|
|
265
315
|
}
|
|
266
316
|
if (!tags.green.has(sha)) continue;
|
|
267
317
|
const own = change(sha);
|
|
318
|
+
const changes = open.map(change).reverse();
|
|
268
319
|
candidates.push({
|
|
269
320
|
commit: sha,
|
|
270
321
|
subject: own.subject,
|
|
271
322
|
date: own.date,
|
|
272
323
|
newFor: environments.filter((e) => i < newestRun.get(e.env)!).map((e) => e.env),
|
|
273
324
|
cut: cutLabels.get(sha) ?? [],
|
|
274
|
-
changes
|
|
325
|
+
changes,
|
|
326
|
+
groups: groupChanges(changes),
|
|
275
327
|
flags: flagReader(open),
|
|
276
328
|
});
|
|
277
329
|
open = [];
|