@intentius/chant 0.124.31 → 0.124.32
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/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +44 -16
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli/registry.d.ts +13 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +10 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +39 -0
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +5 -5
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +12 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.js +6 -4
- package/dist/workspace/ci-run-index.schema.json +5 -3
- package/dist/workspace/ci-run.d.ts +75 -2
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +195 -53
- package/dist/workspace/ci-runner-key.d.ts +16 -3
- package/dist/workspace/ci-runner-key.d.ts.map +1 -1
- package/dist/workspace/ci-runner-key.js +30 -10
- package/dist/workspace/ci-serve.d.ts +54 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +175 -26
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -2
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +18 -0
- package/src/cli/main.ts +43 -16
- package/src/cli/parse-args.ts +12 -0
- package/src/cli/registry.ts +19 -6
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +42 -0
- package/src/pr-forge.ts +40 -0
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +5 -5
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +5 -3
- package/src/workspace/ci-run-index.test.ts +42 -3
- package/src/workspace/ci-run-index.ts +18 -4
- package/src/workspace/ci-run.test.ts +185 -7
- package/src/workspace/ci-run.ts +224 -54
- package/src/workspace/ci-runner-key.test.ts +55 -17
- package/src/workspace/ci-runner-key.ts +36 -11
- package/src/workspace/ci-serve.test.ts +150 -6
- package/src/workspace/ci-serve.ts +169 -25
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.ts +2 -2
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
package/src/pr-loop.test.ts
CHANGED
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
describePlanChanged,
|
|
15
15
|
memberCounts,
|
|
16
16
|
PR_REPORT_DEPLOYED_OUTPUTS_FLOOR,
|
|
17
|
+
PR_REPORT_PLANNED_OUTPUTS_FLOOR,
|
|
18
|
+
prRoundGate,
|
|
17
19
|
PR_REPORT_SCHEMA_ID,
|
|
20
|
+
PR_REPORT_WAVES_FLOOR,
|
|
18
21
|
prApproveCommand,
|
|
19
22
|
prNoteMarker,
|
|
20
23
|
PR_GATE_OP,
|
|
@@ -113,6 +116,34 @@ const resumedReport: PrReport = {
|
|
|
113
116
|
/** A plan whose members read `net` and `a` from the deployed estate (#4104). */
|
|
114
117
|
const deployedReport: PrReport = { ...planReport, deployedOutputs: { a: ["id"], net: ["cidr"], dns: [] } };
|
|
115
118
|
|
|
119
|
+
/** A plan where `app` reads `a.id`, which `a`'s plan knows only after apply, and `a.arn`, which it knows (#3999). */
|
|
120
|
+
const unknownReport: PrReport = {
|
|
121
|
+
...planReport,
|
|
122
|
+
deployedOutputs: { net: ["cidr"] },
|
|
123
|
+
plannedOutputs: { a: ["arn"] },
|
|
124
|
+
members: [planReport.members[0]!, { ...planReport.members[1]!, unknownInputs: ["a.id"], buildInputs: ["img.publish.digest"] }],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const roundDoc = composeChangeSet([part("app", 3, [{ address: "terraform_data.app", action: "update" }])]);
|
|
128
|
+
/** The apply of {@link unknownReport}: `a` applied, and `app`, planned again, waits on round 2's gate (#3999). */
|
|
129
|
+
const waitingReport: PrReport = {
|
|
130
|
+
...unknownReport,
|
|
131
|
+
stage: "apply",
|
|
132
|
+
status: "waiting",
|
|
133
|
+
approval: { status: "approved", approvedBy: ["github:alice"] },
|
|
134
|
+
rounds: [{ round: 2, gate: prRoundGate("pr-apply", 2), digest: roundDoc.digest, components: ["app"], approval: { status: "pending" }, status: "waiting", changeSet: roundDoc }],
|
|
135
|
+
members: [
|
|
136
|
+
{ ...planReport.members[0]!, status: "applied" },
|
|
137
|
+
{
|
|
138
|
+
...unknownReport.members[1]!,
|
|
139
|
+
status: "waiting",
|
|
140
|
+
round: 2,
|
|
141
|
+
planDigest: `jcs1-sha256:${hex(3)}`,
|
|
142
|
+
held: "its plan read a.id before that output applied as another value",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
|
|
116
147
|
describe("the pr-report schema", () => {
|
|
117
148
|
test("is a valid schema and names this module's id", () => {
|
|
118
149
|
expect(validSchema(schema)).toBe(true);
|
|
@@ -120,7 +151,7 @@ describe("the pr-report schema", () => {
|
|
|
120
151
|
});
|
|
121
152
|
|
|
122
153
|
test("every stage's report validates, and its change set validates against the change-set schema", () => {
|
|
123
|
-
for (const report of [planReport, refusedReport, appliedReport, resumedReport, deployedReport]) {
|
|
154
|
+
for (const report of [planReport, refusedReport, appliedReport, resumedReport, deployedReport, unknownReport, waitingReport]) {
|
|
124
155
|
expectValid(report);
|
|
125
156
|
expectValidChangeSet(report.changeSet);
|
|
126
157
|
expect(report.changeSet.digest).toBe(report.digest);
|
|
@@ -170,6 +201,27 @@ describe("the note", () => {
|
|
|
170
201
|
expect(schema.required).not.toContain("deployedOutputs");
|
|
171
202
|
});
|
|
172
203
|
|
|
204
|
+
test("a plan names the outputs read from upstream plans and the ones known only after apply (#3999)", () => {
|
|
205
|
+
const note = renderPrNote(unknownReport);
|
|
206
|
+
expect(note).toContain("Planned against the outputs this change's plans say its upstreams will have: `a` (`arn`).");
|
|
207
|
+
expect(note).toContain("Known only after apply: `app` reads `a.id`. This approval does not cover what those members do with them.");
|
|
208
|
+
expect(renderPrNote(planReport)).not.toContain("Known only after apply");
|
|
209
|
+
expect(note).toContain("From this pull request's build: `app` reads `img.publish.digest`. The image digest comes from this pull request's build");
|
|
210
|
+
expect(renderPrNote(planReport)).not.toContain("From this pull request's build");
|
|
211
|
+
expect(PR_REPORT_PLANNED_OUTPUTS_FLOOR).toBe("0.124.32");
|
|
212
|
+
expect(schema.required).not.toContain("plannedOutputs");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("a waiting apply names the round's digest and the command that approves it (#3999)", () => {
|
|
216
|
+
const note = renderPrNote(waitingReport, { approver: "github:<your-login>" });
|
|
217
|
+
expect(note).toContain("Applied 1 of 2 members; 1 wait on an approval of the plan made after their upstream applied.");
|
|
218
|
+
expect(note).toContain(`chant approve pr-12 pr-apply-round-2 --plan ${roundDoc.digest} --approver github:<your-login> --sign`);
|
|
219
|
+
expect(note).toContain("| `app` | app | waiting on its new plan |");
|
|
220
|
+
expect(prStatusState(waitingReport)).toBe("pending");
|
|
221
|
+
expect(prStatusDescription(waitingReport)).toBe("waiting: app planned again after an upstream applied, not approved yet");
|
|
222
|
+
expect(prRoundGate("pr-apply", 1)).toBe("pr-apply");
|
|
223
|
+
});
|
|
224
|
+
|
|
173
225
|
test("a small limit leaves the summary out rather than cutting the table", () => {
|
|
174
226
|
const note = renderPrNote(planReport, { limit: 800 });
|
|
175
227
|
expect(note).toContain("| `app` |");
|
|
@@ -181,6 +233,7 @@ describe("the status", () => {
|
|
|
181
233
|
test("describes each stage in at most 140 characters", () => {
|
|
182
234
|
expect(prStatusDescription(planReport)).toMatch(/^2 members: 1 to change, 0 to destroy or replace; plan [0-9a-f]{12}$/);
|
|
183
235
|
expect(prStatusDescription(refusedReport)).toBe("refused: the plan changed after review");
|
|
236
|
+
expect(prStatusDescription({ ...refusedReport, refusal: "author-approved" })).toBe("refused: the approver wrote the change");
|
|
184
237
|
expect(prStatusDescription(appliedReport)).toMatch(/^applied 2 members/);
|
|
185
238
|
for (const r of [planReport, refusedReport, appliedReport]) expect(prStatusDescription(r).length).toBeLessThanOrEqual(140);
|
|
186
239
|
});
|
|
@@ -221,3 +274,66 @@ describe("a workspace member's loop (#3465)", () => {
|
|
|
221
274
|
expect(note).toContain("chant approve pr-12-network pr-apply");
|
|
222
275
|
});
|
|
223
276
|
});
|
|
277
|
+
|
|
278
|
+
describe("a gate per wave on merge (#4003)", () => {
|
|
279
|
+
const waveDigest = `jcs1-sha256:${hex(7)}`;
|
|
280
|
+
const wave1 = { wave: 1, components: ["a"], status: "applied" as const, approval: "pull-request" as const, gate: "pr-apply", digest: doc.digest, approvedBy: ["github:alice"] };
|
|
281
|
+
const waveApplied: PrReport = {
|
|
282
|
+
...appliedReport,
|
|
283
|
+
status: "wave-applied",
|
|
284
|
+
waveGate: "release",
|
|
285
|
+
waves: [wave1],
|
|
286
|
+
members: [{ ...planReport.members[0], status: "applied" }, planReport.members[1]],
|
|
287
|
+
};
|
|
288
|
+
const waiting: PrReport = {
|
|
289
|
+
...waveApplied,
|
|
290
|
+
status: "refused",
|
|
291
|
+
refusal: "wave-not-approved",
|
|
292
|
+
message: "Wave 2 was planned after the waves before it applied, and the pull request's approved plan does not cover it.",
|
|
293
|
+
waves: [
|
|
294
|
+
wave1,
|
|
295
|
+
{ wave: 2, components: ["app"], status: "waiting", approval: "wave", gate: "release-wave-2", digest: waveDigest, moved: ["app now writes app.main.cidr to a different value than the approved plan"] },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
test("the reports validate, waveGate and waves are optional, and both came with the same chant", () => {
|
|
300
|
+
for (const r of [waveApplied, waiting]) expectValid(r);
|
|
301
|
+
expect(schema.required).not.toContain("waves");
|
|
302
|
+
expect(schema.required).not.toContain("waveGate");
|
|
303
|
+
expect(PR_REPORT_WAVES_FLOOR).toBe("0.124.32");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("the note has a row per wave saying whose approval it applied under", () => {
|
|
307
|
+
const note = renderPrNote(waveApplied);
|
|
308
|
+
expect(note).toContain("Applied wave 1 of 2. The next wave runs in its own job.");
|
|
309
|
+
expect(note).toContain("| 1 | `a` | applied | the pull request's `pr-apply` by `github:alice` |");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("a wave that waits names why its plan moved and the approve command for its own gate", () => {
|
|
313
|
+
const note = renderPrNote(waiting, { approver: "github:<your-login>" });
|
|
314
|
+
expect(note).toContain("Wave 2 waits at its gate `release-wave-2`; nothing in it applied.");
|
|
315
|
+
expect(note).toContain("| 2 | `app` | waiting | its own `release-wave-2`, not approved yet |");
|
|
316
|
+
expect(note).toContain("- app now writes app.main.cidr to a different value than the approved plan");
|
|
317
|
+
expect(note).toContain(`chant approve pr-12 release-wave-2 --plan ${waveDigest} --approver github:<your-login> --sign`);
|
|
318
|
+
golden("pr-apply-wave-waiting.note.golden.md", note);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("with --wave-gate the plan's note says later waves are planned again and may stop at their own gate; without it, nothing", () => {
|
|
322
|
+
const sentence = "On merge each wave after the first is planned again once the waves before it apply, and a wave whose plan moved stops at its own gate, `release-wave-<k>`, for approval.";
|
|
323
|
+
const planned = renderPrNote({ ...planReport, waveGate: "release" });
|
|
324
|
+
expect(planned).toContain(sentence);
|
|
325
|
+
expect(renderPrNote({ ...planReport, waveGate: "release", approval: { status: "approved", approvedBy: ["github:alice"] } })).toContain(sentence);
|
|
326
|
+
expect(renderPrNote(planReport)).not.toContain("planned again");
|
|
327
|
+
expect(renderPrNote(waveApplied)).not.toContain("planned again");
|
|
328
|
+
expectValid({ ...planReport, waveGate: "release" });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("the status: a wave applied succeeds, a wave waiting on its approval is pending, a moved approval fails", () => {
|
|
332
|
+
expect(prStatusDescription(waveApplied)).toBe("applied wave 1 of 2 under the pull request's approval");
|
|
333
|
+
expect(prStatusState(waveApplied)).toBe("success");
|
|
334
|
+
expect(prStatusDescription(waiting)).toBe("wave 2 waits at release-wave-2: its plan moved, approve it");
|
|
335
|
+
expect(prStatusState(waiting)).toBe("pending");
|
|
336
|
+
expect(prStatusState({ ...waiting, refusal: "wave-plan-changed" })).toBe("failure");
|
|
337
|
+
expect(prStatusDescription({ ...waiting, waves: [wave1], refusal: "earlier-wave" })).toBe("refused: an earlier wave has not applied");
|
|
338
|
+
});
|
|
339
|
+
});
|
package/src/pr-loop.ts
CHANGED
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
* and decides the gate against the new digest. The same plan applies; a moved
|
|
29
29
|
* plan stops with both digests named and nothing applied.
|
|
30
30
|
*
|
|
31
|
+
* ## A gate per wave on merge
|
|
32
|
+
*
|
|
33
|
+
* With `--wave-gate <gate>` (#4003) the apply runs a wave at a time, and
|
|
34
|
+
* plans each wave after the one before it applied. Wave 1 applies under the
|
|
35
|
+
* pull request's approval. A later wave whose new plan the approved change
|
|
36
|
+
* set covers (`planCoveredBy`) applies under that approval too; one whose
|
|
37
|
+
* plan moved waits at `<gate>-wave-<k>`, under the same op, for an approval
|
|
38
|
+
* of its new plan. The report's `waves` says which wave applied under which
|
|
39
|
+
* approval.
|
|
40
|
+
*
|
|
31
41
|
* ## Inside a workspace member
|
|
32
42
|
*
|
|
33
43
|
* Each workspace member that generates the pipeline (#3465) runs its own
|
|
@@ -49,6 +59,15 @@ export const PR_REPORT_CONTRACT = 1;
|
|
|
49
59
|
/** The first chant whose report can carry `deployedOutputs` (#4104). */
|
|
50
60
|
export const PR_REPORT_DEPLOYED_OUTPUTS_FLOOR = "0.124.30";
|
|
51
61
|
|
|
62
|
+
/** The first chant whose report can carry `waveGate` and `waves` (#4003). */
|
|
63
|
+
export const PR_REPORT_WAVES_FLOOR = "0.124.32";
|
|
64
|
+
/**
|
|
65
|
+
* The first chant whose report can carry `plannedOutputs`, `rounds`, a
|
|
66
|
+
* member's `unknownInputs`, `buildInputs`, `round` and `held`, and the `waiting` statuses
|
|
67
|
+
* (#3999).
|
|
68
|
+
*/
|
|
69
|
+
export const PR_REPORT_PLANNED_OUTPUTS_FLOOR = "0.124.32";
|
|
70
|
+
|
|
52
71
|
/** The gate a pull request's apply waits on, unless the pipeline names another. */
|
|
53
72
|
export const PR_APPLY_GATE = "pr-apply";
|
|
54
73
|
|
|
@@ -60,6 +79,16 @@ export function prOp(pr: number, member?: string): string {
|
|
|
60
79
|
return member ? `pr-${pr}-${member}` : `pr-${pr}`;
|
|
61
80
|
}
|
|
62
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The gate a round of an apply waits on (#3999): the pull request's own
|
|
84
|
+
* gate for the first, and `<gate>-round-<n>` for each one planned after an
|
|
85
|
+
* upstream applied. A workspace's `identity.gates` entry for `gate` covers
|
|
86
|
+
* every round.
|
|
87
|
+
*/
|
|
88
|
+
export function prRoundGate(gate: string, round: number): string {
|
|
89
|
+
return round <= 1 ? gate : `${gate}-round-${round}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
/** Whether `op` is a pull request's gate op, with or without a member. Member names follow the declaration's name pattern. */
|
|
64
93
|
export const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
|
|
65
94
|
|
|
@@ -95,7 +124,13 @@ export type PrMemberStatus =
|
|
|
95
124
|
/** The apply failed. */
|
|
96
125
|
| "failed"
|
|
97
126
|
/** Not applied because a member it depends on failed. */
|
|
98
|
-
| "blocked"
|
|
127
|
+
| "blocked"
|
|
128
|
+
/**
|
|
129
|
+
* Apply stage (#3999): held back because its plan read an upstream output
|
|
130
|
+
* that applied as another value, or was known only after apply. It waits
|
|
131
|
+
* on an approval of the plan the apply made for it afterwards (`rounds`).
|
|
132
|
+
*/
|
|
133
|
+
| "waiting";
|
|
99
134
|
|
|
100
135
|
export interface PrMember {
|
|
101
136
|
/** The member's name in the change set: a root, for a terraform-family step. */
|
|
@@ -110,9 +145,48 @@ export interface PrMember {
|
|
|
110
145
|
/**
|
|
111
146
|
* Components this one depends on whose outputs changed when they applied
|
|
112
147
|
* (apply stage). Its applied plan read the values from before, so the next
|
|
113
|
-
* run plans it again.
|
|
148
|
+
* run plans it again. Not written since #3999, which holds such a member
|
|
149
|
+
* back and plans it again in the same apply (`held`, `round`).
|
|
114
150
|
*/
|
|
115
151
|
inputsMoved?: string[];
|
|
152
|
+
/**
|
|
153
|
+
* The upstream outputs its plan read that the change's plans could not
|
|
154
|
+
* say (#3999), as `<component>.<output>`, sorted: known only after that
|
|
155
|
+
* upstream applies. The approval does not cover what this member does
|
|
156
|
+
* with them; the apply plans it again once they are known.
|
|
157
|
+
*/
|
|
158
|
+
unknownInputs?: string[];
|
|
159
|
+
/**
|
|
160
|
+
* The published artifact identities its plan read from a build in the
|
|
161
|
+
* same change (#3999), as `<component>.publish.<field>`, sorted: an image
|
|
162
|
+
* digest, say. Known only after the build runs, but never approved again:
|
|
163
|
+
* the build's provenance and attestation bind the digest, so the member
|
|
164
|
+
* applies without a later round.
|
|
165
|
+
*/
|
|
166
|
+
buildInputs?: string[];
|
|
167
|
+
/** Apply stage (#3999): the round whose plan it applied or waits on, when not the first. */
|
|
168
|
+
round?: number;
|
|
169
|
+
/** Apply stage (#3999): why the first round's plan did not apply. */
|
|
170
|
+
held?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A round of plans an apply made after an upstream applied (#3999), the report's `rounds`. */
|
|
174
|
+
export interface PrRound {
|
|
175
|
+
/** 2 for the first plan made after an upstream applied, and on. */
|
|
176
|
+
round: number;
|
|
177
|
+
/** The gate it waits on: {@link prRoundGate}. */
|
|
178
|
+
gate: string;
|
|
179
|
+
/** The change-set digest of what the round planned. What its gate binds. */
|
|
180
|
+
digest: string;
|
|
181
|
+
/** The components planned again, in apply order. */
|
|
182
|
+
components: string[];
|
|
183
|
+
approval: PrApproval;
|
|
184
|
+
/** `waiting`: no approval stands for `digest`. `plan-failed`: a member failed to plan, so the round did not apply. */
|
|
185
|
+
status: "applied" | "failed" | "waiting" | "plan-failed";
|
|
186
|
+
/** With `waiting`: why an approval that stands does not count (`--require-review`). */
|
|
187
|
+
message?: string;
|
|
188
|
+
/** The change-set document the round planned. */
|
|
189
|
+
changeSet: ChangeSetDocument;
|
|
116
190
|
}
|
|
117
191
|
|
|
118
192
|
/** Where the approval stands for the planned digest. */
|
|
@@ -141,8 +215,19 @@ export type PrStatus =
|
|
|
141
215
|
| "applied"
|
|
142
216
|
/** Apply stage: a member failed to apply. Its dependents did not run. */
|
|
143
217
|
| "failed"
|
|
218
|
+
/**
|
|
219
|
+
* Apply stage with `--wave-gate` and `--wave` (#4003): this wave applied,
|
|
220
|
+
* and a later wave of the change is still to run.
|
|
221
|
+
*/
|
|
222
|
+
| "wave-applied"
|
|
144
223
|
/** Apply stage: nothing applied, see `refusal`. */
|
|
145
|
-
| "refused"
|
|
224
|
+
| "refused"
|
|
225
|
+
/**
|
|
226
|
+
* Apply stage (#3999): the approved plans applied, except members held
|
|
227
|
+
* back after an upstream applied. They wait on the approval of the plan
|
|
228
|
+
* the last of `rounds` made.
|
|
229
|
+
*/
|
|
230
|
+
| "waiting";
|
|
146
231
|
|
|
147
232
|
/** Why an apply refused. */
|
|
148
233
|
export type PrRefusal =
|
|
@@ -152,8 +237,45 @@ export type PrRefusal =
|
|
|
152
237
|
| "plan-changed"
|
|
153
238
|
/** The approval names someone who has not approved the pull request on the forge. */
|
|
154
239
|
| "not-a-reviewer"
|
|
240
|
+
/** The approval names the pull request's author, or an author or committer of one of its commits (#4000). */
|
|
241
|
+
| "author-approved"
|
|
155
242
|
/** No pull request merged this commit, so there is no approval to read. */
|
|
156
|
-
| "no-pull-request"
|
|
243
|
+
| "no-pull-request"
|
|
244
|
+
/** `--wave-gate` (#4003): the wave's new plan is not one the pull request approved, and no approval stands for it. */
|
|
245
|
+
| "wave-not-approved"
|
|
246
|
+
/** `--wave-gate`: an approval of the wave's gate stands for another digest, so its plan moved since. */
|
|
247
|
+
| "wave-plan-changed"
|
|
248
|
+
/** `--wave-gate` with `--wave <k>`: an earlier wave has work left, or no record says it applied. */
|
|
249
|
+
| "earlier-wave";
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* One wave of an apply with `--wave-gate` (#4003): what it planned, where it
|
|
253
|
+
* got to, and which approval it applied or waits under.
|
|
254
|
+
*/
|
|
255
|
+
export interface PrWave {
|
|
256
|
+
/** 1-based, from the derivation (`selection.waves`). */
|
|
257
|
+
wave: number;
|
|
258
|
+
/** The components planned in it on the run that reached it. */
|
|
259
|
+
components: string[];
|
|
260
|
+
/** `applied`; `failed` or `plan-failed`; or `waiting` at its gate. */
|
|
261
|
+
status: "applied" | "failed" | "plan-failed" | "waiting";
|
|
262
|
+
/**
|
|
263
|
+
* `pull-request`: the approved change set covers the wave's plan, so it
|
|
264
|
+
* applies under the pull request's approval. `wave`: its plan moved, so it
|
|
265
|
+
* needs an approval of its own gate.
|
|
266
|
+
*/
|
|
267
|
+
approval: "pull-request" | "wave";
|
|
268
|
+
/** The gate it applied or waits under: the pull request's, or `<wave gate>-wave-<k>`. */
|
|
269
|
+
gate: string;
|
|
270
|
+
/** The digest that gate binds: the pull request's change set, or the wave's new plan. */
|
|
271
|
+
digest: string;
|
|
272
|
+
/** Who approved that digest. */
|
|
273
|
+
approvedBy?: string[];
|
|
274
|
+
/** Waiting: an approval of the wave's gate stands for this other digest. */
|
|
275
|
+
approved?: string;
|
|
276
|
+
/** Why the approved change set does not cover the wave's plan, one line per member. */
|
|
277
|
+
moved?: string[];
|
|
278
|
+
}
|
|
157
279
|
|
|
158
280
|
/**
|
|
159
281
|
* The document `pr-plan` and `pr-apply` write (`pr-report.schema.json`).
|
|
@@ -194,7 +316,28 @@ export interface PrReport {
|
|
|
194
316
|
* {@link PR_REPORT_DEPLOYED_OUTPUTS_FLOOR}.
|
|
195
317
|
*/
|
|
196
318
|
deployedOutputs?: Record<string, string[]>;
|
|
319
|
+
/**
|
|
320
|
+
* Per dependency in the change that a member was planned against (#3999),
|
|
321
|
+
* the names of the outputs its plan knows, sorted. An output it reads that
|
|
322
|
+
* is not here is known only after apply (a member's `unknownInputs`).
|
|
323
|
+
* Absent when no member read one. Added within contract 1 by chant
|
|
324
|
+
* {@link PR_REPORT_PLANNED_OUTPUTS_FLOOR}.
|
|
325
|
+
*/
|
|
326
|
+
plannedOutputs?: Record<string, string[]>;
|
|
327
|
+
/**
|
|
328
|
+
* Apply stage (#3999): the rounds planned after an upstream applied, in
|
|
329
|
+
* order, each with its own gate and digest. Absent when nothing was held.
|
|
330
|
+
*/
|
|
331
|
+
rounds?: PrRound[];
|
|
197
332
|
approval: PrApproval;
|
|
333
|
+
/**
|
|
334
|
+
* The wave gate's base name with `--wave-gate` (#4003): wave k waits on
|
|
335
|
+
* `<waveGate>-wave-<k>` when its plan moved. Added within contract 1 by
|
|
336
|
+
* chant {@link PR_REPORT_WAVES_FLOOR}.
|
|
337
|
+
*/
|
|
338
|
+
waveGate?: string;
|
|
339
|
+
/** With `waveGate`: each wave reached so far, in order. */
|
|
340
|
+
waves?: PrWave[];
|
|
198
341
|
/** Component names: which changed, which run because they depend on one, and what the change touched that no component deploys. */
|
|
199
342
|
selection: {
|
|
200
343
|
changed: string[];
|
|
@@ -252,6 +395,7 @@ const STATUS_WORDS: Record<PrMemberStatus, string> = {
|
|
|
252
395
|
applied: "applied",
|
|
253
396
|
failed: "apply failed",
|
|
254
397
|
blocked: "blocked",
|
|
398
|
+
waiting: "waiting on its new plan",
|
|
255
399
|
};
|
|
256
400
|
|
|
257
401
|
function headline(report: PrReport): string {
|
|
@@ -268,11 +412,55 @@ function headline(report: PrReport): string {
|
|
|
268
412
|
return `Applied ${members}.`;
|
|
269
413
|
case "failed":
|
|
270
414
|
return `The apply failed: ${report.members.filter((m) => m.status === "failed").length} failed, ${report.members.filter((m) => m.status === "applied").length} applied.`;
|
|
271
|
-
case "
|
|
272
|
-
|
|
415
|
+
case "wave-applied": {
|
|
416
|
+
const applied = (report.waves ?? []).filter((w) => w.status === "applied").map((w) => w.wave);
|
|
417
|
+
const total = report.selection.waves.length;
|
|
418
|
+
return `Applied wave ${applied.at(-1) ?? "?"} of ${total}. The next wave runs in its own job.`;
|
|
419
|
+
}
|
|
420
|
+
case "refused": {
|
|
421
|
+
const waiting = report.waves?.find((w) => w.status === "waiting");
|
|
422
|
+
return waiting ? `Wave ${waiting.wave} waits at its gate ${code(waiting.gate)}; nothing in it applied.` : "The apply refused.";
|
|
423
|
+
}
|
|
424
|
+
case "waiting":
|
|
425
|
+
return `Applied ${report.members.filter((m) => m.status === "applied").length} of ${members}; ${report.members.filter((m) => m.status === "waiting").length} wait on an approval of the plan made after their upstream applied.`;
|
|
273
426
|
}
|
|
274
427
|
}
|
|
275
428
|
|
|
429
|
+
/** Who a wave applied or waits under, as a table cell. */
|
|
430
|
+
function waveApproval(w: PrWave): string {
|
|
431
|
+
const whose = w.approval === "pull-request" ? "the pull request's" : "its own";
|
|
432
|
+
const by = w.approvedBy?.length ? ` by ${w.approvedBy.map(code).join(", ")}` : w.status === "waiting" ? ", not approved yet" : "";
|
|
433
|
+
return `${whose} ${code(w.gate)}${by}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The waves of an apply with `--wave-gate` (#4003): a row per wave reached,
|
|
438
|
+
* saying which approval it applied under, then for a wave that waits the
|
|
439
|
+
* reason its plan moved and the approve command for its new plan.
|
|
440
|
+
*/
|
|
441
|
+
function waveLines(report: PrReport, approver: string): string[] {
|
|
442
|
+
const waves = report.waves ?? [];
|
|
443
|
+
if (waves.length === 0) return [];
|
|
444
|
+
const out = ["", "| Wave | Components | Status | Approval |", "|---:|---|---|---|"];
|
|
445
|
+
for (const w of waves) out.push(`| ${w.wave} | ${w.components.map(code).join(", ")} | ${w.status} | ${waveApproval(w)} |`);
|
|
446
|
+
for (const w of waves.filter((x) => x.status === "waiting")) {
|
|
447
|
+
out.push("", `Wave ${w.wave} was planned after the waves before it applied, and the approved plan does not cover it:`, "");
|
|
448
|
+
for (const why of w.moved ?? []) out.push(`- ${why}`);
|
|
449
|
+
if (w.approved) out.push("", `An approval of ${code(w.gate)} stands for ${code(w.approved)}, not for the plan now.`);
|
|
450
|
+
out.push(
|
|
451
|
+
"",
|
|
452
|
+
`Read wave ${w.wave}'s plan, ${code(w.digest)}, and approve it with:`,
|
|
453
|
+
"",
|
|
454
|
+
"```",
|
|
455
|
+
prApproveCommand({ op: report.op, gate: w.gate, digest: w.digest }, approver),
|
|
456
|
+
"```",
|
|
457
|
+
"",
|
|
458
|
+
"Then run the wave's apply job again.",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
276
464
|
function approvalLines(report: PrReport, approver: string): string[] {
|
|
277
465
|
const a = report.approval;
|
|
278
466
|
if (report.status === "nothing") return [];
|
|
@@ -332,6 +520,7 @@ export function renderPrNote(report: PrReport, options: { limit?: number; approv
|
|
|
332
520
|
out.push("", "A member whose inputs moved applied the plan it was approved with. The next run plans it against the new values.");
|
|
333
521
|
}
|
|
334
522
|
}
|
|
523
|
+
out.push(...waveLines(report, options.approver ?? "<you>"));
|
|
335
524
|
if (report.selection.unclaimed.length > 0) {
|
|
336
525
|
out.push("", `Changed, and deployed by no component: ${report.selection.unclaimed.map(code).join(", ")}.`);
|
|
337
526
|
}
|
|
@@ -340,9 +529,43 @@ export function renderPrNote(report: PrReport, options: { limit?: number; approv
|
|
|
340
529
|
const each = deployed.map(([dep, names]) => `${code(dep)} (${names.length > 0 ? names.map(code).join(", ") : "none yet"})`);
|
|
341
530
|
out.push("", `Planned against outputs read from the deployed estate: ${each.join("; ")}.`);
|
|
342
531
|
}
|
|
532
|
+
const planned = Object.entries(report.plannedOutputs ?? {});
|
|
533
|
+
if (planned.length > 0) {
|
|
534
|
+
const each = planned.map(([dep, names]) => `${code(dep)} (${names.length > 0 ? names.map(code).join(", ") : "none known"})`);
|
|
535
|
+
out.push("", `Planned against the outputs this change's plans say its upstreams will have: ${each.join("; ")}.`);
|
|
536
|
+
}
|
|
537
|
+
const unknown = [...new Set(report.members.flatMap((m) => (m.unknownInputs ?? []).map((ref) => `${code(m.component)} reads ${code(ref)}`)))];
|
|
538
|
+
if (unknown.length > 0) {
|
|
539
|
+
out.push(
|
|
540
|
+
"",
|
|
541
|
+
`Known only after apply: ${unknown.join("; ")}. This approval does not cover what those members do with them. ` +
|
|
542
|
+
"The apply plans them again once their upstream applied and asks for an approval of that plan.",
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const fromBuild = [...new Set(report.members.flatMap((m) => (m.buildInputs ?? []).map((ref) => `${code(m.component)} reads ${code(ref)}`)))];
|
|
546
|
+
if (fromBuild.length > 0) {
|
|
547
|
+
out.push(
|
|
548
|
+
"",
|
|
549
|
+
`From this pull request's build: ${fromBuild.join("; ")}. The image digest comes from this pull request's build, whose provenance binds it, ` +
|
|
550
|
+
"so those members apply with it once the build ran, without a second approval.",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
for (const r of report.rounds ?? []) {
|
|
554
|
+
const what = `Round ${r.round} planned ${r.components.map(code).join(", ")} again after their upstream applied, digest ${code(r.digest)}`;
|
|
555
|
+
if (r.status === "waiting") {
|
|
556
|
+
if (r.message) out.push("", `> ${r.message}`);
|
|
557
|
+
out.push("", `${what}. Not approved yet. Approve it, then run the apply again with the same \`--resume\` record:`, "", "```", prApproveCommand({ op: report.op, gate: r.gate, digest: r.digest }, options.approver ?? "<you>"), "```");
|
|
558
|
+
} else {
|
|
559
|
+
out.push("", `${what}: ${r.status === "applied" ? `applied, approved by ${(r.approval.approvedBy ?? []).map(code).join(", ")}` : r.status === "plan-failed" ? "a member failed to plan" : "the apply failed"}.`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
343
562
|
if (report.status !== "nothing") {
|
|
344
563
|
out.push("", `Plan digest: ${code(report.digest)}`, "", ...approvalLines(report, options.approver ?? "<you>"));
|
|
345
564
|
}
|
|
565
|
+
// #4003: with a gate per wave, a later wave's plan on merge may differ from this one.
|
|
566
|
+
if (report.stage === "plan" && report.status === "planned" && report.waveGate) {
|
|
567
|
+
out.push("", `On merge each wave after the first is planned again once the waves before it apply, and a wave whose plan moved stops at its own gate, ${code(`${report.waveGate}-wave-<k>`)}, for approval.`);
|
|
568
|
+
}
|
|
346
569
|
const head = out.join("\n") + "\n";
|
|
347
570
|
if (report.status === "nothing" || report.changeSet.members.length === 0) return head;
|
|
348
571
|
const room = limit - [...head].length - 1;
|
|
@@ -374,19 +597,38 @@ export function prStatusDescription(report: PrReport): string {
|
|
|
374
597
|
case "applied":
|
|
375
598
|
text = `applied ${n} member${n === 1 ? "" : "s"}${digest}`;
|
|
376
599
|
break;
|
|
600
|
+
case "wave-applied": {
|
|
601
|
+
const last = (report.waves ?? []).filter((w) => w.status === "applied").at(-1);
|
|
602
|
+
text = `applied wave ${last?.wave ?? "?"} of ${report.selection.waves.length}${last ? ` under ${last.approval === "pull-request" ? "the pull request's approval" : `${last.gate}`}` : ""}`;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
377
605
|
case "failed":
|
|
378
606
|
text = `apply failed for ${report.members.filter((m) => m.status === "failed").map((m) => m.member).join(", ")}`;
|
|
379
607
|
break;
|
|
380
|
-
case "
|
|
381
|
-
text = `
|
|
608
|
+
case "waiting":
|
|
609
|
+
text = `waiting: ${report.members.filter((m) => m.status === "waiting").map((m) => m.member).join(", ")} planned again after an upstream applied, not approved yet`;
|
|
382
610
|
break;
|
|
611
|
+
case "refused": {
|
|
612
|
+
const waiting = report.waves?.find((w) => w.status === "waiting");
|
|
613
|
+
if (waiting && (report.refusal === "wave-not-approved" || report.refusal === "wave-plan-changed")) {
|
|
614
|
+
text = `wave ${waiting.wave} waits at ${waiting.gate}: ${report.refusal === "wave-plan-changed" ? "its plan changed after its approval" : "its plan moved, approve it"}`;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
if (report.refusal === "earlier-wave") {
|
|
618
|
+
text = "refused: an earlier wave has not applied";
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
text = `refused: ${report.refusal === "plan-changed" ? "the plan changed after review" : report.refusal === "not-a-reviewer" ? "the approver has not approved the pull request" : report.refusal === "author-approved" ? "the approver wrote the change" : report.refusal === "no-pull-request" ? "no pull request merged this commit" : "the plan is not approved"}`;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
383
624
|
}
|
|
384
625
|
return text.length > 140 ? text.slice(0, 137) + "..." : text;
|
|
385
626
|
}
|
|
386
627
|
|
|
387
628
|
/** The commit status state for a report: success, failure, or pending while it waits on approval. */
|
|
388
629
|
export function prStatusState(report: PrReport): "success" | "failure" | "pending" {
|
|
389
|
-
if (report.status === "planned" || report.status === "applied" || report.status === "nothing") return "success";
|
|
390
|
-
if (report.status === "refused" && report.refusal === "not-approved") return "pending";
|
|
630
|
+
if (report.status === "planned" || report.status === "applied" || report.status === "wave-applied" || report.status === "nothing") return "success";
|
|
631
|
+
if (report.status === "refused" && (report.refusal === "not-approved" || report.refusal === "wave-not-approved")) return "pending";
|
|
632
|
+
if (report.status === "waiting") return "pending";
|
|
391
633
|
return "failure";
|
|
392
634
|
}
|
package/src/pr-report-url.ts
CHANGED
|
@@ -44,17 +44,17 @@ export type ReportTarget =
|
|
|
44
44
|
| { scheme: "file"; url: string; dir: string }
|
|
45
45
|
| { scheme: "s3"; url: string; bucket: string; prefix: string };
|
|
46
46
|
|
|
47
|
-
/** `--report-url` read, or why it can't be. */
|
|
48
|
-
export function parseReportUrl(raw: string): ReportTarget | { error: string } {
|
|
47
|
+
/** `--report-url` read, or why it can't be. `name` is what the error calls the value: the flag, or `ci.reportUrl` (#4160). */
|
|
48
|
+
export function parseReportUrl(raw: string, name = "--report-url"): ReportTarget | { error: string } {
|
|
49
49
|
const url = raw.trim();
|
|
50
50
|
if (url.startsWith("file://")) {
|
|
51
51
|
let dir: string;
|
|
52
52
|
try {
|
|
53
53
|
dir = fileURLToPath(url);
|
|
54
54
|
} catch (err) {
|
|
55
|
-
return { error:
|
|
55
|
+
return { error: `${name} ${url}: ${err instanceof Error ? err.message : String(err)}` };
|
|
56
56
|
}
|
|
57
|
-
if (!isAbsolute(dir)) return { error:
|
|
57
|
+
if (!isAbsolute(dir)) return { error: `${name} ${url}: a file URL names an absolute directory` };
|
|
58
58
|
return { scheme: "file", url: url.replace(/\/+$/, ""), dir };
|
|
59
59
|
}
|
|
60
60
|
const s3 = /^s3:\/\/([^/]+)\/?(.*)$/.exec(url);
|
|
@@ -62,7 +62,7 @@ export function parseReportUrl(raw: string): ReportTarget | { error: string } {
|
|
|
62
62
|
const prefix = s3[2]!.replace(/\/+$/, "");
|
|
63
63
|
return { scheme: "s3", url: `s3://${s3[1]}${prefix ? `/${prefix}` : ""}`, bucket: s3[1]!, prefix };
|
|
64
64
|
}
|
|
65
|
-
return { error:
|
|
65
|
+
return { error: `${name} ${url}: takes an s3://<bucket>/<prefix> or file://<directory> URL` };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** The key a stage's files are kept under, below the URL: `<pr>/<commit>[/<member>]`. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { cpSync, mkdtempSync, rmSync } from "fs";
|
|
4
|
+
import { tmpdir } from "os";
|
|
3
5
|
import {
|
|
4
6
|
carveAdvise,
|
|
5
7
|
carveJson,
|
|
@@ -50,10 +52,27 @@ describe("carve advise against the sample estate", () => {
|
|
|
50
52
|
expect(byAddr["random_pet.suffix"].score).toBe(0);
|
|
51
53
|
expect(byAddr["random_pet.suffix"].band).toBe("leave in Terraform");
|
|
52
54
|
|
|
53
|
-
// The report never proposes a mutation.
|
|
54
|
-
|
|
55
|
+
// The report never proposes a mutation. The estate path is the caller's,
|
|
56
|
+
// so it is masked before the check (#3169).
|
|
57
|
+
const text = formatCarveReport(r).split(r.from!).join("<estate>");
|
|
55
58
|
expect(text).not.toMatch(/state rm|apply|destroy/i);
|
|
56
59
|
});
|
|
60
|
+
|
|
61
|
+
test("the no-mutation check holds in a checkout whose path says apply or destroy (#3169)", async () => {
|
|
62
|
+
if (!parserAvailable) return;
|
|
63
|
+
const parent = mkdtempSync(join(tmpdir(), "carve-native-apply-destroy-"));
|
|
64
|
+
try {
|
|
65
|
+
const estate = join(parent, "sample-estate");
|
|
66
|
+
cpSync(ESTATE, estate, { recursive: true });
|
|
67
|
+
const r = await carveAdvise({ from: estate });
|
|
68
|
+
expect(r.ok).toBe(true);
|
|
69
|
+
const text = formatCarveReport(r);
|
|
70
|
+
expect(text).toContain(estate);
|
|
71
|
+
expect(text.split(r.from!).join("<estate>")).not.toMatch(/state rm|apply|destroy/i);
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(parent, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
57
76
|
});
|
|
58
77
|
|
|
59
78
|
/**
|