@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a promote, rollback or redeploy left behind when it did not deploy
|
|
3
|
+
* every component it planned (#4035).
|
|
4
|
+
*
|
|
5
|
+
* The release ledger records a component only once its deploy succeeded, so
|
|
6
|
+
* after a run that applied partly it still names the earlier release for
|
|
7
|
+
* every component that failed, and nothing says that component may be
|
|
8
|
+
* running part of the new one. A deploy outcome is the record of that run:
|
|
9
|
+
* one line per run in `<env>/outcomes.jsonl` on `chant/lifecycle`, beside
|
|
10
|
+
* the environment's `releases.jsonl` and under the same member prefix,
|
|
11
|
+
* naming each planned component and what happened to it.
|
|
12
|
+
*
|
|
13
|
+
* It is written only when the run ended with some components deployed and
|
|
14
|
+
* others not, or with a component that failed. A run that stopped at a gate
|
|
15
|
+
* before deploying anything changed nothing in the environment; its pending
|
|
16
|
+
* gate is what `workspace status` shows for it.
|
|
17
|
+
*
|
|
18
|
+
* The file is append-only, like the release ledger. Whether the environment
|
|
19
|
+
* is still mixed is derived when it is read ({@link ./mixed-environment.ts}):
|
|
20
|
+
* a component the outcome names as failed or gated stays unsettled until the
|
|
21
|
+
* release ledger has a newer record of it, which a redeploy, a rollback or
|
|
22
|
+
* the same promote run again writes.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { sortedJsonReplacer } from "../utils";
|
|
26
|
+
import type { PromotionPlan } from "../components/promote";
|
|
27
|
+
import type { DriverRunResult } from "../components/driver";
|
|
28
|
+
|
|
29
|
+
/** The file a deploy outcome is appended to, under the environment's directory. */
|
|
30
|
+
export const OUTCOMES_FILE = "outcomes.jsonl";
|
|
31
|
+
|
|
32
|
+
/** What a run did to one component it planned. */
|
|
33
|
+
export type DeployOutcomeState = "deployed" | "failed" | "gated" | "not-run";
|
|
34
|
+
|
|
35
|
+
export interface DeployOutcomeComponent {
|
|
36
|
+
component: string;
|
|
37
|
+
/** `deployed` (a release record was written), `failed`, `gated` (stopped at a gate after the steps before it ran), or `not-run`. */
|
|
38
|
+
outcome: DeployOutcomeState;
|
|
39
|
+
/** The artifact digest the run was deploying. */
|
|
40
|
+
digest: string;
|
|
41
|
+
/** The release the run was deploying for this component, or null when it named none. */
|
|
42
|
+
releaseDigest: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One line of `<env>/outcomes.jsonl`. */
|
|
46
|
+
export interface DeployOutcome {
|
|
47
|
+
version: 1;
|
|
48
|
+
env: string;
|
|
49
|
+
verb: "promote" | "rollback" | "redeploy";
|
|
50
|
+
/** The run id the run's release records carry. */
|
|
51
|
+
runId: string;
|
|
52
|
+
/** The timestamp the run's release records carry. */
|
|
53
|
+
timestamp: string;
|
|
54
|
+
actor: string;
|
|
55
|
+
/** The release the run deployed under (`--release`), or null. */
|
|
56
|
+
releaseDigest: string | null;
|
|
57
|
+
/** How the run ended. */
|
|
58
|
+
status: "fail" | "gated";
|
|
59
|
+
components: DeployOutcomeComponent[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The outcome to record for a run, or null when there is nothing to record:
|
|
64
|
+
* every planned component deployed, or the run stopped at a gate with
|
|
65
|
+
* nothing deployed and nothing failed.
|
|
66
|
+
*/
|
|
67
|
+
export function deployOutcomeOf(input: {
|
|
68
|
+
verb: DeployOutcome["verb"];
|
|
69
|
+
plan: PromotionPlan;
|
|
70
|
+
run: Pick<DriverRunResult, "status" | "results">;
|
|
71
|
+
/** The release the run deployed under, if any. */
|
|
72
|
+
releaseDigest: string | null;
|
|
73
|
+
runId: string;
|
|
74
|
+
timestamp: string;
|
|
75
|
+
actor: string;
|
|
76
|
+
}): DeployOutcome | null {
|
|
77
|
+
const { plan, run } = input;
|
|
78
|
+
if (run.status === "ok") return null;
|
|
79
|
+
const components: DeployOutcomeComponent[] = plan.items.map((item) => {
|
|
80
|
+
const result = run.results.find((r) => r.component === item.component);
|
|
81
|
+
const outcome: DeployOutcomeState = !result ? "not-run" : result.status === "ok" ? "deployed" : result.status === "gated" ? "gated" : "failed";
|
|
82
|
+
return {
|
|
83
|
+
component: item.component,
|
|
84
|
+
outcome,
|
|
85
|
+
digest: item.digest,
|
|
86
|
+
releaseDigest: input.releaseDigest ?? item.source.releaseDigest ?? null,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
const deployed = components.some((c) => c.outcome === "deployed");
|
|
90
|
+
const failed = components.some((c) => c.outcome === "failed");
|
|
91
|
+
if (!deployed && !failed) return null;
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
env: plan.to,
|
|
95
|
+
verb: input.verb,
|
|
96
|
+
runId: input.runId,
|
|
97
|
+
timestamp: input.timestamp,
|
|
98
|
+
actor: input.actor,
|
|
99
|
+
releaseDigest: input.releaseDigest,
|
|
100
|
+
status: run.status,
|
|
101
|
+
components,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const STATES: readonly string[] = ["deployed", "failed", "gated", "not-run"];
|
|
106
|
+
|
|
107
|
+
function isOutcome(v: unknown): v is DeployOutcome {
|
|
108
|
+
const o = v as Partial<DeployOutcome> | null;
|
|
109
|
+
return (
|
|
110
|
+
o !== null &&
|
|
111
|
+
typeof o === "object" &&
|
|
112
|
+
o.version === 1 &&
|
|
113
|
+
typeof o.env === "string" &&
|
|
114
|
+
typeof o.runId === "string" &&
|
|
115
|
+
typeof o.timestamp === "string" &&
|
|
116
|
+
Array.isArray(o.components) &&
|
|
117
|
+
o.components.every((c) => c && typeof c.component === "string" && STATES.includes(c.outcome) && typeof c.digest === "string")
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Parse the lines of an `outcomes.jsonl`, oldest first. A line that is not an outcome is counted and skipped. */
|
|
122
|
+
export function parseDeployOutcomes(text: string | null): { outcomes: DeployOutcome[]; malformed: number } {
|
|
123
|
+
const outcomes: DeployOutcome[] = [];
|
|
124
|
+
let malformed = 0;
|
|
125
|
+
for (const line of (text ?? "").split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(line) as unknown;
|
|
128
|
+
if (isOutcome(parsed)) outcomes.push(parsed);
|
|
129
|
+
else malformed++;
|
|
130
|
+
} catch {
|
|
131
|
+
malformed++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { outcomes, malformed };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Append one outcome to `<env>/outcomes.jsonl`. Does not push; the caller pushes with the release records. */
|
|
138
|
+
export async function appendDeployOutcome(outcome: DeployOutcome, opts?: { cwd?: string }): Promise<string> {
|
|
139
|
+
const { appendLedgerLine } = await import("./git");
|
|
140
|
+
return appendLedgerLine(outcome.env, OUTCOMES_FILE, JSON.stringify(outcome, sortedJsonReplacer), "Deploy outcome", opts);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every outcome recorded for `env`, oldest first, read where `readReleaseLedger` reads; `[]` when none is. */
|
|
144
|
+
export async function readDeployOutcomes(env: string, opts?: import("./git").LedgerLocation): Promise<DeployOutcome[]> {
|
|
145
|
+
const { readBlobFromPath } = await import("./git");
|
|
146
|
+
return parseDeployOutcomes(await readBlobFromPath(env, OUTCOMES_FILE, opts)).outcomes;
|
|
147
|
+
}
|
|
@@ -10,19 +10,27 @@
|
|
|
10
10
|
* (`../change-set-graph.ts`), and stores the document under its digest
|
|
11
11
|
* (`./change-set-store.ts`).
|
|
12
12
|
*
|
|
13
|
+
* Each entry mapped onto a node that has a source location also records that
|
|
14
|
+
* file from the repository root as `source` (#4025), which the overlap report
|
|
15
|
+
* in `workspace status` attributes the entry to work items by.
|
|
16
|
+
*
|
|
13
17
|
* The graph is read only when the digest is not stored yet. A graph that
|
|
14
18
|
* can't be read never stops the gate: the document is stored with `graph:
|
|
15
19
|
* null` and every `node` null, and the caller gets a warning to print.
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
22
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
23
|
+
import { posix } from "node:path";
|
|
19
24
|
import { mapChangeSetToGraph, type ChangeSetGraphNodes } from "../change-set-graph";
|
|
20
25
|
import type { ChangeSetDocument, ChangeSetGraph } from "../change-set";
|
|
21
26
|
import { persistChangeSet, readStoredChangeSet } from "./change-set-store";
|
|
22
27
|
import { resolveMemberLedger } from "./member-ledger";
|
|
23
28
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Reads the node ids of the graph of the project at `projectDir`, with each
|
|
31
|
+
* node's source file (relative to `projectDir`) when the graph gives one.
|
|
32
|
+
*/
|
|
33
|
+
export type GraphIdReader = (projectDir: string) => Promise<{ ids: string[]; sources?: Record<string, string> } | { error: string }>;
|
|
26
34
|
|
|
27
35
|
const GRAPH_TIMEOUT_MS = 300_000;
|
|
28
36
|
|
|
@@ -32,7 +40,7 @@ const GRAPH_TIMEOUT_MS = 300_000;
|
|
|
32
40
|
* one), as `chant workspace graph` runs each member. `args` are passed after
|
|
33
41
|
* `graph --format ir`, such as the run's `--param` flags.
|
|
34
42
|
*/
|
|
35
|
-
export async function readGraphIds(projectDir: string, args: readonly string[] = []): Promise<{ ids: string[] } | { error: string }> {
|
|
43
|
+
export async function readGraphIds(projectDir: string, args: readonly string[] = []): Promise<{ ids: string[]; sources: Record<string, string> } | { error: string }> {
|
|
36
44
|
const { readerToolchain, resolveToolchain } = await import("../workspace/member-commands");
|
|
37
45
|
const { readMemberIr } = await import("../workspace/compose-graph");
|
|
38
46
|
const { findWorkspaceRoot } = await import("../project-root");
|
|
@@ -61,7 +69,9 @@ export async function readGraphIds(projectDir: string, args: readonly string[] =
|
|
|
61
69
|
}
|
|
62
70
|
const read = readMemberIr(out.stdout);
|
|
63
71
|
if ("reason" in read) return { error: read.reason.message };
|
|
64
|
-
|
|
72
|
+
const sources: Record<string, string> = {};
|
|
73
|
+
for (const n of read.ir.nodes) if (n.sourceLoc?.file) sources[n.id] = n.sourceLoc.file;
|
|
74
|
+
return { ids: read.ir.nodes.map((n) => n.id), sources };
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
let graphReaderOverride: GraphIdReader | undefined;
|
|
@@ -102,6 +112,27 @@ export interface StoreGateChangeSetResult {
|
|
|
102
112
|
warning?: string;
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Each node's source file as a path from the repository root (#4025): the
|
|
117
|
+
* graph gives it relative to the member's directory at `cwd`. Undefined when
|
|
118
|
+
* the graph gives none or `cwd` is in no git repository.
|
|
119
|
+
*/
|
|
120
|
+
function repoSources(cwd: string, sources: Record<string, string> | undefined): Map<string, string> | undefined {
|
|
121
|
+
if (!sources || Object.keys(sources).length === 0) return undefined;
|
|
122
|
+
let prefix: string;
|
|
123
|
+
try {
|
|
124
|
+
prefix = execFileSync("git", ["rev-parse", "--show-prefix"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const out = new Map<string, string>();
|
|
129
|
+
for (const [id, file] of Object.entries(sources)) {
|
|
130
|
+
const path = posix.normalize(posix.join(prefix, file.split("\\").join("/")));
|
|
131
|
+
if (!path.startsWith("../") && path !== "..") out.set(id, path);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
/**
|
|
106
137
|
* Store `doc` under its digest in the member's ledger at `cwd`, mapped onto
|
|
107
138
|
* the member's graph, unless it is stored already. Does not push; the gate's
|
|
@@ -118,7 +149,7 @@ export async function storeGateChangeSet(input: StoreGateChangeSetInput): Promis
|
|
|
118
149
|
try {
|
|
119
150
|
const read = await (input.readGraph ?? gateGraphReader())(cwd);
|
|
120
151
|
if ("error" in read) warning = read.error;
|
|
121
|
-
else nodes = { member, at: input.at ?? null, ids: read.ids };
|
|
152
|
+
else nodes = { member, at: input.at ?? null, ids: read.ids, sources: repoSources(cwd, read.sources) };
|
|
122
153
|
} catch (err) {
|
|
123
154
|
warning = err instanceof Error ? err.message : String(err);
|
|
124
155
|
}
|
|
@@ -47,7 +47,7 @@ import { currentGateOrigin, type GateOrigin } from "./gate-origin";
|
|
|
47
47
|
import type { GateApprover, GatePolicyDecision, ResolvedGateApproval } from "../op/gate-approval";
|
|
48
48
|
import type { GateApprovalSource } from "../op/op-waves";
|
|
49
49
|
import type { GateRunLocator } from "../op/gate-run-locator";
|
|
50
|
-
import { readBlobFromPath, readPathSha, readBlobBySha, writeBlobToPath, RefCASConflictError } from "./git";
|
|
50
|
+
import { readBlobFromPath, readPathSha, readBlobBySha, writeBlobToPath, RefCASConflictError, type LedgerLocation } from "./git";
|
|
51
51
|
|
|
52
52
|
// The facts a pending record is written with live apart, so a bundle can import them without git (#3816).
|
|
53
53
|
export { DEFAULT_GATE_EXPIRY, isApprovalUrl, resolveApprovalUrl } from "./gate-pending";
|
|
@@ -519,7 +519,7 @@ async function appendGateLine(
|
|
|
519
519
|
*/
|
|
520
520
|
export async function readGateLedger(
|
|
521
521
|
op: string,
|
|
522
|
-
opts?:
|
|
522
|
+
opts?: LedgerLocation,
|
|
523
523
|
): Promise<{ resolutions: GateResolutionRecord[]; pending: PendingGateRecord[]; malformed: number }> {
|
|
524
524
|
const content = await readBlobFromPath(DIR, filename(op), opts);
|
|
525
525
|
if (!content) return { resolutions: [], pending: [], malformed: 0 };
|
package/src/lifecycle/git.ts
CHANGED
|
@@ -171,6 +171,21 @@ export interface LedgerFileWrite {
|
|
|
171
171
|
expectPriorSha?: string | null;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* One file for {@link writeLedgerFiles} to remove (#4036): retention's
|
|
176
|
+
* garbage collection takes a stored change set, release or template out of
|
|
177
|
+
* the tree in a new commit. The history keeps it.
|
|
178
|
+
*/
|
|
179
|
+
export interface LedgerFileRemoval {
|
|
180
|
+
/** The file's full path on the branch. No prefix is added. */
|
|
181
|
+
path: string;
|
|
182
|
+
remove: true;
|
|
183
|
+
/** The blob sha the caller saw at the path; a different sha, or no file, is a conflict. */
|
|
184
|
+
expectPriorSha?: string | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const isRemoval = (f: LedgerFileWrite | LedgerFileRemoval): f is LedgerFileRemoval => (f as LedgerFileRemoval).remove === true;
|
|
188
|
+
|
|
174
189
|
/**
|
|
175
190
|
* Write several files to the branch in one commit (#2538). Paths are full
|
|
176
191
|
* branch paths, so one commit can write the ledgers of several members, for
|
|
@@ -185,9 +200,18 @@ export interface LedgerFileWrite {
|
|
|
185
200
|
* of an unchanged directory keeps its sha.
|
|
186
201
|
*/
|
|
187
202
|
export async function writeLedgerFiles(
|
|
188
|
-
files: LedgerFileWrite
|
|
203
|
+
files: Array<LedgerFileWrite | LedgerFileRemoval>,
|
|
189
204
|
commitMessage: string,
|
|
190
|
-
opts?: {
|
|
205
|
+
opts?: {
|
|
206
|
+
cwd?: string;
|
|
207
|
+
/**
|
|
208
|
+
* The branch tip the caller read before deciding what to write (#4036).
|
|
209
|
+
* A different tip is a conflict, thrown at once and never retried here,
|
|
210
|
+
* so a caller whose choice depends on files it does not write (garbage
|
|
211
|
+
* collection reading the gate ledgers) reads again.
|
|
212
|
+
*/
|
|
213
|
+
expectTip?: string;
|
|
214
|
+
},
|
|
191
215
|
): Promise<string> {
|
|
192
216
|
const rt = getRuntime();
|
|
193
217
|
const cwd = opts?.cwd;
|
|
@@ -208,8 +232,13 @@ export async function writeLedgerFiles(
|
|
|
208
232
|
// kubectl.kubernetes.io/last-applied-configuration annotation) and no
|
|
209
233
|
// shell-quoting dance around embedded single quotes. Content-addressed
|
|
210
234
|
// and idempotent, so this stays outside the retry loop below.
|
|
211
|
-
|
|
235
|
+
// A removal has no blob: its sha is null, and step 3 deletes the entry.
|
|
236
|
+
const blobs: { path: string; sha: string | null }[] = [];
|
|
212
237
|
for (const f of files) {
|
|
238
|
+
if (isRemoval(f)) {
|
|
239
|
+
blobs.push({ path: f.path, sha: null });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
213
242
|
const blobResult = await rt.spawn(["git", "hash-object", "-w", "--stdin"], { cwd, stdin: f.content });
|
|
214
243
|
if (blobResult.exitCode !== 0) {
|
|
215
244
|
throw ledgerWriteError("git hash-object", f.path, blobResult.stderr);
|
|
@@ -283,6 +312,9 @@ export async function writeLedgerFiles(
|
|
|
283
312
|
// there instead can observe a newer tip than the listings reflect, which
|
|
284
313
|
// makes the CAS succeed against a tree built from a stale read.
|
|
285
314
|
const tip = await getStateBranchTip(cwd);
|
|
315
|
+
if (opts?.expectTip !== undefined && tip !== opts.expectTip) {
|
|
316
|
+
throw new RefCASConflictError(`refs/heads/${STATE_BRANCH}`, opts.expectTip, `the branch moved to ${tip ?? "(none)"} since it was read`);
|
|
317
|
+
}
|
|
286
318
|
const trees = new Map<string, Map<string, TreeLine>>();
|
|
287
319
|
const listing = async (dir: string): Promise<Map<string, TreeLine>> => {
|
|
288
320
|
let entries = trees.get(dir);
|
|
@@ -312,16 +344,22 @@ export async function writeLedgerFiles(
|
|
|
312
344
|
// rebuild below has each parent in hand.
|
|
313
345
|
for (const b of blobs) {
|
|
314
346
|
const { dir, name } = splitPath(b.path);
|
|
315
|
-
(await listing(dir)).
|
|
347
|
+
if (b.sha === null) (await listing(dir)).delete(name);
|
|
348
|
+
else (await listing(dir)).set(name, { mode: "100644", type: "blob", sha: b.sha });
|
|
316
349
|
for (let d = dir; d !== ""; d = parentDir(d)) await listing(parentDir(d));
|
|
317
350
|
}
|
|
318
351
|
|
|
319
|
-
// 4. Rebuild the touched trees, deepest first, then the root.
|
|
352
|
+
// 4. Rebuild the touched trees, deepest first, then the root. A directory
|
|
353
|
+
// a removal left empty is dropped from its parent, as git itself would.
|
|
320
354
|
const touched = [...trees.keys()].sort((a, b) => depth(b) - depth(a) || (a < b ? -1 : 1));
|
|
321
355
|
let rootTreeSha = "";
|
|
322
356
|
for (const dir of touched) {
|
|
357
|
+
if (dir !== "" && trees.get(dir)!.size === 0) {
|
|
358
|
+
trees.get(parentDir(dir))!.delete(baseName(dir));
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
323
361
|
const lines = [...trees.get(dir)!].map(([name, e]) => `${e.mode} ${e.type} ${e.sha}\t${name}`).join("\n");
|
|
324
|
-
const result = await rt.spawn(["git", "mktree"], { cwd, stdin: `${lines}\n` });
|
|
362
|
+
const result = await rt.spawn(["git", "mktree"], { cwd, stdin: lines === "" ? "" : `${lines}\n` });
|
|
325
363
|
if (result.exitCode !== 0) {
|
|
326
364
|
throw ledgerWriteError(`git mktree (${dir === "" ? "root" : dir})`, path, result.stderr);
|
|
327
365
|
}
|
|
@@ -517,15 +555,30 @@ export async function appendReleaseRecordLine(
|
|
|
517
555
|
recordJson: string,
|
|
518
556
|
opts?: { cwd?: string },
|
|
519
557
|
): Promise<string> {
|
|
520
|
-
|
|
558
|
+
return appendLedgerLine(environment, "releases.jsonl", recordJson, "Release record", opts);
|
|
559
|
+
}
|
|
521
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Append one line to `<environment>/<filename>` on the orphan branch, with
|
|
563
|
+
* the same read, append and compare-and-swap retry as
|
|
564
|
+
* {@link appendReleaseRecordLine}. The deploy outcomes of
|
|
565
|
+
* ../lifecycle/deploy-outcome.ts (#4035) are appended through here, beside
|
|
566
|
+
* the environment's `releases.jsonl`.
|
|
567
|
+
*/
|
|
568
|
+
export async function appendLedgerLine(
|
|
569
|
+
environment: string,
|
|
570
|
+
filename: string,
|
|
571
|
+
recordJson: string,
|
|
572
|
+
commitMessage: string,
|
|
573
|
+
opts?: { cwd?: string },
|
|
574
|
+
): Promise<string> {
|
|
522
575
|
let lastErr: unknown;
|
|
523
576
|
for (let attempt = 1; attempt <= WRITE_BLOB_RETRY_ATTEMPTS; attempt++) {
|
|
524
577
|
try {
|
|
525
578
|
const priorSha = await readPathSha(environment, filename, opts);
|
|
526
579
|
const existing = priorSha ? await readBlobBySha(priorSha, opts) : null;
|
|
527
580
|
const content = `${existing ? `${existing.replace(/\n$/, "")}\n` : ""}${recordJson}\n`;
|
|
528
|
-
return await writeBlobToPath(environment, filename, content,
|
|
581
|
+
return await writeBlobToPath(environment, filename, content, commitMessage, {
|
|
529
582
|
...opts,
|
|
530
583
|
expectPriorPathSha: priorSha,
|
|
531
584
|
});
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ export * from "./unobserved-gate";
|
|
|
12
12
|
export * from "./receipt-plan";
|
|
13
13
|
export * from "./affected";
|
|
14
14
|
export * from "./release-ledger";
|
|
15
|
+
export * from "./release-join";
|
|
16
|
+
export * from "./retention-policy";
|
|
17
|
+
export * from "./delivery-metrics";
|
|
18
|
+
export { planLifecycleGc, runLifecycleGc, type GcFile, type GcKept, type GcKeepCode, type GcKeepReason, type GcKind, type GcReport, type LifecycleGcPlan } from "./retention";
|
|
15
19
|
export * from "./build-ledger";
|
|
16
20
|
export * from "./build-ledger-store";
|
|
17
21
|
export * from "./oras-referrer-lookup";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4035: what a partly applied run records, and how a mixed environment is
|
|
3
|
+
* derived from it and the release ledger. Pure; nothing is read or written.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { deployOutcomeOf, parseDeployOutcomes, type DeployOutcome } from "./deploy-outcome";
|
|
8
|
+
import { mixedEnvironment, rollbackTarget, unsettledComponents, type ReleaseCheck } from "./mixed-environment";
|
|
9
|
+
import type { ReleaseRecord } from "./release-ledger";
|
|
10
|
+
import type { PromotionPlan } from "../components/promote";
|
|
11
|
+
|
|
12
|
+
const R1 = `jcs1-sha256:${"1".repeat(64)}`;
|
|
13
|
+
const R2 = `jcs1-sha256:${"2".repeat(64)}`;
|
|
14
|
+
const R3 = `jcs1-sha256:${"3".repeat(64)}`;
|
|
15
|
+
|
|
16
|
+
const rec = (component: string, timestamp: string, releaseDigest?: string): ReleaseRecord => ({
|
|
17
|
+
version: 1, component, env: "prod", digest: `sha256:${component}-${releaseDigest?.slice(12, 13) ?? "x"}`, gitSha: "abc", runId: "r", timestamp, actor: "ci",
|
|
18
|
+
...(releaseDigest ? { releaseDigest } : {}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const plan: PromotionPlan = {
|
|
22
|
+
from: "staging",
|
|
23
|
+
to: "prod",
|
|
24
|
+
items: ["api", "web"].map((component) => ({ component, digest: `sha256:${component}`, source: rec(component, "2026-01-01T00:00:00.000Z") })),
|
|
25
|
+
notPromoted: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const outcome = (timestamp: string, components: DeployOutcome["components"], releaseDigest: string | null = R2): DeployOutcome => ({
|
|
29
|
+
version: 1, env: "prod", verb: "promote", runId: "run-9", timestamp, actor: "bob", releaseDigest, status: "fail", components,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const all = async (): Promise<ReleaseCheck> => ({ kept: true, revoked: false });
|
|
33
|
+
const labels: Record<string, string> = { [R1]: "v1", [R2]: "v2", [R3]: "v3" };
|
|
34
|
+
const label = async (d: string) => labels[d];
|
|
35
|
+
|
|
36
|
+
describe("deployOutcomeOf", () => {
|
|
37
|
+
const run = (statuses: Array<"ok" | "fail" | "gated" | undefined>, status: "ok" | "fail" | "gated") => ({
|
|
38
|
+
status,
|
|
39
|
+
results: statuses.flatMap((s, i) => (s ? [{ component: plan.items[i].component, ok: s === "ok", status: s, records: [] }] : [])),
|
|
40
|
+
});
|
|
41
|
+
const of = (r: ReturnType<typeof run>) => deployOutcomeOf({ verb: "promote", plan, run: r, releaseDigest: R2, runId: "run-9", timestamp: "t", actor: "bob" });
|
|
42
|
+
|
|
43
|
+
test("records a run that deployed some components and not others, or failed", () => {
|
|
44
|
+
expect(of(run(["ok", "fail"], "fail"))?.components.map((c) => [c.component, c.outcome, c.releaseDigest])).toEqual([["api", "deployed", R2], ["web", "failed", R2]]);
|
|
45
|
+
expect(of(run(["fail", undefined], "fail"))?.components.map((c) => c.outcome)).toEqual(["failed", "not-run"]);
|
|
46
|
+
expect(of(run(["ok", "gated"], "gated"))?.status).toBe("gated");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("records nothing for a run that deployed everything, or stopped at a gate before deploying anything", () => {
|
|
50
|
+
expect(of(run(["ok", "ok"], "ok"))).toBeNull();
|
|
51
|
+
expect(of(run(["gated", undefined], "gated"))).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("parseDeployOutcomes", () => {
|
|
56
|
+
test("skips a line that is not an outcome", () => {
|
|
57
|
+
const good = JSON.stringify(outcome("t", [{ component: "api", outcome: "failed", digest: "sha256:a", releaseDigest: null }]));
|
|
58
|
+
expect(parseDeployOutcomes(`${good}\nnot json\n{"version":1}\n`)).toMatchObject({ outcomes: [{ runId: "run-9" }], malformed: 2 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("mixed environments", () => {
|
|
63
|
+
test("a failed component stays unsettled until the ledger records it after the run", () => {
|
|
64
|
+
const o = outcome("2026-01-02T00:00:00.000Z", [{ component: "api", outcome: "deployed", digest: "sha256:a", releaseDigest: R2 }, { component: "web", outcome: "failed", digest: "sha256:w", releaseDigest: R2 }]);
|
|
65
|
+
const records = [rec("api", "2026-01-01T00:00:00.000Z", R1), rec("web", "2026-01-01T00:00:00.000Z", R1), rec("api", "2026-01-02T00:00:00.000Z", R2)];
|
|
66
|
+
expect([...unsettledComponents(records, [o]).keys()]).toEqual(["web"]);
|
|
67
|
+
expect(unsettledComponents([...records, rec("web", "2026-01-03T00:00:00.000Z", R1)], [o]).size).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("components on different releases are mixed, a record of no release counting as one; no release at all is not", async () => {
|
|
71
|
+
const mixed = await mixedEnvironment({ env: "prod", records: [rec("api", "2026-01-02T00:00:00.000Z", R2), rec("web", "2026-01-01T00:00:00.000Z")], outcomes: [], label, check: all });
|
|
72
|
+
expect(mixed?.components.map((c) => [c.component, c.releaseLabel, c.state])).toEqual([["api", "v2", "settled"], ["web", null, "settled"]]);
|
|
73
|
+
expect(mixed?.rollback).toBeNull();
|
|
74
|
+
expect(await mixedEnvironment({ env: "prod", records: [rec("api", "t1"), rec("web", "t1")], outcomes: [], label, check: all })).toBeNull();
|
|
75
|
+
expect(await mixedEnvironment({ env: "prod", records: [rec("api", "t1", R1), rec("web", "t1", R1)], outcomes: [], label, check: all })).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a run that failed before deploying anything leaves the environment mixed, and the rollback goes back past the release it tried", async () => {
|
|
79
|
+
const records = [rec("api", "2026-01-01T00:00:00.000Z", R1), rec("api", "2026-01-02T00:00:00.000Z", R2)];
|
|
80
|
+
const o = outcome("2026-01-03T00:00:00.000Z", [{ component: "api", outcome: "failed", digest: "sha256:a3", releaseDigest: R3 }], R3);
|
|
81
|
+
const mixed = await mixedEnvironment({ env: "prod", records, outcomes: [o], label, check: all });
|
|
82
|
+
expect(mixed).toMatchObject({
|
|
83
|
+
components: [{ component: "api", releaseLabel: "v2", state: "failed", attempt: { releaseLabel: "v3" } }],
|
|
84
|
+
redeploy: ["chant components redeploy prod --component api"],
|
|
85
|
+
rollback: "chant components rollback prod --release v2",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("the rollback target skips a revoked release and one that is no longer kept, and honours before", async () => {
|
|
90
|
+
const records = [rec("api", "2026-01-01T00:00:00.000Z", R1), rec("api", "2026-01-02T00:00:00.000Z", R2), rec("api", "2026-01-03T00:00:00.000Z", R3)];
|
|
91
|
+
const check = async (d: string): Promise<ReleaseCheck> => ({ kept: d !== R2, revoked: false });
|
|
92
|
+
expect(await rollbackTarget({ records, exclude: new Set([R3]), check })).toBe(R1);
|
|
93
|
+
expect(await rollbackTarget({ records, exclude: new Set(), check: async (d) => ({ kept: true, revoked: d === R3 }) })).toBe(R2);
|
|
94
|
+
expect(await rollbackTarget({ records, exclude: new Set(), before: "2026-01-02T00:00:00.000Z", check: all })).toBe(R1);
|
|
95
|
+
expect(await rollbackTarget({ records, exclude: new Set([R1, R2, R3]), check: all })).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|