@intentius/chant 0.124.28 → 0.124.29
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/build-params.d.ts +34 -1
- package/dist/build-params.d.ts.map +1 -1
- package/dist/build-params.js +31 -1
- package/dist/change-set-graph.d.ts +8 -1
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +4 -2
- package/dist/change-set.d.ts +10 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +4 -3
- package/dist/cli/build-params-cli.d.ts +7 -1
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/build-params-cli.js +17 -8
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -3
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +45 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -4
- package/dist/cli/handlers/fan-out.d.ts +9 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +40 -3
- package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
- package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-gc.js +143 -0
- package/dist/cli/handlers/pr.d.ts +11 -3
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +134 -26
- package/dist/cli/handlers/promote.d.ts +7 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +210 -33
- package/dist/cli/handlers/release.d.ts +62 -5
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +414 -28
- package/dist/cli/handlers/run.d.ts +11 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +67 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +61 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -2
- package/dist/cli/registry.d.ts +15 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +3 -2
- package/dist/cli/run-events.d.ts.map +1 -1
- package/dist/cli/run-events.js +5 -3
- package/dist/components/auto-release.d.ts +2 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +1 -0
- package/dist/components/capability.d.ts +9 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +35 -0
- package/dist/components/fan-out-inputs.d.ts +69 -0
- package/dist/components/fan-out-inputs.d.ts.map +1 -0
- package/dist/components/fan-out-inputs.js +169 -0
- package/dist/components/fan-out-record.d.ts +13 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +16 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +50 -0
- package/dist/components/fan-out.d.ts +4 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +25 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +22 -4
- package/dist/components/promote.d.ts +23 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +60 -2
- package/dist/components/release-compile.d.ts +9 -0
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +9 -3
- package/dist/components/release-gate.d.ts +11 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +10 -4
- package/dist/components/wave-plan.d.ts +5 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +10 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/flag-params.d.ts +54 -0
- package/dist/flag-params.d.ts.map +1 -0
- package/dist/flag-params.js +130 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
- package/dist/lifecycle/change-set-store.d.ts +2 -1
- package/dist/lifecycle/change-set-store.d.ts.map +1 -1
- package/dist/lifecycle/change-set-store.js +2 -1
- package/dist/lifecycle/delivery-metrics.d.ts +139 -0
- package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
- package/dist/lifecycle/delivery-metrics.js +145 -0
- package/dist/lifecycle/deploy-outcome.d.ts +81 -0
- package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
- package/dist/lifecycle/deploy-outcome.js +101 -0
- package/dist/lifecycle/gate-change-set.d.ts +10 -1
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
- package/dist/lifecycle/gate-change-set.js +35 -3
- package/dist/lifecycle/gate-ledger.d.ts +2 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +30 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +31 -5
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/mixed-environment.d.ts +95 -0
- package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
- package/dist/lifecycle/mixed-environment.js +154 -0
- package/dist/lifecycle/publish-attachments.d.ts +79 -0
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
- package/dist/lifecycle/publish-attachments.js +179 -0
- package/dist/lifecycle/publish-credentials.d.ts +37 -0
- package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
- package/dist/lifecycle/publish-credentials.js +69 -0
- package/dist/lifecycle/publish-declaration.d.ts +97 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.js +83 -0
- package/dist/lifecycle/publish-forge.d.ts +53 -0
- package/dist/lifecycle/publish-forge.d.ts.map +1 -0
- package/dist/lifecycle/publish-forge.js +298 -0
- package/dist/lifecycle/publish-npm.d.ts +61 -0
- package/dist/lifecycle/publish-npm.d.ts.map +1 -0
- package/dist/lifecycle/publish-npm.js +242 -0
- package/dist/lifecycle/publish-oci.d.ts +64 -0
- package/dist/lifecycle/publish-oci.d.ts.map +1 -0
- package/dist/lifecycle/publish-oci.js +383 -0
- package/dist/lifecycle/publish-record.d.ts +66 -0
- package/dist/lifecycle/publish-record.d.ts.map +1 -0
- package/dist/lifecycle/publish-record.js +84 -0
- package/dist/lifecycle/publish-targets.d.ts +45 -0
- package/dist/lifecycle/publish-targets.d.ts.map +1 -0
- package/dist/lifecycle/publish-targets.js +85 -0
- package/dist/lifecycle/release-cut.d.ts +48 -1
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +116 -23
- package/dist/lifecycle/release-join.d.ts +95 -0
- package/dist/lifecycle/release-join.d.ts.map +1 -0
- package/dist/lifecycle/release-join.js +139 -0
- package/dist/lifecycle/release-publish.d.ts +312 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -0
- package/dist/lifecycle/release-publish.js +335 -0
- package/dist/lifecycle/release-revocation.d.ts +80 -0
- package/dist/lifecycle/release-revocation.d.ts.map +1 -0
- package/dist/lifecycle/release-revocation.js +123 -0
- package/dist/lifecycle/release-store.d.ts +12 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +4 -2
- package/dist/lifecycle/release-verify.d.ts +61 -0
- package/dist/lifecycle/release-verify.d.ts.map +1 -0
- package/dist/lifecycle/release-verify.js +92 -0
- package/dist/lifecycle/release-workstation.d.ts +91 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -0
- package/dist/lifecycle/release-workstation.js +180 -0
- package/dist/lifecycle/retention-policy.d.ts +45 -0
- package/dist/lifecycle/retention-policy.d.ts.map +1 -0
- package/dist/lifecycle/retention-policy.js +43 -0
- package/dist/lifecycle/retention.d.ts +185 -0
- package/dist/lifecycle/retention.d.ts.map +1 -0
- package/dist/lifecycle/retention.js +400 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-loop.d.ts +7 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +23 -4
- package/dist/pr-report-url.d.ts +82 -0
- package/dist/pr-report-url.d.ts.map +1 -0
- package/dist/pr-report-url.js +174 -0
- package/dist/provenance.d.ts +2 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/run-event-store.d.ts +29 -2
- package/dist/run-event-store.d.ts.map +1 -1
- package/dist/run-event-store.js +50 -3
- package/dist/run-event-stream.d.ts +2 -1
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +5 -1
- package/dist/run-event.d.ts +19 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +2 -0
- package/dist/wave-runtime.mjs +260 -229
- package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
- package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
- package/dist/workspace/change-set.schema.json +5 -1
- package/dist/workspace/check.schema.json +4 -2
- package/dist/workspace/checks/records.d.ts +9 -1
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks/records.js +35 -1
- package/dist/workspace/checks.d.ts +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +2 -1
- package/dist/workspace/ci-green.d.ts +8 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +10 -3
- package/dist/workspace/declaration.d.ts +16 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +26 -0
- package/dist/workspace/declaration.schema.json +64 -0
- package/dist/workspace/declared-kinds.d.ts +16 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/declared-kinds.js +25 -0
- package/dist/workspace/delivery-metrics.d.ts +91 -0
- package/dist/workspace/delivery-metrics.d.ts.map +1 -0
- package/dist/workspace/delivery-metrics.js +217 -0
- package/dist/workspace/delivery-metrics.schema.json +519 -0
- package/dist/workspace/flags-read.d.ts +37 -0
- package/dist/workspace/flags-read.d.ts.map +1 -0
- package/dist/workspace/flags-read.js +103 -0
- package/dist/workspace/flags.d.ts +100 -0
- package/dist/workspace/flags.d.ts.map +1 -0
- package/dist/workspace/flags.js +98 -0
- package/dist/workspace/graph-cli.js +1 -1
- package/dist/workspace/intent-cli.d.ts +11 -2
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.js +54 -5
- package/dist/workspace/intent-trace.d.ts +219 -0
- package/dist/workspace/intent-trace.d.ts.map +1 -0
- package/dist/workspace/intent-trace.js +314 -0
- package/dist/workspace/intent.d.ts +30 -6
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +141 -21
- package/dist/workspace/intent.schema.json +702 -6
- package/dist/workspace/overlap.d.ts +146 -0
- package/dist/workspace/overlap.d.ts.map +1 -0
- package/dist/workspace/overlap.js +276 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +3 -0
- package/dist/workspace/records.d.ts +15 -2
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +32 -0
- package/dist/workspace/records.schema.json +21 -2
- package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/dist/workspace/releases.d.ts +9 -0
- package/dist/workspace/releases.d.ts.map +1 -1
- package/dist/workspace/releases.js +9 -0
- package/dist/workspace/releases.schema.json +123 -1
- package/dist/workspace/run-event.schema.json +10 -3
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +78 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +170 -3
- package/dist/workspace/status.schema.json +198 -1
- package/dist/workspace/wip.d.ts.map +1 -1
- package/dist/workspace/wip.js +11 -3
- package/package.json +1 -1
- package/src/build-params.test.ts +46 -0
- package/src/build-params.ts +55 -1
- package/src/change-set-graph.ts +10 -2
- package/src/change-set.ts +14 -3
- package/src/cli/build-params-cli.ts +22 -9
- package/src/cli/commands/build.ts +6 -3
- package/src/cli/handlers/approve-release.test.ts +5 -1
- package/src/cli/handlers/approve-revoked.test.ts +155 -0
- package/src/cli/handlers/approve.ts +47 -0
- package/src/cli/handlers/build.test.ts +15 -0
- package/src/cli/handlers/build.ts +17 -4
- package/src/cli/handlers/fan-out.test.ts +180 -1
- package/src/cli/handlers/fan-out.ts +38 -3
- package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
- package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
- package/src/cli/handlers/lifecycle-gc.ts +146 -0
- package/src/cli/handlers/pr.test.ts +148 -1
- package/src/cli/handlers/pr.ts +139 -28
- package/src/cli/handlers/promote.test.ts +280 -2
- package/src/cli/handlers/promote.ts +210 -34
- package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
- package/src/cli/handlers/release-join.e2e.test.ts +185 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
- package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
- package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
- package/src/cli/handlers/release.e2e.test.ts +146 -32
- package/src/cli/handlers/release.ts +415 -28
- package/src/cli/handlers/run-auto-release.test.ts +9 -0
- package/src/cli/handlers/run-release.test.ts +55 -0
- package/src/cli/handlers/run.ts +69 -5
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +61 -10
- package/src/cli/parse-args.ts +19 -2
- package/src/cli/registry.ts +15 -3
- package/src/cli/run-events.ts +5 -2
- package/src/components/auto-release.test.ts +9 -0
- package/src/components/auto-release.ts +3 -0
- package/src/components/capability.ts +9 -0
- package/src/components/driver.test.ts +24 -0
- package/src/components/driver.ts +36 -0
- package/src/components/fan-out-inputs.test.ts +85 -0
- package/src/components/fan-out-inputs.ts +192 -0
- package/src/components/fan-out-record.test.ts +10 -0
- package/src/components/fan-out-record.ts +19 -0
- package/src/components/fan-out-run.ts +48 -0
- package/src/components/fan-out.ts +4 -0
- package/src/components/pr-pipeline.ts +35 -4
- package/src/components/promote.ts +68 -2
- package/src/components/release-compile.ts +17 -3
- package/src/components/release-gate.ts +18 -4
- package/src/components/wave-plan.ts +15 -2
- package/src/config.ts +8 -0
- package/src/flag-params.test.ts +144 -0
- package/src/flag-params.ts +134 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
- package/src/lifecycle/change-set-store.ts +2 -1
- package/src/lifecycle/delivery-metrics.ts +263 -0
- package/src/lifecycle/deploy-outcome.ts +147 -0
- package/src/lifecycle/gate-change-set.ts +37 -6
- package/src/lifecycle/gate-ledger.ts +2 -2
- package/src/lifecycle/git.ts +61 -8
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/mixed-environment.test.ts +97 -0
- package/src/lifecycle/mixed-environment.ts +204 -0
- package/src/lifecycle/publish-attachments.ts +196 -0
- package/src/lifecycle/publish-credentials.ts +84 -0
- package/src/lifecycle/publish-declaration.ts +95 -0
- package/src/lifecycle/publish-forge.ts +325 -0
- package/src/lifecycle/publish-npm.ts +259 -0
- package/src/lifecycle/publish-oci.ts +396 -0
- package/src/lifecycle/publish-record.ts +113 -0
- package/src/lifecycle/publish-resume.test.ts +168 -0
- package/src/lifecycle/publish-targets.test.ts +494 -0
- package/src/lifecycle/publish-targets.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +74 -2
- package/src/lifecycle/release-cut.ts +158 -22
- package/src/lifecycle/release-flag.test.ts +112 -0
- package/src/lifecycle/release-join.test.ts +68 -0
- package/src/lifecycle/release-join.ts +182 -0
- package/src/lifecycle/release-publish.test.ts +225 -0
- package/src/lifecycle/release-publish.ts +568 -0
- package/src/lifecycle/release-revocation.ts +147 -0
- package/src/lifecycle/release-store.ts +15 -4
- package/src/lifecycle/release-verify.test.ts +110 -0
- package/src/lifecycle/release-verify.ts +120 -0
- package/src/lifecycle/release-workstation.test.ts +204 -0
- package/src/lifecycle/release-workstation.ts +216 -0
- package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
- package/src/lifecycle/retention-policy.ts +63 -0
- package/src/lifecycle/retention.test.ts +363 -0
- package/src/lifecycle/retention.ts +508 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-loop.ts +25 -4
- package/src/pr-report-url.test.ts +131 -0
- package/src/pr-report-url.ts +209 -0
- package/src/provenance.ts +2 -2
- package/src/run-event-store.ts +63 -3
- package/src/run-event-stream.ts +4 -1
- package/src/run-event.ts +21 -1
- package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
- package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
- package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
- package/src/workspace/change-set.schema.json +5 -1
- package/src/workspace/check.schema.json +4 -2
- package/src/workspace/checks/records.ts +35 -1
- package/src/workspace/checks.ts +2 -1
- package/src/workspace/ci-green.ts +14 -3
- package/src/workspace/declaration.schema.json +64 -0
- package/src/workspace/declaration.test.ts +28 -0
- package/src/workspace/declaration.ts +38 -0
- package/src/workspace/declared-kinds.ts +40 -1
- package/src/workspace/delivery-metrics.schema.json +519 -0
- package/src/workspace/delivery-metrics.test.ts +236 -0
- package/src/workspace/delivery-metrics.ts +286 -0
- package/src/workspace/flags-read.ts +121 -0
- package/src/workspace/flags.test.ts +141 -0
- package/src/workspace/flags.ts +157 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/intent-cli.ts +46 -5
- package/src/workspace/intent-trace.test.ts +173 -0
- package/src/workspace/intent-trace.ts +484 -0
- package/src/workspace/intent.schema.json +702 -6
- package/src/workspace/intent.ts +148 -25
- package/src/workspace/older-records.test.ts +412 -0
- package/src/workspace/overlap.test.ts +165 -0
- package/src/workspace/overlap.ts +340 -0
- package/src/workspace/read-contract.e2e.test.ts +2 -1
- package/src/workspace/reason-codes.test.ts +5 -0
- package/src/workspace/reason-codes.ts +3 -0
- package/src/workspace/record-assets.test.ts +2 -1
- package/src/workspace/records-contract.e2e.test.ts +2 -1
- package/src/workspace/records.schema.json +21 -2
- package/src/workspace/records.ts +36 -2
- package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/src/workspace/releases.schema.json +123 -1
- package/src/workspace/releases.ts +11 -0
- package/src/workspace/run-event.schema.json +10 -3
- package/src/workspace/status-contract.test.ts +29 -0
- package/src/workspace/status-gates.ts +8 -0
- package/src/workspace/status-mixed.test.ts +152 -0
- package/src/workspace/status-revoked.test.ts +153 -0
- package/src/workspace/status.schema.json +198 -1
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -233,7 +233,8 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
|
|
|
233
233
|
|
|
234
234
|
ran = [];
|
|
235
235
|
stderrLines = [];
|
|
236
|
-
|
|
236
|
+
// The same environment: progress is bound to it (#4081).
|
|
237
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, dryRun: true }))).toBe(0);
|
|
237
238
|
|
|
238
239
|
expect(ran).toEqual([]);
|
|
239
240
|
const out = stderr();
|
|
@@ -656,6 +657,46 @@ describe("--events-json (#4041)", () => {
|
|
|
656
657
|
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
|
|
657
658
|
});
|
|
658
659
|
|
|
660
|
+
test("a component behind a failure settles blocked, naming it, after the failing wave and before the next starts (#4007)", async () => {
|
|
661
|
+
failing = ["cluster-a"];
|
|
662
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", eventsJson: true }))).toBe(1);
|
|
663
|
+
const events = checkRunEventLines(lines());
|
|
664
|
+
const blocked = events.filter((e) => e.type === "component-settle" && e.status === "blocked");
|
|
665
|
+
expect(blocked.map((e) => ({ wave: (e as { wave: number }).wave, component: (e as { component: string }).component, blockedBy: (e as { blockedBy?: string }).blockedBy }))).toEqual([
|
|
666
|
+
{ wave: 3, component: "app-one", blockedBy: "cluster-a" },
|
|
667
|
+
{ wave: 3, component: "app-two", blockedBy: "cluster-a" },
|
|
668
|
+
]);
|
|
669
|
+
const wave2Settle = events.findIndex((e) => e.type === "wave-settle" && e.wave === 2);
|
|
670
|
+
const wave3Start = events.findIndex((e) => e.type === "wave-start" && e.wave === 3);
|
|
671
|
+
expect(events.indexOf(blocked[0]!)).toBeGreaterThan(wave2Settle);
|
|
672
|
+
expect(events.indexOf(blocked[1]!)).toBeLessThan(wave3Start);
|
|
673
|
+
// A blocked component never starts.
|
|
674
|
+
expect(events.some((e) => e.type === "component-start" && e.component === "app-one")).toBe(false);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("a resumed fan-out settles what the earlier attempt applied as already-applied, in the derivation's waves (#4007)", async () => {
|
|
678
|
+
const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
|
|
679
|
+
try {
|
|
680
|
+
const resume = join(dir, "attempt.json");
|
|
681
|
+
failing = ["cluster-a"];
|
|
682
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
|
|
683
|
+
failing = [];
|
|
684
|
+
stdoutLines = [];
|
|
685
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, eventsJson: true }))).toBe(0);
|
|
686
|
+
const events = checkRunEventLines(lines());
|
|
687
|
+
const firstWave = events.findIndex((e) => e.type === "wave-start");
|
|
688
|
+
const applied = events.filter((e) => e.type === "component-settle" && e.status === "already-applied");
|
|
689
|
+
expect(applied.map((e) => [(e as { component: string }).component, (e as { wave: number }).wave])).toEqual([["app-three", 3], ["cluster-b", 2], ["net", 1]]);
|
|
690
|
+
for (const e of applied) expect(events.indexOf(e)).toBeLessThan(firstWave);
|
|
691
|
+
// --progress-json never printed them, and still does not.
|
|
692
|
+
stdoutLines = [];
|
|
693
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, progressJson: true }))).toBe(0);
|
|
694
|
+
expect(stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l).type)).toEqual(["run-start", "run-done"]);
|
|
695
|
+
} finally {
|
|
696
|
+
await rm(dir, { recursive: true, force: true });
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
659
700
|
test("--progress-json on a set the gate stops now prints run-start and run-done gated (a level-0 change)", async () => {
|
|
660
701
|
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", progressJson: true }))).toBe(3);
|
|
661
702
|
const progress = stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l));
|
|
@@ -670,3 +711,141 @@ describe("--events-json (#4041)", () => {
|
|
|
670
711
|
expect(stderr()).toContain("--events-json and --progress-json both stream to stdout");
|
|
671
712
|
});
|
|
672
713
|
});
|
|
714
|
+
|
|
715
|
+
describe("progress carries only for an unchanged input (#4081)", () => {
|
|
716
|
+
/** The estate with each component deploying a built template from `templates/`, read relative to the project. */
|
|
717
|
+
const templated = ESTATE.map((c) => ({
|
|
718
|
+
...c,
|
|
719
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: `${c.name}-stack`, template: `templates/${c.name}.json` }] }],
|
|
720
|
+
}));
|
|
721
|
+
let dir: string;
|
|
722
|
+
const writeTemplate = (name: string, body: unknown) =>
|
|
723
|
+
writeFile(join(dir, "templates", `${name}.json`), JSON.stringify(body));
|
|
724
|
+
|
|
725
|
+
beforeEach(async () => {
|
|
726
|
+
dir = await mkdtemp(join(tmpdir(), "fan-out-inputs-"));
|
|
727
|
+
const { mkdir } = await import("node:fs/promises");
|
|
728
|
+
await mkdir(join(dir, "templates"));
|
|
729
|
+
for (const c of templated) await writeTemplate(c.name, { Resources: { [c.name]: { Type: "AWS::SNS::Topic" } } });
|
|
730
|
+
// The project is the temp directory, so the templates are read from there and nothing touches the checkout.
|
|
731
|
+
vi.spyOn(process, "cwd").mockReturnValue(dir);
|
|
732
|
+
discoverComponentsMock.mockResolvedValue({
|
|
733
|
+
components: new Map(templated.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
|
|
734
|
+
sourceFiles: [],
|
|
735
|
+
errors: [],
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
afterEach(async () => {
|
|
739
|
+
await rm(dir, { recursive: true, force: true });
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("same shape, changed content: a stale record does not skip the components whose input moved", async () => {
|
|
743
|
+
const resume = join(dir, "attempt.json");
|
|
744
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
745
|
+
const first = JSON.parse(await readFile(resume, "utf8"));
|
|
746
|
+
expect(first.completed).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
|
|
747
|
+
expect(Object.keys(first.inputs).sort()).toEqual(first.completed);
|
|
748
|
+
const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
|
|
749
|
+
|
|
750
|
+
// The next change: net's topic gets a display name and cluster-a a queue.
|
|
751
|
+
// Same components, same edges, so the same plan digest.
|
|
752
|
+
await writeTemplate("net", { Resources: { net: { Type: "AWS::SNS::Topic", Properties: { DisplayName: "estate" } } } });
|
|
753
|
+
await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
|
|
754
|
+
ran = [];
|
|
755
|
+
stderrLines = [];
|
|
756
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
757
|
+
|
|
758
|
+
expect([...ran].sort()).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
|
|
759
|
+
const out = stderr();
|
|
760
|
+
expect(out).toContain(`plan: ${digest}`);
|
|
761
|
+
expect(out).toContain("net: changed since the earlier attempt, so it runs again");
|
|
762
|
+
expect(out).toContain("cluster-a: changed since the earlier attempt, so it runs again");
|
|
763
|
+
// Downstream of net, so its parameters read a changed upstream.
|
|
764
|
+
expect(out).toContain("app-three: changed since the earlier attempt, so it runs again");
|
|
765
|
+
expect(out).not.toMatch(/already-applied/);
|
|
766
|
+
expect(out).toContain("fan-out completed: 6 applied, 0 failed, 0 blocked");
|
|
767
|
+
const second = JSON.parse(await readFile(resume, "utf8"));
|
|
768
|
+
expect(second.inputs.net).not.toBe(first.inputs.net);
|
|
769
|
+
expect(second.inputs["cluster-b"]).not.toBe(first.inputs["cluster-b"]);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test("a change below the root reruns only its own subtree", async () => {
|
|
773
|
+
const resume = join(dir, "attempt.json");
|
|
774
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
775
|
+
await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
|
|
776
|
+
ran = [];
|
|
777
|
+
stderrLines = [];
|
|
778
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
779
|
+
expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
|
|
780
|
+
expect(stderr()).toMatch(/cluster-b +already-applied/);
|
|
781
|
+
expect(stderr()).not.toContain("net: changed");
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("the README's fail-then-fix: the fix reruns the failed subtree and the rest stay already-applied", async () => {
|
|
785
|
+
const resume = join(dir, "attempt.json");
|
|
786
|
+
failing = ["cluster-a"];
|
|
787
|
+
await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events" } } } });
|
|
788
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
|
|
789
|
+
const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
|
|
790
|
+
|
|
791
|
+
// The fix changes cluster-a's template, which never applied.
|
|
792
|
+
failing = [];
|
|
793
|
+
await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events.fifo" } } } });
|
|
794
|
+
ran = [];
|
|
795
|
+
stderrLines = [];
|
|
796
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
797
|
+
expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
|
|
798
|
+
const out = stderr();
|
|
799
|
+
expect(out).toContain(`plan: ${digest}`);
|
|
800
|
+
expect(out).not.toContain("changed since the earlier attempt");
|
|
801
|
+
for (const name of ["net", "cluster-b", "app-three"]) expect(out).toMatch(new RegExp(`${name} +already-applied`));
|
|
802
|
+
expect(out).toContain("fan-out completed: 3 applied, 0 failed, 0 blocked");
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("the fix keeps the approval: approval binds the derivation, progress binds the inputs", async () => {
|
|
806
|
+
const resume = join(dir, "attempt.json");
|
|
807
|
+
failing = ["cluster-a"];
|
|
808
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(3);
|
|
809
|
+
const pending = gateLedger.appended[0]!;
|
|
810
|
+
gateLedger = memoryGateLedgerPort({
|
|
811
|
+
pending: [pending],
|
|
812
|
+
resolutions: [
|
|
813
|
+
{
|
|
814
|
+
version: 1,
|
|
815
|
+
kind: "resolution",
|
|
816
|
+
op: "fan-out",
|
|
817
|
+
gate: "release",
|
|
818
|
+
resolvedBy: "operator",
|
|
819
|
+
timestamp: new Date(Date.parse(pending.timestamp) + 1000).toISOString(),
|
|
820
|
+
planDigest: pending.planDigest,
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
});
|
|
824
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(1);
|
|
825
|
+
|
|
826
|
+
failing = [];
|
|
827
|
+
await writeTemplate("cluster-a", { Resources: { fixed: { Type: "AWS::SQS::Queue" } } });
|
|
828
|
+
ran = [];
|
|
829
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(0);
|
|
830
|
+
expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("the input digest is per environment: progress against one does not carry into another", async () => {
|
|
834
|
+
const resume = join(dir, "attempt.json");
|
|
835
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
836
|
+
ran = [];
|
|
837
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "prod", resume }))).toBe(0);
|
|
838
|
+
expect(ran).toHaveLength(6);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test("a record without input digests carries nothing, and says so", async () => {
|
|
842
|
+
const resume = join(dir, "attempt.json");
|
|
843
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
|
|
844
|
+
const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())![1]!;
|
|
845
|
+
await writeFile(resume, JSON.stringify({ digest, completed: ["net", "cluster-b"], failed: [], outputs: {} }));
|
|
846
|
+
stderrLines = [];
|
|
847
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
848
|
+
expect(ran).toHaveLength(6);
|
|
849
|
+
expect(stderr()).toContain("does not record what each component applied, so none of its progress carries");
|
|
850
|
+
});
|
|
851
|
+
});
|
|
@@ -34,6 +34,15 @@
|
|
|
34
34
|
* the plan before the gate is decided, and the digest is carried rather than
|
|
35
35
|
* recomputed, so the approval still stands.
|
|
36
36
|
*
|
|
37
|
+
* The approval and the progress are bound to different things (#4081). The
|
|
38
|
+
* approval is bound to the plan digest, the derivation: who runs, in what
|
|
39
|
+
* order, off which edges. The progress is bound to each component's input
|
|
40
|
+
* digest (../../components/fan-out-inputs.ts): a component the earlier
|
|
41
|
+
* attempt applied stays `already-applied` only while what it would apply is
|
|
42
|
+
* the same, and one whose template, step inputs or upstream moved runs again
|
|
43
|
+
* with a line saying so. The fix after a failure keeps the approval, and a
|
|
44
|
+
* different change of the same shape does not inherit the progress.
|
|
45
|
+
*
|
|
37
46
|
* `--wave-gate <name>` (#3049) swaps the one gate for one per wave: the same
|
|
38
47
|
* repeated command stops at each wave's gate in turn, each planned after the
|
|
39
48
|
* wave before it applied, and `chant approve fan-out <name>-wave-<n>` answers
|
|
@@ -49,6 +58,7 @@ import { changedFilesBetween, lexiconChangedUnits } from "../../components/chang
|
|
|
49
58
|
import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
|
|
50
59
|
import { EarlierWaveNotAppliedError, runFanOut } from "../../components/fan-out-run";
|
|
51
60
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../../components/fan-out-record";
|
|
61
|
+
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
52
62
|
import { describeChangedWave, withWaveRecord, type WaveRecord } from "../../gated-waves";
|
|
53
63
|
import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
|
|
54
64
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
@@ -253,7 +263,12 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
253
263
|
// real run happens inside `runFanOut`, before the gate is decided, so an
|
|
254
264
|
// approval survives the resume rather than being re-asked for.
|
|
255
265
|
const resumePath = args.resume ? resolve(args.resume) : undefined;
|
|
266
|
+
// What each component would apply now (#4081). The record keeps it for
|
|
267
|
+
// every component that applies, and a later attempt carries progress only
|
|
268
|
+
// where it is unchanged.
|
|
269
|
+
const inputsNow = resumePath ? componentInputDigests(derived.components, { projectRoot: projectPath, env: args.env ?? "local" }) : {};
|
|
256
270
|
let priorCompleted: string[] = [];
|
|
271
|
+
let priorInputs: Record<string, string> = {};
|
|
257
272
|
let priorOutputs: Record<string, Record<string, unknown>> = {};
|
|
258
273
|
let priorWaves: WaveRecord[] = [];
|
|
259
274
|
let priorCarried: Record<string, unknown> = {};
|
|
@@ -277,11 +292,28 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
277
292
|
// as progress would block their subtrees again on the very run that
|
|
278
293
|
// exists to retry them — the operator repeated the command precisely
|
|
279
294
|
// because the thing that failed is now expected to work.
|
|
280
|
-
|
|
295
|
+
//
|
|
296
|
+
// And only the part of `completed` whose input is the same now (#4081).
|
|
297
|
+
// The plan digest identifies the derivation, which a different change
|
|
298
|
+
// of the same shape shares; the input digest says what was applied.
|
|
299
|
+
const split = progressThatCarries(attempt, inputsNow);
|
|
300
|
+
if (split.unrecorded && split.changed.length > 0) {
|
|
301
|
+
console.error(formatWarning({
|
|
302
|
+
message: `--resume: "${args.resume}" does not record what each component applied, so none of its progress carries`,
|
|
303
|
+
hint: "It was written before chant recorded component inputs. Everything selected will run.",
|
|
304
|
+
}));
|
|
305
|
+
} else {
|
|
306
|
+
for (const name of split.changed) {
|
|
307
|
+
console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const carried = new Set(split.carried);
|
|
311
|
+
priorCompleted = split.carried;
|
|
312
|
+
priorInputs = Object.fromEntries(split.carried.map((name) => [name, inputsNow[name]!]));
|
|
281
313
|
priorOutputs = Object.fromEntries(
|
|
282
|
-
Object.entries(attempt.outputs).filter(([name]) =>
|
|
314
|
+
Object.entries(attempt.outputs).filter(([name]) => carried.has(name)),
|
|
283
315
|
);
|
|
284
|
-
progress = { completed:
|
|
316
|
+
progress = { completed: split.carried };
|
|
285
317
|
priorWaves = attempt.waves ?? [];
|
|
286
318
|
// What steps kept per member (#3459), such as a choudoufu root's wave
|
|
287
319
|
// resume file. Each step decides whether what it kept still applies.
|
|
@@ -319,6 +351,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
319
351
|
// runner, ctrl-c) then still knows that the first two applied, and the
|
|
320
352
|
// repeated command picks up from there instead of redoing them.
|
|
321
353
|
const completedSoFar = new Set(priorCompleted);
|
|
354
|
+
const inputsSoFar: Record<string, string> = { ...priorInputs };
|
|
322
355
|
const failedSoFar: string[] = [];
|
|
323
356
|
const outputsSoFar = { ...priorOutputs };
|
|
324
357
|
let wavesSoFar = priorWaves;
|
|
@@ -330,6 +363,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
330
363
|
completed: [...completedSoFar].sort(),
|
|
331
364
|
failed: [...failedSoFar].sort(),
|
|
332
365
|
outputs: outputsSoFar,
|
|
366
|
+
inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
|
|
333
367
|
...(wavesSoFar.length > 0 ? { waves: wavesSoFar } : {}),
|
|
334
368
|
...(Object.keys(carriedSoFar).length > 0 ? { carried: carriedSoFar } : {}),
|
|
335
369
|
};
|
|
@@ -377,6 +411,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
377
411
|
onComponentSettled: (settled, outputs) => {
|
|
378
412
|
if (settled.status === "ok") {
|
|
379
413
|
completedSoFar.add(settled.component);
|
|
414
|
+
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
380
415
|
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
381
416
|
} else if (settled.status === "fail") {
|
|
382
417
|
failedSoFar.push(settled.component);
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The why chain of `graph --intent` (#4023), from work item to production
|
|
3
|
+
* release. Everything runs in a temp repo with no remote, so nothing is
|
|
4
|
+
* pushed: a workspace with a work item, a commit whose trailer links it, a
|
|
5
|
+
* green tag, a real cut, a real `components promote --release` into prod that
|
|
6
|
+
* stops at its release gate, a real `chant approve`, and the promote that
|
|
7
|
+
* follows. Only the capability registry is replaced, so no deploy step
|
|
8
|
+
* reaches a cloud, and the member's graph, so the resource node resolves to
|
|
9
|
+
* its component file without building the member.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
13
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { CapabilityRegistry, type DeployContext } from "../../components/capability";
|
|
18
|
+
import type { DriverComponent } from "../../components/driver";
|
|
19
|
+
import type { ParsedArgs } from "../registry";
|
|
20
|
+
import { appendReleaseRecord, readReleaseLedger } from "../../lifecycle/release-ledger";
|
|
21
|
+
import { readGateLedger } from "../../lifecycle/gate-ledger";
|
|
22
|
+
import { cutRelease } from "../../lifecycle/release-cut";
|
|
23
|
+
import { resetGateOrigin } from "../../lifecycle/gate-origin";
|
|
24
|
+
import { contract, REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
25
|
+
import { intentGraph, type IntentDocument } from "../../workspace/intent";
|
|
26
|
+
import intentSchema from "../../workspace/intent.schema.json";
|
|
27
|
+
import type { ReleaseArtifact } from "../../lifecycle/release-store";
|
|
28
|
+
import type { CompiledTarget, TargetCompiler } from "../../components/release-compile";
|
|
29
|
+
|
|
30
|
+
const DIGEST = `sha256:${"0123456789abcdef".repeat(4)}`;
|
|
31
|
+
|
|
32
|
+
const api: DriverComponent = {
|
|
33
|
+
name: "api",
|
|
34
|
+
deploy: [
|
|
35
|
+
{ phase: "Publish", steps: [{ kind: "publish-image", from: "archive:api.tar" }] },
|
|
36
|
+
{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
vi.mock("../../config", async () => {
|
|
41
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
42
|
+
return { ...actual, loadChantConfig: async () => ({ config: {} }) };
|
|
43
|
+
});
|
|
44
|
+
vi.mock("../../components/cli-support", () => ({
|
|
45
|
+
isComponentList: () => false,
|
|
46
|
+
resolveComponentTargets: async () => ({ success: true, targets: [api] }),
|
|
47
|
+
}));
|
|
48
|
+
vi.mock("../../components/fan-out-support", () => ({
|
|
49
|
+
fanOutRegistry: async () => {
|
|
50
|
+
const registry = new CapabilityRegistry();
|
|
51
|
+
registry.register({ kind: "publish-image", async run(_ctx: DeployContext) { return { digest: DIGEST, uri: `reg/api@${DIGEST}` }; } } as never);
|
|
52
|
+
registry.register({ kind: "apply", async run() { return {}; } } as never);
|
|
53
|
+
return registry;
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
|
|
57
|
+
vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
|
|
58
|
+
|
|
59
|
+
const { runComponentsPromote } = await import("./promote");
|
|
60
|
+
const { recordGateApproval } = await import("./approve");
|
|
61
|
+
|
|
62
|
+
function git(dir: string, ...args: string[]): string {
|
|
63
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
64
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
65
|
+
return r.stdout.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A compiler that compiles each environment to its own template and builds one image. */
|
|
69
|
+
const compiler: TargetCompiler = {
|
|
70
|
+
async compile(env): Promise<CompiledTarget> {
|
|
71
|
+
return {
|
|
72
|
+
env,
|
|
73
|
+
params: [],
|
|
74
|
+
components: ["api"],
|
|
75
|
+
buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
|
|
76
|
+
buildInputs: { api: ["api"] },
|
|
77
|
+
componentFiles: { api: "api.component.ts" },
|
|
78
|
+
templates: [{ component: "api", path: "dist/api.template.json", content: `{"env":"${env}"}\n` }],
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
|
|
82
|
+
return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: DIGEST }));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function args(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
87
|
+
return {
|
|
88
|
+
command: "components", path: "promote", format: "", fix: false, watch: false, verbose: false, help: false, live: false,
|
|
89
|
+
migrateFrom: "staging", migrateTo: "prod", actor: "bob", runId: "run-9", ...overrides,
|
|
90
|
+
} as ParsedArgs;
|
|
91
|
+
}
|
|
92
|
+
const ctx = (a: Partial<ParsedArgs>) => ({ args: args(a), plugins: [] }) as never;
|
|
93
|
+
|
|
94
|
+
const REF = join(REPO, "reference-workspace");
|
|
95
|
+
const KIND_FILES: Record<string, string> = {
|
|
96
|
+
"work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
|
|
97
|
+
"work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
|
|
98
|
+
"decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
|
|
99
|
+
"decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
|
|
100
|
+
};
|
|
101
|
+
const W001 = `---\n${JSON.stringify({ schema: 1, id: "W-001", title: "Ship the api to prod", state: "open", implements: [], needs: [], constrains: ["path:api.component.ts"], evidence: [], opened_on: "2026-10-16", supersedes: [] }, null, 2)}\n---\n\n# W-001\n`;
|
|
102
|
+
const DECLARATION = JSON.stringify({ name: "shop", schema: 1, members: [{ name: "api", dir: ".", kind: "other", because: "the api and its component" }] });
|
|
103
|
+
const intent = contract(intentSchema);
|
|
104
|
+
type Result = Exclude<IntentDocument, { error: unknown }>;
|
|
105
|
+
|
|
106
|
+
describe("the why chain from work item to production release (#4023)", () => {
|
|
107
|
+
const cwd = process.cwd();
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
process.chdir(cwd);
|
|
110
|
+
resetGateOrigin();
|
|
111
|
+
vi.restoreAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test(
|
|
115
|
+
"the trace from the api's resource node walks W-001, the linked commit, its green tag, the cut, the release gate approval and the prod deploy",
|
|
116
|
+
async () => {
|
|
117
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
118
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
119
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
120
|
+
await withTestDir(async (dir) => {
|
|
121
|
+
git(dir, "init", "-q", "-b", "main");
|
|
122
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
123
|
+
git(dir, "config", "user.name", "T");
|
|
124
|
+
const files: Record<string, string> = {
|
|
125
|
+
"chant.workspace.json": DECLARATION,
|
|
126
|
+
"chant.config.ts": `export default { environments: ["dev", "staging", "prod"] };\n`,
|
|
127
|
+
"api.component.ts": "export const api = {};\n",
|
|
128
|
+
"work/W-001-ship.md": W001,
|
|
129
|
+
...KIND_FILES,
|
|
130
|
+
};
|
|
131
|
+
for (const [path, text] of Object.entries(files)) {
|
|
132
|
+
mkdirSync(join(dir, path, ".."), { recursive: true });
|
|
133
|
+
writeFileSync(join(dir, path), text);
|
|
134
|
+
}
|
|
135
|
+
git(dir, "add", ".");
|
|
136
|
+
git(dir, "commit", "-q", "-m", "the api and W-001");
|
|
137
|
+
// The work: a commit whose trailer names W-001, made green.
|
|
138
|
+
writeFileSync(join(dir, "api.component.ts"), "export const api = { port: 8081 };\n");
|
|
139
|
+
git(dir, "commit", "-q", "-am", "serve the api on 8081", "--trailer", "Chant-Record: work:W-001");
|
|
140
|
+
const sha = git(dir, "rev-parse", "HEAD");
|
|
141
|
+
git(dir, "tag", `ci/green/${sha}`, sha);
|
|
142
|
+
process.chdir(dir);
|
|
143
|
+
|
|
144
|
+
// The release is cut from the green commit, and promoted to prod through its release gate.
|
|
145
|
+
const cut = await cutRelease({ cwd: dir, compiler, label: "v1.0.0", actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
|
|
146
|
+
const releaseDigest = cut.release.releaseDigest;
|
|
147
|
+
await appendReleaseRecord({ component: "api", env: "staging", digest: DIGEST, gitSha: sha, runId: "run-1", timestamp: "2026-10-16T10:30:00.000Z", actor: "ci" }, { cwd: dir });
|
|
148
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
149
|
+
expect((await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" })).ok).toBe(true);
|
|
150
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
151
|
+
const [deploy] = (await readReleaseLedger("prod", { cwd: dir })).records;
|
|
152
|
+
const [resolution] = (await readGateLedger("release", { cwd: dir })).resolutions;
|
|
153
|
+
|
|
154
|
+
const walk = async (region: string): Promise<Result> => {
|
|
155
|
+
const { doc } = await intentGraph({
|
|
156
|
+
cwd: dir,
|
|
157
|
+
region,
|
|
158
|
+
kinds: [join(dir, "work/work.kind.mjs")],
|
|
159
|
+
resolveNode: async (_cwd, _at, member, id) => (member === "api" && id === "api/api" ? { file: "api.component.ts", line: null } : null),
|
|
160
|
+
});
|
|
161
|
+
intent.expectValid(doc);
|
|
162
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
163
|
+
return doc;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// From the resource node: who asked for it, and who let it ship, in one read.
|
|
167
|
+
const doc = await walk("api/api");
|
|
168
|
+
expect(doc.nodes[0]).toMatchObject({ kind: "region", path: "api.component.ts", node: "api/api" });
|
|
169
|
+
const trace = doc.trace!;
|
|
170
|
+
expect(trace.reasons).toEqual([]);
|
|
171
|
+
const c = trace.commits.find((x) => x.sha === sha)!;
|
|
172
|
+
expect(c.work).toEqual({ state: "linked", items: [{ id: "work:W-001", node: "record:work/W-001", source: { ref: sha, field: "Chant-Record", value: "work:W-001" } }], problems: [] });
|
|
173
|
+
expect(c.tag).toMatchObject({ state: "green", green: { tag: `ci/green/${sha}` } });
|
|
174
|
+
expect(c.releases).toEqual([{ release: releaseDigest, link: "cut-from", source: { ref: `chant/lifecycle:_releases/${releaseDigest.replace(/:/g, "_")}.json`, field: "commit", value: sha } }]);
|
|
175
|
+
expect(c.deploys.map((d) => [d.env, d.link])).toEqual([["prod", "built-from"], ["staging", "built-from"]]);
|
|
176
|
+
|
|
177
|
+
const [release] = trace.releases;
|
|
178
|
+
expect(trace.releases).toHaveLength(1);
|
|
179
|
+
expect(release).toMatchObject({ releaseDigest, label: "v1.0.0", commit: sha, tag: { state: "green" }, cuts: [{ label: "v1.0.0", actor: "alice" }] });
|
|
180
|
+
// The gate approval: who, on which channel, when, and the change set the promote stored for it (#4022).
|
|
181
|
+
expect(release.gates).toEqual([
|
|
182
|
+
expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", resolvedBy: "alice", channel: "cli", at: resolution.timestamp, planDigest: resolution.planDigest, changeSet: resolution.planDigest, empty: false }),
|
|
183
|
+
]);
|
|
184
|
+
expect(release.gates[0].changeSetSource).toEqual({ ref: `chant/lifecycle:_change-sets/${resolution.planDigest!.replace(/:/g, "_")}.json`, field: "planDigest", value: resolution.planDigest });
|
|
185
|
+
|
|
186
|
+
// The prod deploy record, admitted by that resolution.
|
|
187
|
+
const prod = trace.deploys.find((d) => d.env === "prod")!;
|
|
188
|
+
expect(release.deploys).toEqual([prod.id]);
|
|
189
|
+
expect(prod).toMatchObject({ component: "api", runId: deploy.runId, gitSha: sha, release: releaseDigest, approver: "alice", admittedBy: { op: "release", gate: "prod-release", resolvedBy: "alice", at: resolution.timestamp } });
|
|
190
|
+
// Staging was a plain deploy of the same commit: it names no release.
|
|
191
|
+
expect(trace.deploys.find((d) => d.env === "staging")).toMatchObject({ release: null, admittedBy: null });
|
|
192
|
+
|
|
193
|
+
// The same chain from the commit itself.
|
|
194
|
+
const byCommit = await walk(sha);
|
|
195
|
+
expect(byCommit.trace!.commits).toEqual([c]);
|
|
196
|
+
expect(byCommit.trace!.releases).toEqual(trace.releases);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
240_000,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** #4036: `chant lifecycle gc [--yes] [--push] [--json]`, in temp repos. */
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
8
|
+
import type { ParsedArgs } from "../registry";
|
|
9
|
+
import { runLifecycleGc } from "./lifecycle-gc";
|
|
10
|
+
import { writeLedgerFiles } from "../../lifecycle/git";
|
|
11
|
+
import { makeRelease } from "../../lifecycle/release-store";
|
|
12
|
+
import type { GcReport } from "../../lifecycle/retention";
|
|
13
|
+
|
|
14
|
+
function args(o: Partial<ParsedArgs>): ParsedArgs {
|
|
15
|
+
return { command: "lifecycle", path: ".", format: "", fix: false, watch: false, verbose: false, help: false, live: false, ...o };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const git = (cwd: string, ...a: string[]) => execFileSync("git", a, { cwd, encoding: "utf-8" }).trim();
|
|
19
|
+
|
|
20
|
+
let dir: string;
|
|
21
|
+
let cwd: string;
|
|
22
|
+
let out: string[];
|
|
23
|
+
let err: string[];
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
cwd = process.cwd();
|
|
26
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "lifecycle-gc-")));
|
|
27
|
+
vi.stubEnv("CHANT_RUN_EVENTS_DIR", join(dir, ".events"));
|
|
28
|
+
git(dir, "init", "-q", "-b", "main");
|
|
29
|
+
git(dir, "config", "user.email", "t@t");
|
|
30
|
+
git(dir, "config", "user.name", "t");
|
|
31
|
+
process.chdir(dir);
|
|
32
|
+
out = [];
|
|
33
|
+
err = [];
|
|
34
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => void out.push(String(s)));
|
|
35
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => void err.push(String(s)));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
vi.unstubAllEnvs();
|
|
40
|
+
process.chdir(cwd);
|
|
41
|
+
rmSync(dir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function commitAll(): string {
|
|
45
|
+
writeFileSync(join(dir, "README.md"), "x\n");
|
|
46
|
+
git(dir, "add", ".");
|
|
47
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
48
|
+
return git(dir, "rev-parse", "HEAD");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const changeSet = `_change-sets/jcs1-sha256_${"1".repeat(64)}.json`;
|
|
52
|
+
const ctx = (o: Partial<ParsedArgs>) => ({ args: args(o), plugins: [], serializers: [] });
|
|
53
|
+
const report = (): GcReport => JSON.parse(out.join("\n")) as GcReport;
|
|
54
|
+
|
|
55
|
+
describe("chant lifecycle gc", () => {
|
|
56
|
+
test("a dry run by default; --yes prunes in one local commit and pushes nothing", async () => {
|
|
57
|
+
writeFileSync(join(dir, "chant.workspace.json"), JSON.stringify({ name: "w", schema: 1, members: [], retention: { changeSets: { days: 0 } } }));
|
|
58
|
+
commitAll();
|
|
59
|
+
await writeLedgerFiles([{ path: changeSet, content: "{}\n", expectPriorSha: null }], "fixture", { cwd: dir });
|
|
60
|
+
const tip = git(dir, "rev-parse", "chant/lifecycle");
|
|
61
|
+
|
|
62
|
+
expect(await runLifecycleGc(ctx({}))).toBe(0);
|
|
63
|
+
expect(out[0]).toMatch(/^chant\/lifecycle at [0-9a-f]{12}: would remove 1 change set\(s\), 0 release\(s\) and 0 template\(s\)/);
|
|
64
|
+
expect(err.join("\n")).toContain("Dry run: nothing was removed");
|
|
65
|
+
expect(git(dir, "rev-parse", "chant/lifecycle")).toBe(tip);
|
|
66
|
+
|
|
67
|
+
out = [];
|
|
68
|
+
expect(await runLifecycleGc(ctx({ yes: true, json: true }))).toBe(0);
|
|
69
|
+
const r = report();
|
|
70
|
+
expect(r.dryRun).toBe(false);
|
|
71
|
+
expect(r.policy.changeSets.days).toBe(0);
|
|
72
|
+
expect(r.lifecycle.remove.map((f) => f.path)).toEqual([changeSet]);
|
|
73
|
+
expect(r.lifecycle.commit).toBe(git(dir, "rev-parse", "chant/lifecycle"));
|
|
74
|
+
expect(git(dir, "rev-parse", "chant/lifecycle^")).toBe(tip);
|
|
75
|
+
expect(r.lifecycle.pushed).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("outside a workspace the defaults apply, and a release of a revoked commit is not promotable", async () => {
|
|
79
|
+
const head = commitAll();
|
|
80
|
+
const cut = (commit: string) => {
|
|
81
|
+
const r = makeRelease({ member: null, commit, artifactDigests: [], targets: [{ env: "prod", templates: [], params: [] }] });
|
|
82
|
+
return { r, line: JSON.stringify({ version: 1, releaseDigest: r.releaseDigest, label: null, actor: "a", timestamp: new Date().toISOString(), runOrigin: null, signature: null }) };
|
|
83
|
+
};
|
|
84
|
+
const revoked = cut(head);
|
|
85
|
+
const fine = cut("b".repeat(40));
|
|
86
|
+
const file = (d: string) => `_releases/${d.replace(/:/g, "_")}.json`;
|
|
87
|
+
await writeLedgerFiles(
|
|
88
|
+
[
|
|
89
|
+
{ path: changeSet, content: "{}\n" },
|
|
90
|
+
{ path: file(revoked.r.releaseDigest), content: `${JSON.stringify(revoked.r)}\n` },
|
|
91
|
+
{ path: file(fine.r.releaseDigest), content: `${JSON.stringify(fine.r)}\n` },
|
|
92
|
+
{ path: "_releases/cuts.jsonl", content: `${revoked.line}\n${fine.line}\n` },
|
|
93
|
+
],
|
|
94
|
+
"fixture",
|
|
95
|
+
{ cwd: dir },
|
|
96
|
+
);
|
|
97
|
+
git(dir, "tag", `ci/revoked/${head}`, head);
|
|
98
|
+
|
|
99
|
+
expect(await runLifecycleGc(ctx({ json: true }))).toBe(0);
|
|
100
|
+
const r = report();
|
|
101
|
+
expect(r.policy.releases).toEqual({ days: 14, keep: 10 });
|
|
102
|
+
expect(r.lifecycle.remove.map((f) => f.path)).toEqual([file(revoked.r.releaseDigest)]);
|
|
103
|
+
expect(r.lifecycle.keep.map((f) => [f.path, f.reasons.map((x) => x.code)])).toEqual(
|
|
104
|
+
expect.arrayContaining([[changeSet, ["within-window"]], [file(fine.r.releaseDigest), ["newest", "within-window"]]]),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a workspace whose declaration can't be read prunes nothing", async () => {
|
|
109
|
+
writeFileSync(join(dir, "chant.workspace.json"), JSON.stringify({ name: "w", schema: 1, members: [], retention: { changeSets: { days: -1 } } }));
|
|
110
|
+
commitAll();
|
|
111
|
+
expect(await runLifecycleGc(ctx({ yes: true }))).toBe(1);
|
|
112
|
+
expect(err.join("\n")).toContain("its retention policy is unknown and nothing is pruned");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a directory outside git is refused", async () => {
|
|
116
|
+
const plain = realpathSync(mkdtempSync(join(tmpdir(), "lifecycle-gc-plain-")));
|
|
117
|
+
try {
|
|
118
|
+
process.chdir(plain);
|
|
119
|
+
expect(await runLifecycleGc(ctx({}))).toBe(1);
|
|
120
|
+
expect(err.join("\n")).toContain("not in a git repository");
|
|
121
|
+
} finally {
|
|
122
|
+
process.chdir(dir);
|
|
123
|
+
rmSync(plain, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|