@intentius/chant 0.124.28 → 0.124.30
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 +18 -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 +67 -8
- 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 +91 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +73 -2
- package/dist/cli/registry.d.ts +32 -4
- 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 +49 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +38 -4
- package/dist/components/pr-run.d.ts +20 -2
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +26 -13
- 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 +16 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +36 -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-ledger.d.ts +8 -3
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +2 -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/lifecycle/run-ledger.d.ts +1 -1
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +9 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +1 -0
- package/dist/op/activities/shell.d.ts +6 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +1 -1
- package/dist/op/local-executor.d.ts +23 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/runtime.d.ts +27 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtime.js +20 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-forge.d.ts +23 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +44 -0
- package/dist/pr-loop.d.ts +17 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +30 -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 +4 -3
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +7 -3
- package/dist/run-event.d.ts +34 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +4 -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-forge.d.ts +2 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +10 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +14 -5
- package/dist/workspace/ci-run.d.ts +222 -0
- package/dist/workspace/ci-run.d.ts.map +1 -0
- package/dist/workspace/ci-run.js +464 -0
- package/dist/workspace/ci-serve.d.ts +214 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -0
- package/dist/workspace/ci-serve.js +412 -0
- 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/pr-report.schema.json +5 -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 +24 -6
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +2 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +1 -0
- 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 +210 -2
- 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 +30 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +262 -1
- package/src/cli/handlers/fan-out.ts +72 -8
- 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 +144 -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 +91 -10
- package/src/cli/parse-args.ts +54 -2
- package/src/cli/registry.ts +32 -4
- 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 +67 -4
- package/src/components/pr-run.test.ts +127 -0
- package/src/components/pr-run.ts +32 -12
- 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 +45 -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-ledger.ts +9 -3
- 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/lifecycle/run-ledger.ts +7 -1
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/shell.ts +7 -1
- package/src/op/local-executor.ts +27 -4
- package/src/op/runtime.ts +42 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-forge.test.ts +25 -0
- package/src/pr-forge.ts +62 -0
- package/src/pr-loop.test.ts +13 -1
- package/src/pr-loop.ts +41 -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 +8 -4
- package/src/run-event.ts +38 -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-forge.ts +2 -0
- package/src/workspace/ci-green.ts +20 -5
- package/src/workspace/ci-run.test.ts +416 -0
- package/src/workspace/ci-run.ts +571 -0
- package/src/workspace/ci-serve.test.ts +268 -0
- package/src/workspace/ci-serve.ts +521 -0
- 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/pr-report.schema.json +5 -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 +24 -6
- 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-stewards.ts +3 -0
- package/src/workspace/status.schema.json +210 -2
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* failed or stopped at a gate, so the ledger never misses a deploy that
|
|
13
13
|
* happened. Unlike an auto-recorded release, the record is what these
|
|
14
14
|
* commands are for, so a failed write is an error rather than a warning.
|
|
15
|
+
* A run that applied partly, or failed, also appends a deploy outcome
|
|
16
|
+
* (#4035, ../../lifecycle/deploy-outcome.ts) naming what it did to each
|
|
17
|
+
* component, so `workspace status` reads the environment as mixed.
|
|
18
|
+
*
|
|
19
|
+
* `rollback --release` (#4035) is a promote of an earlier release into the
|
|
20
|
+
* environment: every component of the release at the digest the environment
|
|
21
|
+
* ran from it, through the release gate `promote --release` binds (#4045).
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
24
|
import { resolve } from "node:path";
|
|
@@ -38,6 +45,7 @@ import {
|
|
|
38
45
|
parseDigestPins,
|
|
39
46
|
planPromotion,
|
|
40
47
|
planRollback,
|
|
48
|
+
planReleaseRollback,
|
|
41
49
|
planRedeploy,
|
|
42
50
|
withoutBuildSteps,
|
|
43
51
|
runPromotion,
|
|
@@ -59,11 +67,12 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
|
59
67
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
60
68
|
import { GATED_EXIT_CODE } from "./run";
|
|
61
69
|
import type { CommandContext } from "../registry";
|
|
70
|
+
import { redeployLine, releaseRollbackLine, rollbackTarget, unsettledComponents } from "../../lifecycle/mixed-environment";
|
|
62
71
|
|
|
63
72
|
const PROMOTE_USAGE =
|
|
64
73
|
"chant components promote --to <env> [--from <env>] [--allow-skip] [--release <label|digest>] [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
|
|
65
74
|
const ROLLBACK_USAGE =
|
|
66
|
-
"chant components rollback <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
|
|
75
|
+
"chant components rollback <env> (--component <name> [--digest <sha256:...>] | --release <label|digest>) [--dry-run] [--json]";
|
|
67
76
|
const REDEPLOY_USAGE =
|
|
68
77
|
"chant components redeploy <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
|
|
69
78
|
|
|
@@ -171,7 +180,8 @@ async function bindReleaseGate(input: {
|
|
|
171
180
|
|
|
172
181
|
// What the environment runs now, to compare each component with.
|
|
173
182
|
const { readRelease } = await import("../../lifecycle/release-store");
|
|
174
|
-
const
|
|
183
|
+
const envRecords = (await readLedger(env)).filter((r) => r.env === env);
|
|
184
|
+
const latest = latestPerComponent(envRecords);
|
|
175
185
|
const releases = new Map<string, import("../../lifecycle/release-store").Release | null>();
|
|
176
186
|
const current = new Map<string, import("../../components/release-gate").CurrentDeploy>();
|
|
177
187
|
for (const item of plan.items) {
|
|
@@ -182,6 +192,13 @@ async function bindReleaseGate(input: {
|
|
|
182
192
|
current.set(item.component, { record, release: named !== undefined ? (releases.get(named) ?? null) : null });
|
|
183
193
|
}
|
|
184
194
|
|
|
195
|
+
// #4035: a component a run left failed or gated runs something the ledger doesn't say.
|
|
196
|
+
const { readDeployOutcomes } = await import("../../lifecycle/deploy-outcome");
|
|
197
|
+
const unsettled = new Map<string, string>();
|
|
198
|
+
for (const [component, u] of unsettledComponents(envRecords, await readDeployOutcomes(env).catch(() => []))) {
|
|
199
|
+
unsettled.set(component, `the ${u.outcome.verb} run ${u.outcome.runId} left ${component} ${u.state} in "${env}" and nothing has been recorded for it since, so what it runs is not known`);
|
|
200
|
+
}
|
|
201
|
+
|
|
185
202
|
const doc = await gate.planReleaseChangeSet({
|
|
186
203
|
release: under.release,
|
|
187
204
|
env,
|
|
@@ -189,6 +206,7 @@ async function bindReleaseGate(input: {
|
|
|
189
206
|
digests: new Map(plan.items.map((i) => [i.component, i.digest])),
|
|
190
207
|
registry: input.registry,
|
|
191
208
|
current,
|
|
209
|
+
unsettled,
|
|
192
210
|
seededOutputs: input.seededOutputs,
|
|
193
211
|
});
|
|
194
212
|
const provisional = gate.provisionalMembers(doc);
|
|
@@ -423,7 +441,11 @@ async function deployPlan(
|
|
|
423
441
|
const timestamp = new Date().toISOString();
|
|
424
442
|
const recorded: ReleaseRecord[] = [];
|
|
425
443
|
let recordError: string | undefined;
|
|
426
|
-
|
|
444
|
+
// #4035: a run that applied partly, or failed, records what it did to each
|
|
445
|
+
// component, so the environment reads as mixed until it is settled.
|
|
446
|
+
const { deployOutcomeOf, appendDeployOutcome } = await import("../../lifecycle/deploy-outcome");
|
|
447
|
+
const outcome = deployOutcomeOf({ verb, plan, run, releaseDigest: under?.release.releaseDigest ?? null, runId, timestamp, actor: actor! });
|
|
448
|
+
if (deployed.length > 0 || outcome) {
|
|
427
449
|
try {
|
|
428
450
|
for (const item of deployed) {
|
|
429
451
|
const result = run.results.find((r) => r.component === item.component);
|
|
@@ -433,6 +455,7 @@ async function deployPlan(
|
|
|
433
455
|
const { record: written } = await appendReleaseRecord(under ? { ...input, releaseDigest: under.release.releaseDigest } : input);
|
|
434
456
|
recorded.push(written);
|
|
435
457
|
}
|
|
458
|
+
if (outcome) await appendDeployOutcome(outcome);
|
|
436
459
|
await pushLifecycle();
|
|
437
460
|
} catch (err) {
|
|
438
461
|
recordError = err instanceof Error ? err.message : String(err);
|
|
@@ -441,7 +464,7 @@ async function deployPlan(
|
|
|
441
464
|
|
|
442
465
|
if (args.json) {
|
|
443
466
|
const withChangeSet = under && changeSetJson ? { release: { ...releaseJson.release!, ...changeSetJson } } : releaseJson;
|
|
444
|
-
console.log(JSON.stringify({ ...planJson(plan, removed), ...withChangeSet, run, recorded }, null, 2));
|
|
467
|
+
console.log(JSON.stringify({ ...planJson(plan, removed), ...withChangeSet, run, recorded, ...(outcome ? { outcome } : {}) }, null, 2));
|
|
445
468
|
} else {
|
|
446
469
|
for (const r of recorded) {
|
|
447
470
|
console.error(formatSuccess(
|
|
@@ -454,9 +477,20 @@ async function deployPlan(
|
|
|
454
477
|
}
|
|
455
478
|
}
|
|
456
479
|
|
|
480
|
+
if (outcome && recordError === undefined) {
|
|
481
|
+
const unsettled = outcome.components.filter((c) => c.outcome === "failed" || c.outcome === "gated");
|
|
482
|
+
console.error(formatWarning({
|
|
483
|
+
message: `"${plan.to}" now runs a mix: ${outcome.components.map((c) => `${c.component} ${c.outcome}`).join(", ")}. ` +
|
|
484
|
+
`chant workspace status ${plan.to} --json lists what each component runs and the rollback line`,
|
|
485
|
+
}));
|
|
486
|
+
for (const c of unsettled) console.error(formatInfo(`settle ${c.component}: ${redeployLine(plan.to, c.component)}, or run the same ${verb} again`));
|
|
487
|
+
}
|
|
488
|
+
|
|
457
489
|
if (recordError !== undefined) {
|
|
458
490
|
console.error(formatError({
|
|
459
|
-
message:
|
|
491
|
+
message: deployed.length > 0
|
|
492
|
+
? `deployed to "${plan.to}", but the release record was not written: ${recordError}`
|
|
493
|
+
: `the outcome of the ${verb} in "${plan.to}" was not written: ${recordError}`,
|
|
460
494
|
hint: `Pull the chant/lifecycle branch and check \`chant components status ${plan.to}\` before running this again.`,
|
|
461
495
|
}));
|
|
462
496
|
return 1;
|
|
@@ -506,6 +540,115 @@ function actorOrRefuse(ctx: CommandContext): { actor: string | undefined } | num
|
|
|
506
540
|
return { actor };
|
|
507
541
|
}
|
|
508
542
|
|
|
543
|
+
/** The revoked commits, or the exit code of a refusal already printed when the ci tags can't be judged (#4033). */
|
|
544
|
+
async function revokedOrRefuse(what: string): Promise<Map<string, import("../../lifecycle/release-revocation").ReleaseRevocation> | number> {
|
|
545
|
+
const { readRevokedCommits } = await import("../../lifecycle/release-revocation");
|
|
546
|
+
try {
|
|
547
|
+
return await readRevokedCommits(process.cwd());
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error(formatError({
|
|
550
|
+
message: `the ci tags can't be judged, so whether ${what} is revoked is unknown: ${err instanceof Error ? err.message : String(err)} [declaration-invalid]`,
|
|
551
|
+
hint: "Fix ci.green in chant.workspace.json at base; chant workspace check names the problem.",
|
|
552
|
+
}));
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Resolve `--release` for a promote or a rollback to `env` (#4026, #4035):
|
|
559
|
+
* the release, its label and its target for `env`. Refused, with the exit
|
|
560
|
+
* code returned, when the release can't be found, has no target for `env`,
|
|
561
|
+
* is of a project with no components (#4046), or was cut from a revoked
|
|
562
|
+
* commit (#4033), so a gate pending for it can't pass.
|
|
563
|
+
*/
|
|
564
|
+
async function resolveUnderRelease(ref: string, env: string, verb: "promote" | "rollback", fix: (release: { releaseDigest: string }) => Promise<string> | string): Promise<UnderRelease | number> {
|
|
565
|
+
const { resolveRelease } = await import("../../lifecycle/release-store");
|
|
566
|
+
const { targetFor, isImplicitRelease } = await import("../../components/release-promote");
|
|
567
|
+
const found = await resolveRelease(ref);
|
|
568
|
+
if (!found) {
|
|
569
|
+
console.error(formatError({ message: `no release is labelled or digested "${ref}" in this member`, hint: "List them with: chant release list" }));
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
const target = targetFor(found.release, env);
|
|
573
|
+
if ("error" in target) {
|
|
574
|
+
console.error(formatError({ message: target.error }));
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
// #4046: a release of a project with no components records build output
|
|
578
|
+
// and no component, so there is no deploy step here to point at it.
|
|
579
|
+
if (isImplicitRelease(found.release)) {
|
|
580
|
+
const name = found.label ?? found.release.releaseDigest;
|
|
581
|
+
console.error(formatError({
|
|
582
|
+
message: `release ${name} is of a project with no components, so ${verb} has no component to deploy`,
|
|
583
|
+
hint: `Write its "${env}" build output with \`chant release export ${name} --env ${env}\`, then run the project's own deploy against those files.`,
|
|
584
|
+
}));
|
|
585
|
+
return 1;
|
|
586
|
+
}
|
|
587
|
+
// #4033 (ws-105): a release cut from a revoked commit is never deployed,
|
|
588
|
+
// so a gate pending for it can't pass. Read from local tags, as the cut reads them.
|
|
589
|
+
const revoked = await revokedOrRefuse(`release ${found.label ?? found.release.releaseDigest}`);
|
|
590
|
+
if (typeof revoked === "number") return revoked;
|
|
591
|
+
const { revocationOf, revokedRefusal } = await import("../../lifecycle/release-revocation");
|
|
592
|
+
const revocation = revocationOf(found.release, revoked);
|
|
593
|
+
if (revocation) {
|
|
594
|
+
console.error(formatError(revokedRefusal({ releaseDigest: found.release.releaseDigest, label: found.label }, revocation, await fix(found.release))));
|
|
595
|
+
return 1;
|
|
596
|
+
}
|
|
597
|
+
return { release: found.release, label: found.label, target: target.target };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The line that rolls `env` back past `exclude` (#4035): to the newest
|
|
602
|
+
* release in its ledger that is still kept and not revoked, by label, or
|
|
603
|
+
* null when there is none.
|
|
604
|
+
*/
|
|
605
|
+
async function rollbackLineFor(env: string, records: readonly ReleaseRecord[], exclude: ReadonlySet<string>, revoked: ReadonlyMap<string, import("../../lifecycle/release-revocation").ReleaseRevocation>): Promise<string | null> {
|
|
606
|
+
const { readRelease, readCuts, labelsByDigest } = await import("../../lifecycle/release-store");
|
|
607
|
+
const { revocationOf } = await import("../../lifecycle/release-revocation");
|
|
608
|
+
const target = await rollbackTarget({
|
|
609
|
+
records,
|
|
610
|
+
exclude,
|
|
611
|
+
check: async (digest) => {
|
|
612
|
+
const release = await readRelease(digest).catch(() => null);
|
|
613
|
+
return { kept: release !== null, revoked: release !== null && revocationOf(release, revoked) !== null };
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
if (!target) return null;
|
|
617
|
+
const labels = await readCuts().then((c) => labelsByDigest(c.cuts), () => new Map<string, string>());
|
|
618
|
+
return releaseRollbackLine(env, target, labels.get(target));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Refuse a rollback or a redeploy that would put back a release cut from a
|
|
623
|
+
* revoked commit (#4033, #4035), naming the release to go back to instead.
|
|
624
|
+
* Returns the exit code of the refusal, or undefined when nothing restored
|
|
625
|
+
* names a revoked release.
|
|
626
|
+
*/
|
|
627
|
+
async function refuseRevokedRestore(verb: "rollback" | "redeploy", plan: PromotionPlan, records: readonly ReleaseRecord[]): Promise<number | undefined> {
|
|
628
|
+
const named = [...new Set(plan.items.map((i) => i.source.releaseDigest).filter((d): d is string => d !== undefined))];
|
|
629
|
+
if (named.length === 0) return undefined;
|
|
630
|
+
const revoked = await revokedOrRefuse(`the release this ${verb} restores`);
|
|
631
|
+
if (typeof revoked === "number") return revoked;
|
|
632
|
+
if (revoked.size === 0) return undefined;
|
|
633
|
+
const { readRelease, readCuts, labelsByDigest } = await import("../../lifecycle/release-store");
|
|
634
|
+
const { revocationOf, revokedRefusal } = await import("../../lifecycle/release-revocation");
|
|
635
|
+
for (const digest of named) {
|
|
636
|
+
const release = await readRelease(digest).catch(() => null);
|
|
637
|
+
const revocation = release ? revocationOf(release, revoked) : null;
|
|
638
|
+
if (!revocation) continue;
|
|
639
|
+
const labels = await readCuts().then((c) => labelsByDigest(c.cuts), () => new Map<string, string>());
|
|
640
|
+
const back = await rollbackLineFor(plan.to, records, new Set([digest]), revoked);
|
|
641
|
+
console.error(formatError(revokedRefusal(
|
|
642
|
+
{ releaseDigest: digest, label: labels.get(digest) ?? null },
|
|
643
|
+
revocation,
|
|
644
|
+
back ? `Restore a release cut from a green commit instead: ${back}` : `No other release "${plan.to}" ran is kept and green; promote a release cut from a green commit (\`chant ci last-green\` names the newest).`,
|
|
645
|
+
)));
|
|
646
|
+
console.error(formatError({ message: "nothing was deployed" }));
|
|
647
|
+
return 1;
|
|
648
|
+
}
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
|
|
509
652
|
export async function runComponentsPromote(ctx: CommandContext): Promise<number> {
|
|
510
653
|
const { args } = ctx;
|
|
511
654
|
const to = args.migrateTo;
|
|
@@ -560,29 +703,11 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
560
703
|
// #4026: --release names the release this promote deploys, by label or digest.
|
|
561
704
|
let under: UnderRelease | undefined;
|
|
562
705
|
if (args.release) {
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
return 1;
|
|
569
|
-
}
|
|
570
|
-
const target = targetFor(found.release, to);
|
|
571
|
-
if ("error" in target) {
|
|
572
|
-
console.error(formatError({ message: target.error }));
|
|
573
|
-
return 1;
|
|
574
|
-
}
|
|
575
|
-
// #4046: a release of a project with no components records build output
|
|
576
|
-
// and no component, so there is no deploy step here to point at it.
|
|
577
|
-
if (isImplicitRelease(found.release)) {
|
|
578
|
-
const name = found.label ?? found.release.releaseDigest;
|
|
579
|
-
console.error(formatError({
|
|
580
|
-
message: `release ${name} is of a project with no components, so promote has no component to deploy`,
|
|
581
|
-
hint: `Write its "${to}" build output with \`chant release export ${name} --env ${to}\`, then run the project's own deploy against those files.`,
|
|
582
|
-
}));
|
|
583
|
-
return 1;
|
|
584
|
-
}
|
|
585
|
-
under = { release: found.release, label: found.label, target: target.target };
|
|
706
|
+
const found = await resolveUnderRelease(args.release, to, "promote", () =>
|
|
707
|
+
"Promote a release cut from a green commit: `chant ci last-green` names the newest, and `chant release list` marks the revoked releases. Fetch the tags first: git fetch origin --tags",
|
|
708
|
+
);
|
|
709
|
+
if (typeof found === "number") return found;
|
|
710
|
+
under = found;
|
|
586
711
|
}
|
|
587
712
|
|
|
588
713
|
const plan = planPromotion({
|
|
@@ -604,31 +729,79 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
604
729
|
export async function runComponentsRollback(ctx: CommandContext): Promise<number> {
|
|
605
730
|
const { args } = ctx;
|
|
606
731
|
const env = args.extraPositional;
|
|
607
|
-
if (!env || !args.component) {
|
|
608
|
-
console.error(formatError({ message: "an environment and --component <name> are required", hint: ROLLBACK_USAGE }));
|
|
732
|
+
if (!env || (!args.component && !args.release)) {
|
|
733
|
+
console.error(formatError({ message: "an environment and --component <name> or --release <label|digest> are required", hint: ROLLBACK_USAGE }));
|
|
734
|
+
return 1;
|
|
735
|
+
}
|
|
736
|
+
if (args.component && args.release) {
|
|
737
|
+
console.error(formatError({ message: "--release rolls back every component of a release, so it does not take --component", hint: ROLLBACK_USAGE }));
|
|
738
|
+
return 1;
|
|
739
|
+
}
|
|
740
|
+
if (args.release && args.digest) {
|
|
741
|
+
console.error(formatError({ message: "--digest picks one release of one component, so it needs --component, not --release", hint: ROLLBACK_USAGE }));
|
|
609
742
|
return 1;
|
|
610
743
|
}
|
|
611
744
|
const who = actorOrRefuse(ctx);
|
|
612
745
|
if (typeof who === "number") return who;
|
|
746
|
+
if (args.release) return runReleaseRollback(ctx, env, args.release, who.actor);
|
|
613
747
|
|
|
614
|
-
const project = await loadProject(ctx, args.component);
|
|
748
|
+
const project = await loadProject(ctx, args.component!);
|
|
615
749
|
if (typeof project === "number") return project;
|
|
616
750
|
|
|
617
751
|
await fetchLifecycle().catch(() => false);
|
|
752
|
+
const records = await readLedger(env);
|
|
618
753
|
const plan = planRollback({
|
|
619
754
|
env,
|
|
620
|
-
records
|
|
755
|
+
records,
|
|
621
756
|
declared: project.targets.map((c) => c.name),
|
|
622
|
-
component: args.component
|
|
757
|
+
component: args.component!,
|
|
623
758
|
...(args.digest ? { digest: args.digest } : {}),
|
|
624
759
|
});
|
|
625
760
|
if ("error" in plan) {
|
|
626
761
|
console.error(formatError({ message: plan.error, hint: ROLLBACK_USAGE }));
|
|
627
762
|
return 1;
|
|
628
763
|
}
|
|
764
|
+
const refused = await refuseRevokedRestore("rollback", plan, records);
|
|
765
|
+
if (refused !== undefined) return refused;
|
|
629
766
|
return deployPlan(ctx, "rollback", plan, project, who.actor, rollbackRecord);
|
|
630
767
|
}
|
|
631
768
|
|
|
769
|
+
/**
|
|
770
|
+
* `chant components rollback <env> --release <label|digest>` (#4035): a
|
|
771
|
+
* gated promote of an earlier release into `env`. Every component of the
|
|
772
|
+
* release is restored at the digest `env` recorded running it from that
|
|
773
|
+
* release, its deploy reads the templates the release recorded for `env`,
|
|
774
|
+
* and its gates bind the release's change set planned against what `env`
|
|
775
|
+
* runs now, as `promote --release` binds them (#4045).
|
|
776
|
+
*/
|
|
777
|
+
async function runReleaseRollback(ctx: CommandContext, env: string, ref: string, actor: string | undefined): Promise<number> {
|
|
778
|
+
const project = await loadProject(ctx, "all");
|
|
779
|
+
if (typeof project === "number") return project;
|
|
780
|
+
await fetchLifecycle().catch(() => false);
|
|
781
|
+
const records = await readLedger(env);
|
|
782
|
+
const under = await resolveUnderRelease(ref, env, "rollback", async (release) => {
|
|
783
|
+
const revoked = await revokedOrRefuse("the release before it").then((r) => (typeof r === "number" ? new Map() : r));
|
|
784
|
+
const back = await rollbackLineFor(env, records, new Set([release.releaseDigest]), revoked);
|
|
785
|
+
return back ? `Roll back to a release cut from a green commit: ${back}` : "Roll back to a release cut from a green commit; `chant release list` marks the revoked releases.";
|
|
786
|
+
});
|
|
787
|
+
if (typeof under === "number") return under;
|
|
788
|
+
const { readDeployOutcomes } = await import("../../lifecycle/deploy-outcome");
|
|
789
|
+
const outcomes = await readDeployOutcomes(env).catch(() => []);
|
|
790
|
+
const plan = planReleaseRollback({
|
|
791
|
+
env,
|
|
792
|
+
records,
|
|
793
|
+
declared: project.targets.map((c) => c.name),
|
|
794
|
+
release: under.release,
|
|
795
|
+
label: under.label,
|
|
796
|
+
unsettled: new Set(unsettledComponents(records, outcomes).keys()),
|
|
797
|
+
});
|
|
798
|
+
if ("error" in plan) {
|
|
799
|
+
console.error(formatError({ message: plan.error, hint: ROLLBACK_USAGE }));
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
802
|
+
return deployPlan(ctx, "rollback", plan, project, actor, rollbackRecord, under);
|
|
803
|
+
}
|
|
804
|
+
|
|
632
805
|
export async function runComponentsRedeploy(ctx: CommandContext): Promise<number> {
|
|
633
806
|
const { args } = ctx;
|
|
634
807
|
const env = args.extraPositional;
|
|
@@ -643,9 +816,10 @@ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number
|
|
|
643
816
|
if (typeof project === "number") return project;
|
|
644
817
|
|
|
645
818
|
await fetchLifecycle().catch(() => false);
|
|
819
|
+
const records = await readLedger(env);
|
|
646
820
|
const plan = planRedeploy({
|
|
647
821
|
env,
|
|
648
|
-
records
|
|
822
|
+
records,
|
|
649
823
|
declared: project.targets.map((c) => c.name),
|
|
650
824
|
component: args.component,
|
|
651
825
|
...(args.digest ? { digest: args.digest } : {}),
|
|
@@ -654,5 +828,7 @@ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number
|
|
|
654
828
|
console.error(formatError({ message: plan.error, hint: REDEPLOY_USAGE }));
|
|
655
829
|
return 1;
|
|
656
830
|
}
|
|
831
|
+
const refused = await refuseRevokedRestore("redeploy", plan, records);
|
|
832
|
+
if (refused !== undefined) return refused;
|
|
657
833
|
return deployPlan(ctx, "redeploy", plan, project, who.actor, redeployRecord);
|
|
658
834
|
}
|
|
@@ -82,13 +82,21 @@ describe("an implicit release of a project with no components", () => {
|
|
|
82
82
|
git(a, "add", ".");
|
|
83
83
|
git(a, "commit", "-q", "-m", "init");
|
|
84
84
|
const sha = git(a, "rev-parse", "HEAD");
|
|
85
|
-
|
|
86
|
-
git(root, "
|
|
87
|
-
git(
|
|
85
|
+
// Each clone has a bare remote of its own holding main, so the commit is on trunk and pushed (#4027).
|
|
86
|
+
git(root, "init", "-q", "--bare", "-b", "main", "a.git");
|
|
87
|
+
git(a, "remote", "add", "origin", join(root, "a.git"));
|
|
88
|
+
git(a, "push", "-q", "origin", "main");
|
|
89
|
+
git(a, "fetch", "-q", "origin");
|
|
90
|
+
git(root, "clone", "-q", join(root, "a.git"), b);
|
|
91
|
+
git(root, "init", "-q", "--bare", "-b", "main", "b.git");
|
|
92
|
+
git(b, "remote", "set-url", "origin", join(root, "b.git"));
|
|
93
|
+
git(b, "push", "-q", "origin", "main");
|
|
94
|
+
git(b, "fetch", "-q", "origin");
|
|
95
|
+
for (const d of [a, b]) git(d, "tag", `ci/green/${sha}`);
|
|
88
96
|
for (const d of [a, b]) symlinkSync(join(REPO, "node_modules"), join(d, "node_modules"), "dir");
|
|
89
97
|
|
|
90
98
|
const cuts = [a, b].map((d, i) => {
|
|
91
|
-
const r = chant(d, "release", "cut", ...(i === 0 ? ["v1"] : []), "--params-file", "params.{env}.json", "--json");
|
|
99
|
+
const r = chant(d, "release", "cut", ...(i === 0 ? ["v1"] : []), "--params-file", "params.{env}.json", "--yes", "--json");
|
|
92
100
|
expect(r.code, r.stderr).toBe(0);
|
|
93
101
|
return JSON.parse(r.stdout);
|
|
94
102
|
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A commit, a release, a release gate and a deploy record join through the
|
|
3
|
+
* release digest (#4043). Everything runs in a temp repo with no remote, so
|
|
4
|
+
* nothing is pushed: a green tag, a real cut, a real `components promote
|
|
5
|
+
* --release` that stops at its gate, a real `chant approve`, and the promote
|
|
6
|
+
* that follows. Only the capability registry is replaced, so no deploy step
|
|
7
|
+
* reaches a cloud.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
11
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { CapabilityRegistry, type DeployContext } from "../../components/capability";
|
|
16
|
+
import type { DriverComponent } from "../../components/driver";
|
|
17
|
+
import type { ParsedArgs } from "../registry";
|
|
18
|
+
import { appendReleaseRecord, readReleaseLedger } from "../../lifecycle/release-ledger";
|
|
19
|
+
import { readGateLedger } from "../../lifecycle/gate-ledger";
|
|
20
|
+
import { cutRelease } from "../../lifecycle/release-cut";
|
|
21
|
+
import { joinRelease, type ReleaseJoinStart } from "../../lifecycle/release-join";
|
|
22
|
+
import { resetGateOrigin } from "../../lifecycle/gate-origin";
|
|
23
|
+
import type { ReleaseArtifact } from "../../lifecycle/release-store";
|
|
24
|
+
import type { CompiledTarget, TargetCompiler } from "../../components/release-compile";
|
|
25
|
+
|
|
26
|
+
const DIGEST = `sha256:${"a".repeat(64)}`;
|
|
27
|
+
|
|
28
|
+
const api: DriverComponent = {
|
|
29
|
+
name: "api",
|
|
30
|
+
deploy: [
|
|
31
|
+
{ phase: "Publish", steps: [{ kind: "publish-image", from: "archive:api.tar" }] },
|
|
32
|
+
{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
vi.mock("../../config", async () => {
|
|
37
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
38
|
+
return { ...actual, loadChantConfig: async () => ({ config: {} }) };
|
|
39
|
+
});
|
|
40
|
+
vi.mock("../../components/cli-support", () => ({
|
|
41
|
+
isComponentList: () => false,
|
|
42
|
+
resolveComponentTargets: async () => ({ success: true, targets: [api] }),
|
|
43
|
+
}));
|
|
44
|
+
vi.mock("../../components/fan-out-support", () => ({
|
|
45
|
+
fanOutRegistry: async () => {
|
|
46
|
+
const registry = new CapabilityRegistry();
|
|
47
|
+
registry.register({ kind: "publish-image", async run(_ctx: DeployContext) { return { digest: DIGEST, uri: `reg/api@${DIGEST}` }; } } as never);
|
|
48
|
+
registry.register({ kind: "apply", async run() { return {}; } } as never);
|
|
49
|
+
return registry;
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
|
|
53
|
+
vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
|
|
54
|
+
|
|
55
|
+
const { runComponentsPromote, runComponentsRedeploy } = await import("./promote");
|
|
56
|
+
const { recordGateApproval } = await import("./approve");
|
|
57
|
+
|
|
58
|
+
function git(dir: string, ...args: string[]): string {
|
|
59
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
60
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
61
|
+
return r.stdout.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A compiler that compiles each environment to its own template and builds one image. */
|
|
65
|
+
const compiler: TargetCompiler = {
|
|
66
|
+
async compile(env): Promise<CompiledTarget> {
|
|
67
|
+
return {
|
|
68
|
+
env,
|
|
69
|
+
params: [],
|
|
70
|
+
components: ["api"],
|
|
71
|
+
buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
|
|
72
|
+
buildInputs: { api: ["api"] },
|
|
73
|
+
componentFiles: { api: "api.component.ts" },
|
|
74
|
+
templates: [{ component: "api", path: "dist/api.template.json", content: `{"env":"${env}"}\n` }],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
|
|
78
|
+
return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: DIGEST }));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function args(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
83
|
+
return {
|
|
84
|
+
command: "components", path: "promote", format: "", fix: false, watch: false, verbose: false, help: false, live: false,
|
|
85
|
+
migrateFrom: "staging", migrateTo: "prod", actor: "bob", runId: "run-9", ...overrides,
|
|
86
|
+
} as ParsedArgs;
|
|
87
|
+
}
|
|
88
|
+
const ctx = (a: Partial<ParsedArgs>) => ({ args: args(a), plugins: [] }) as never;
|
|
89
|
+
|
|
90
|
+
describe("commit, release, gate and deploy record join through the release digest (#4043)", () => {
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
process.chdir(cwd);
|
|
94
|
+
resetGateOrigin();
|
|
95
|
+
vi.restoreAllMocks();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test(
|
|
99
|
+
"a cut, a gated promote, an approval and a redeploy join from the commit, the release, the gate and the deploy",
|
|
100
|
+
async () => {
|
|
101
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
102
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
103
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
104
|
+
await withTestDir(async (dir) => {
|
|
105
|
+
git(dir, "init", "-q", "-b", "main");
|
|
106
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
107
|
+
git(dir, "config", "user.name", "T");
|
|
108
|
+
writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev", "staging", "prod"] };\n`);
|
|
109
|
+
writeFileSync(join(dir, "api.component.ts"), "export const api = {};\n");
|
|
110
|
+
git(dir, "add", ".");
|
|
111
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
112
|
+
const sha = git(dir, "rev-parse", "HEAD");
|
|
113
|
+
git(dir, "tag", `ci/green/${sha}`, sha);
|
|
114
|
+
process.chdir(dir);
|
|
115
|
+
|
|
116
|
+
// The release is cut from the green commit.
|
|
117
|
+
const cut = await cutRelease({ cwd: dir, compiler, label: "v1.0.0", actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
|
|
118
|
+
const releaseDigest = cut.release.releaseDigest;
|
|
119
|
+
|
|
120
|
+
// Staging is what prod promotes from. It is a plain deploy of the same commit.
|
|
121
|
+
await appendReleaseRecord(
|
|
122
|
+
{ component: "api", env: "staging", digest: DIGEST, gitSha: sha, runId: "run-1", timestamp: "2026-10-16T10:30:00.000Z", actor: "ci" },
|
|
123
|
+
{ cwd: dir },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// The promote stops at the release gate and leaves a pending fact for the release.
|
|
127
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
128
|
+
const afterStop = await readGateLedger("release", { cwd: dir });
|
|
129
|
+
expect(afterStop.pending).toEqual([expect.objectContaining({ gate: "prod-release", environment: "prod", releaseDigest })]);
|
|
130
|
+
expect((await readReleaseLedger("prod", { cwd: dir })).records).toEqual([]);
|
|
131
|
+
|
|
132
|
+
// A person approves it, and the promote then deploys.
|
|
133
|
+
const approved = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
|
|
134
|
+
expect(approved.ok).toBe(true);
|
|
135
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
136
|
+
const [deploy] = (await readReleaseLedger("prod", { cwd: dir })).records;
|
|
137
|
+
expect(deploy).toMatchObject({ env: "prod", component: "api", releaseDigest, approver: "alice", admission: { op: "release", gate: "prod-release" } });
|
|
138
|
+
|
|
139
|
+
// A redeploy puts the same deploy back; it stops at the component's own gate first.
|
|
140
|
+
const redeployArgs = { path: "redeploy", component: "api", migrateTo: undefined, migrateFrom: undefined, extraPositional: "prod", runId: "run-10" };
|
|
141
|
+
const stopped = await runComponentsRedeploy(ctx(redeployArgs));
|
|
142
|
+
expect(stopped).toBe(3);
|
|
143
|
+
const gateOp = (await readGateLedger("api", { cwd: dir })).pending.at(-1)!;
|
|
144
|
+
expect((await recordGateApproval(gateOp.op, gateOp.gate, { origin: "cli", actor: "alice", cwd: dir, environment: "prod" })).ok).toBe(true);
|
|
145
|
+
expect(await runComponentsRedeploy(ctx(redeployArgs))).toBe(0);
|
|
146
|
+
const prodRecords = (await readReleaseLedger("prod", { cwd: dir })).records;
|
|
147
|
+
expect(prodRecords).toHaveLength(2);
|
|
148
|
+
expect(prodRecords[1]).toMatchObject({ releaseDigest, redeploys: { env: "prod", runId: deploy.runId } });
|
|
149
|
+
|
|
150
|
+
// From each starting point the same release, gate and deploys come back.
|
|
151
|
+
const resolution = (await readGateLedger("release", { cwd: dir })).resolutions[0];
|
|
152
|
+
const pending = (await readGateLedger("release", { cwd: dir })).pending[0];
|
|
153
|
+
const starts: Array<[string, ReleaseJoinStart]> = [
|
|
154
|
+
["commit", { commit: sha }],
|
|
155
|
+
["commit prefix", { commit: sha.slice(0, 10) }],
|
|
156
|
+
["release label", { release: "v1.0.0" }],
|
|
157
|
+
["release digest", { release: releaseDigest }],
|
|
158
|
+
["deploy record", { deploy: deploy }],
|
|
159
|
+
["gate resolution", { gate: resolution }],
|
|
160
|
+
["pending fact", { gate: pending }],
|
|
161
|
+
["deploy record without releaseDigest, by its admission", { deploy: { admission: deploy.admission } }],
|
|
162
|
+
];
|
|
163
|
+
for (const [name, from] of starts) {
|
|
164
|
+
const joined = await joinRelease(from, { cwd: dir });
|
|
165
|
+
expect(joined.releases, name).toHaveLength(1);
|
|
166
|
+
const [one] = joined.releases;
|
|
167
|
+
expect(one.releaseDigest, name).toBe(releaseDigest);
|
|
168
|
+
expect(one.commit, name).toBe(sha);
|
|
169
|
+
expect(one.label, name).toBe("v1.0.0");
|
|
170
|
+
expect(one.gates.resolutions.map((r) => [r.gate, r.resolvedBy, r.environment]), name).toEqual([["prod-release", "alice", "prod"]]);
|
|
171
|
+
expect(one.gates.pending.map((p) => p.planDigest), name).toEqual([resolution.planDigest]);
|
|
172
|
+
expect(one.deploys.map((d) => [d.env, d.runId]), name).toEqual([["prod", deploy.runId], ["prod", "run-10"]]);
|
|
173
|
+
// The deploy names the gate's resolution by address, and the join finds that very resolution.
|
|
174
|
+
expect(one.deploys[0].admission).toMatchObject({ op: "release", gate: one.gates.resolutions[0].gate, resolvedAt: one.gates.resolutions[0].timestamp });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// An unknown commit and a deploy that came from no release join to nothing, with a reason.
|
|
178
|
+
expect(await joinRelease({ commit: "f".repeat(40) }, { cwd: dir })).toMatchObject({ releases: [], reason: expect.stringMatching(/no release was cut/) });
|
|
179
|
+
expect(await joinRelease({ deploy: { releaseDigest: undefined } }, { cwd: dir })).toMatchObject({ releases: [], reason: expect.stringMatching(/not made from one/) });
|
|
180
|
+
expect(await joinRelease({ commit: "abc" }, { cwd: dir })).toMatchObject({ releases: [], reason: expect.stringMatching(/at least 7/) });
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
240_000,
|
|
184
|
+
);
|
|
185
|
+
});
|