@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
|
@@ -84,6 +84,13 @@ let inputs: Record<string, Record<string, unknown>> = {};
|
|
|
84
84
|
let stepOutputs: Record<string, Record<string, unknown>> = {};
|
|
85
85
|
/** Called as a component's step starts, before it applies. */
|
|
86
86
|
let onStart: (component: string) => void = () => {};
|
|
87
|
+
/**
|
|
88
|
+
* What each stack exposes in the deployed estate, read through the step's
|
|
89
|
+
* `outputs` as cfn-deploy's `describeStack` does (#4104). `undefined` leaves
|
|
90
|
+
* the capability with no `outputs`; an `Error` makes the read fail.
|
|
91
|
+
*/
|
|
92
|
+
let liveOutputs: Record<string, Record<string, unknown> | Error> | undefined;
|
|
93
|
+
let outputsRead: string[] = [];
|
|
87
94
|
|
|
88
95
|
function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
89
96
|
return {
|
|
@@ -114,6 +121,8 @@ beforeEach(() => {
|
|
|
114
121
|
inputs = {};
|
|
115
122
|
stepOutputs = {};
|
|
116
123
|
onStart = () => {};
|
|
124
|
+
liveOutputs = undefined;
|
|
125
|
+
outputsRead = [];
|
|
117
126
|
gateLedger = memoryGateLedgerPort();
|
|
118
127
|
stderrLines = [];
|
|
119
128
|
stdoutLines = [];
|
|
@@ -149,6 +158,16 @@ beforeEach(() => {
|
|
|
149
158
|
if (failing.includes(runCtx.component)) throw new Error(`${runCtx.component} failed`);
|
|
150
159
|
return { ok: true, ...(stepOutputs[runCtx.component] ? { outputs: stepOutputs[runCtx.component] } : {}) };
|
|
151
160
|
},
|
|
161
|
+
...(liveOutputs
|
|
162
|
+
? {
|
|
163
|
+
async outputs(readCtx: { component: string }) {
|
|
164
|
+
outputsRead.push(readCtx.component);
|
|
165
|
+
const live = liveOutputs![readCtx.component];
|
|
166
|
+
if (live instanceof Error) throw live;
|
|
167
|
+
return { ...(live ?? {}) };
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: {}),
|
|
152
171
|
} as never);
|
|
153
172
|
return Promise.resolve(registry);
|
|
154
173
|
});
|
|
@@ -233,7 +252,8 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
|
|
|
233
252
|
|
|
234
253
|
ran = [];
|
|
235
254
|
stderrLines = [];
|
|
236
|
-
|
|
255
|
+
// The same environment: progress is bound to it (#4081).
|
|
256
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, dryRun: true }))).toBe(0);
|
|
237
257
|
|
|
238
258
|
expect(ran).toEqual([]);
|
|
239
259
|
const out = stderr();
|
|
@@ -656,6 +676,46 @@ describe("--events-json (#4041)", () => {
|
|
|
656
676
|
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
|
|
657
677
|
});
|
|
658
678
|
|
|
679
|
+
test("a component behind a failure settles blocked, naming it, after the failing wave and before the next starts (#4007)", async () => {
|
|
680
|
+
failing = ["cluster-a"];
|
|
681
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", eventsJson: true }))).toBe(1);
|
|
682
|
+
const events = checkRunEventLines(lines());
|
|
683
|
+
const blocked = events.filter((e) => e.type === "component-settle" && e.status === "blocked");
|
|
684
|
+
expect(blocked.map((e) => ({ wave: (e as { wave: number }).wave, component: (e as { component: string }).component, blockedBy: (e as { blockedBy?: string }).blockedBy }))).toEqual([
|
|
685
|
+
{ wave: 3, component: "app-one", blockedBy: "cluster-a" },
|
|
686
|
+
{ wave: 3, component: "app-two", blockedBy: "cluster-a" },
|
|
687
|
+
]);
|
|
688
|
+
const wave2Settle = events.findIndex((e) => e.type === "wave-settle" && e.wave === 2);
|
|
689
|
+
const wave3Start = events.findIndex((e) => e.type === "wave-start" && e.wave === 3);
|
|
690
|
+
expect(events.indexOf(blocked[0]!)).toBeGreaterThan(wave2Settle);
|
|
691
|
+
expect(events.indexOf(blocked[1]!)).toBeLessThan(wave3Start);
|
|
692
|
+
// A blocked component never starts.
|
|
693
|
+
expect(events.some((e) => e.type === "component-start" && e.component === "app-one")).toBe(false);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test("a resumed fan-out settles what the earlier attempt applied as already-applied, in the derivation's waves (#4007)", async () => {
|
|
697
|
+
const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
|
|
698
|
+
try {
|
|
699
|
+
const resume = join(dir, "attempt.json");
|
|
700
|
+
failing = ["cluster-a"];
|
|
701
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
|
|
702
|
+
failing = [];
|
|
703
|
+
stdoutLines = [];
|
|
704
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, eventsJson: true }))).toBe(0);
|
|
705
|
+
const events = checkRunEventLines(lines());
|
|
706
|
+
const firstWave = events.findIndex((e) => e.type === "wave-start");
|
|
707
|
+
const applied = events.filter((e) => e.type === "component-settle" && e.status === "already-applied");
|
|
708
|
+
expect(applied.map((e) => [(e as { component: string }).component, (e as { wave: number }).wave])).toEqual([["app-three", 3], ["cluster-b", 2], ["net", 1]]);
|
|
709
|
+
for (const e of applied) expect(events.indexOf(e)).toBeLessThan(firstWave);
|
|
710
|
+
// --progress-json never printed them, and still does not.
|
|
711
|
+
stdoutLines = [];
|
|
712
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, progressJson: true }))).toBe(0);
|
|
713
|
+
expect(stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l).type)).toEqual(["run-start", "run-done"]);
|
|
714
|
+
} finally {
|
|
715
|
+
await rm(dir, { recursive: true, force: true });
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
659
719
|
test("--progress-json on a set the gate stops now prints run-start and run-done gated (a level-0 change)", async () => {
|
|
660
720
|
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", progressJson: true }))).toBe(3);
|
|
661
721
|
const progress = stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l));
|
|
@@ -670,3 +730,204 @@ describe("--events-json (#4041)", () => {
|
|
|
670
730
|
expect(stderr()).toContain("--events-json and --progress-json both stream to stdout");
|
|
671
731
|
});
|
|
672
732
|
});
|
|
733
|
+
|
|
734
|
+
describe("progress carries only for an unchanged input (#4081)", () => {
|
|
735
|
+
/** The estate with each component deploying a built template from `templates/`, read relative to the project. */
|
|
736
|
+
const templated = ESTATE.map((c) => ({
|
|
737
|
+
...c,
|
|
738
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: `${c.name}-stack`, template: `templates/${c.name}.json` }] }],
|
|
739
|
+
}));
|
|
740
|
+
let dir: string;
|
|
741
|
+
const writeTemplate = (name: string, body: unknown) =>
|
|
742
|
+
writeFile(join(dir, "templates", `${name}.json`), JSON.stringify(body));
|
|
743
|
+
|
|
744
|
+
beforeEach(async () => {
|
|
745
|
+
dir = await mkdtemp(join(tmpdir(), "fan-out-inputs-"));
|
|
746
|
+
const { mkdir } = await import("node:fs/promises");
|
|
747
|
+
await mkdir(join(dir, "templates"));
|
|
748
|
+
for (const c of templated) await writeTemplate(c.name, { Resources: { [c.name]: { Type: "AWS::SNS::Topic" } } });
|
|
749
|
+
// The project is the temp directory, so the templates are read from there and nothing touches the checkout.
|
|
750
|
+
vi.spyOn(process, "cwd").mockReturnValue(dir);
|
|
751
|
+
discoverComponentsMock.mockResolvedValue({
|
|
752
|
+
components: new Map(templated.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
|
|
753
|
+
sourceFiles: [],
|
|
754
|
+
errors: [],
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
afterEach(async () => {
|
|
758
|
+
await rm(dir, { recursive: true, force: true });
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("same shape, changed content: a stale record does not skip the components whose input moved", async () => {
|
|
762
|
+
const resume = join(dir, "attempt.json");
|
|
763
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
764
|
+
const first = JSON.parse(await readFile(resume, "utf8"));
|
|
765
|
+
expect(first.completed).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
|
|
766
|
+
expect(Object.keys(first.inputs).sort()).toEqual(first.completed);
|
|
767
|
+
const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
|
|
768
|
+
|
|
769
|
+
// The next change: net's topic gets a display name and cluster-a a queue.
|
|
770
|
+
// Same components, same edges, so the same plan digest.
|
|
771
|
+
await writeTemplate("net", { Resources: { net: { Type: "AWS::SNS::Topic", Properties: { DisplayName: "estate" } } } });
|
|
772
|
+
await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
|
|
773
|
+
ran = [];
|
|
774
|
+
stderrLines = [];
|
|
775
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
776
|
+
|
|
777
|
+
expect([...ran].sort()).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
|
|
778
|
+
const out = stderr();
|
|
779
|
+
expect(out).toContain(`plan: ${digest}`);
|
|
780
|
+
expect(out).toContain("net: changed since the earlier attempt, so it runs again");
|
|
781
|
+
expect(out).toContain("cluster-a: changed since the earlier attempt, so it runs again");
|
|
782
|
+
// Downstream of net, so its parameters read a changed upstream.
|
|
783
|
+
expect(out).toContain("app-three: changed since the earlier attempt, so it runs again");
|
|
784
|
+
expect(out).not.toMatch(/already-applied/);
|
|
785
|
+
expect(out).toContain("fan-out completed: 6 applied, 0 failed, 0 blocked");
|
|
786
|
+
const second = JSON.parse(await readFile(resume, "utf8"));
|
|
787
|
+
expect(second.inputs.net).not.toBe(first.inputs.net);
|
|
788
|
+
expect(second.inputs["cluster-b"]).not.toBe(first.inputs["cluster-b"]);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("a change below the root reruns only its own subtree", async () => {
|
|
792
|
+
const resume = join(dir, "attempt.json");
|
|
793
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
794
|
+
await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
|
|
795
|
+
ran = [];
|
|
796
|
+
stderrLines = [];
|
|
797
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
798
|
+
expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
|
|
799
|
+
expect(stderr()).toMatch(/cluster-b +already-applied/);
|
|
800
|
+
expect(stderr()).not.toContain("net: changed");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("the README's fail-then-fix: the fix reruns the failed subtree and the rest stay already-applied", async () => {
|
|
804
|
+
const resume = join(dir, "attempt.json");
|
|
805
|
+
failing = ["cluster-a"];
|
|
806
|
+
await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events" } } } });
|
|
807
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
|
|
808
|
+
const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
|
|
809
|
+
|
|
810
|
+
// The fix changes cluster-a's template, which never applied.
|
|
811
|
+
failing = [];
|
|
812
|
+
await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events.fifo" } } } });
|
|
813
|
+
ran = [];
|
|
814
|
+
stderrLines = [];
|
|
815
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
816
|
+
expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
|
|
817
|
+
const out = stderr();
|
|
818
|
+
expect(out).toContain(`plan: ${digest}`);
|
|
819
|
+
expect(out).not.toContain("changed since the earlier attempt");
|
|
820
|
+
for (const name of ["net", "cluster-b", "app-three"]) expect(out).toMatch(new RegExp(`${name} +already-applied`));
|
|
821
|
+
expect(out).toContain("fan-out completed: 3 applied, 0 failed, 0 blocked");
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("the fix keeps the approval: approval binds the derivation, progress binds the inputs", async () => {
|
|
825
|
+
const resume = join(dir, "attempt.json");
|
|
826
|
+
failing = ["cluster-a"];
|
|
827
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(3);
|
|
828
|
+
const pending = gateLedger.appended[0]!;
|
|
829
|
+
gateLedger = memoryGateLedgerPort({
|
|
830
|
+
pending: [pending],
|
|
831
|
+
resolutions: [
|
|
832
|
+
{
|
|
833
|
+
version: 1,
|
|
834
|
+
kind: "resolution",
|
|
835
|
+
op: "fan-out",
|
|
836
|
+
gate: "release",
|
|
837
|
+
resolvedBy: "operator",
|
|
838
|
+
timestamp: new Date(Date.parse(pending.timestamp) + 1000).toISOString(),
|
|
839
|
+
planDigest: pending.planDigest,
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
});
|
|
843
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(1);
|
|
844
|
+
|
|
845
|
+
failing = [];
|
|
846
|
+
await writeTemplate("cluster-a", { Resources: { fixed: { Type: "AWS::SQS::Queue" } } });
|
|
847
|
+
ran = [];
|
|
848
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(0);
|
|
849
|
+
expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("the input digest is per environment: progress against one does not carry into another", async () => {
|
|
853
|
+
const resume = join(dir, "attempt.json");
|
|
854
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
855
|
+
ran = [];
|
|
856
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "prod", resume }))).toBe(0);
|
|
857
|
+
expect(ran).toHaveLength(6);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test("a record without input digests carries nothing, and says so", async () => {
|
|
861
|
+
const resume = join(dir, "attempt.json");
|
|
862
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
|
|
863
|
+
const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())![1]!;
|
|
864
|
+
await writeFile(resume, JSON.stringify({ digest, completed: ["net", "cluster-b"], failed: [], outputs: {} }));
|
|
865
|
+
stderrLines = [];
|
|
866
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
|
|
867
|
+
expect(ran).toHaveLength(6);
|
|
868
|
+
expect(stderr()).toContain("does not record what each component applied, so none of its progress carries");
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
describe("a seed's outputs, read from the deployed estate (#4104)", () => {
|
|
873
|
+
/** cluster-a reads net's TopicArn, and the change touches cluster-a alone. */
|
|
874
|
+
function wireClusterA(): void {
|
|
875
|
+
const wired: Component = {
|
|
876
|
+
name: "cluster-a",
|
|
877
|
+
dependsOn: ["net"],
|
|
878
|
+
deploy: [
|
|
879
|
+
{
|
|
880
|
+
phase: "Apply",
|
|
881
|
+
steps: [{ kind: "cfn-deploy", stack: "cluster-a-stack", inputs: { topic: { stackOutput: { stack: "net", name: "TopicArn" } } } }],
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
};
|
|
885
|
+
const estate = ESTATE.map((c) => (c.name === "cluster-a" ? wired : c));
|
|
886
|
+
discoverComponentsMock.mockResolvedValue({
|
|
887
|
+
components: new Map(estate.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
|
|
888
|
+
sourceFiles: [],
|
|
889
|
+
errors: [],
|
|
890
|
+
});
|
|
891
|
+
affectedStacksMock.mockResolvedValue({ changed: ["cluster-a-stack"], dependents: [], indeterminate: [] });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
test("a cluster-a-only change runs cluster-a with net's deployed outputs, and says it read them", async () => {
|
|
895
|
+
wireClusterA();
|
|
896
|
+
liveOutputs = { net: { TopicArn: "arn:aws:sns:us-east-1:000000000000:net", QueueUrl: "http://q" } };
|
|
897
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(0);
|
|
898
|
+
expect(ran).not.toContain("net");
|
|
899
|
+
expect(outputsRead).toEqual(["net"]);
|
|
900
|
+
expect(inputs["cluster-a"]).toEqual(expect.objectContaining({ inputs: { topic: "arn:aws:sns:us-east-1:000000000000:net" } }));
|
|
901
|
+
expect(stderr()).toContain("net: outputs read from the deployed estate (QueueUrl, TopicArn)");
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("--seed-outputs wins: a seed it gives is not read", async () => {
|
|
905
|
+
wireClusterA();
|
|
906
|
+
liveOutputs = { net: { TopicArn: "arn:deployed" } };
|
|
907
|
+
const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
|
|
908
|
+
try {
|
|
909
|
+
const seed = join(dir, "seed.json");
|
|
910
|
+
await writeFile(seed, JSON.stringify({ net: { TopicArn: "arn:seeded" } }));
|
|
911
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", seedOutputs: [seed] }))).toBe(0);
|
|
912
|
+
expect(outputsRead).toEqual([]);
|
|
913
|
+
expect(inputs["cluster-a"]).toEqual(expect.objectContaining({ inputs: { topic: "arn:seeded" } }));
|
|
914
|
+
} finally {
|
|
915
|
+
await rm(dir, { recursive: true, force: true });
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("a read that fails stops the fan-out before anything runs, naming the seed", async () => {
|
|
920
|
+
wireClusterA();
|
|
921
|
+
liveOutputs = { net: new Error("ExpiredToken") };
|
|
922
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(1);
|
|
923
|
+
expect(ran).toEqual([]);
|
|
924
|
+
expect(stderr()).toContain("could not read the deployed outputs of net: ExpiredToken");
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("--dry-run reads nothing", async () => {
|
|
928
|
+
wireClusterA();
|
|
929
|
+
liveOutputs = { net: { TopicArn: "arn:deployed" } };
|
|
930
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
|
|
931
|
+
expect(outputsRead).toEqual([]);
|
|
932
|
+
});
|
|
933
|
+
});
|
|
@@ -34,6 +34,15 @@
|
|
|
34
34
|
* the plan before the gate is decided, and the digest is carried rather than
|
|
35
35
|
* recomputed, so the approval still stands.
|
|
36
36
|
*
|
|
37
|
+
* The approval and the progress are bound to different things (#4081). The
|
|
38
|
+
* approval is bound to the plan digest, the derivation: who runs, in what
|
|
39
|
+
* order, off which edges. The progress is bound to each component's input
|
|
40
|
+
* digest (../../components/fan-out-inputs.ts): a component the earlier
|
|
41
|
+
* attempt applied stays `already-applied` only while what it would apply is
|
|
42
|
+
* the same, and one whose template, step inputs or upstream moved runs again
|
|
43
|
+
* with a line saying so. The fix after a failure keeps the approval, and a
|
|
44
|
+
* different change of the same shape does not inherit the progress.
|
|
45
|
+
*
|
|
37
46
|
* `--wave-gate <name>` (#3049) swaps the one gate for one per wave: the same
|
|
38
47
|
* repeated command stops at each wave's gate in turn, each planned after the
|
|
39
48
|
* wave before it applied, and `chant approve fan-out <name>-wave-<n>` answers
|
|
@@ -49,6 +58,7 @@ import { changedFilesBetween, lexiconChangedUnits } from "../../components/chang
|
|
|
49
58
|
import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
|
|
50
59
|
import { EarlierWaveNotAppliedError, runFanOut } from "../../components/fan-out-run";
|
|
51
60
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../../components/fan-out-record";
|
|
61
|
+
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
52
62
|
import { describeChangedWave, withWaveRecord, type WaveRecord } from "../../gated-waves";
|
|
53
63
|
import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
|
|
54
64
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
@@ -57,6 +67,8 @@ import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary
|
|
|
57
67
|
import { approveCommand, gateIsSealed } from "../../op/gate";
|
|
58
68
|
import { getHeadCommit } from "../../lifecycle/git";
|
|
59
69
|
import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
|
|
70
|
+
import { readDeployedOutputs } from "../../components/wave-plan";
|
|
71
|
+
import { deployContext } from "../../components/driver";
|
|
60
72
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
61
73
|
import { formatError, formatInfo, formatWarning } from "../format";
|
|
62
74
|
import { FAN_OUT_GATE_OP } from "../../op/gate-name";
|
|
@@ -253,7 +265,12 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
253
265
|
// real run happens inside `runFanOut`, before the gate is decided, so an
|
|
254
266
|
// approval survives the resume rather than being re-asked for.
|
|
255
267
|
const resumePath = args.resume ? resolve(args.resume) : undefined;
|
|
268
|
+
// What each component would apply now (#4081). The record keeps it for
|
|
269
|
+
// every component that applies, and a later attempt carries progress only
|
|
270
|
+
// where it is unchanged.
|
|
271
|
+
const inputsNow = resumePath ? componentInputDigests(derived.components, { projectRoot: projectPath, env: args.env ?? "local" }) : {};
|
|
256
272
|
let priorCompleted: string[] = [];
|
|
273
|
+
let priorInputs: Record<string, string> = {};
|
|
257
274
|
let priorOutputs: Record<string, Record<string, unknown>> = {};
|
|
258
275
|
let priorWaves: WaveRecord[] = [];
|
|
259
276
|
let priorCarried: Record<string, unknown> = {};
|
|
@@ -277,11 +294,28 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
277
294
|
// as progress would block their subtrees again on the very run that
|
|
278
295
|
// exists to retry them — the operator repeated the command precisely
|
|
279
296
|
// because the thing that failed is now expected to work.
|
|
280
|
-
|
|
297
|
+
//
|
|
298
|
+
// And only the part of `completed` whose input is the same now (#4081).
|
|
299
|
+
// The plan digest identifies the derivation, which a different change
|
|
300
|
+
// of the same shape shares; the input digest says what was applied.
|
|
301
|
+
const split = progressThatCarries(attempt, inputsNow);
|
|
302
|
+
if (split.unrecorded && split.changed.length > 0) {
|
|
303
|
+
console.error(formatWarning({
|
|
304
|
+
message: `--resume: "${args.resume}" does not record what each component applied, so none of its progress carries`,
|
|
305
|
+
hint: "It was written before chant recorded component inputs. Everything selected will run.",
|
|
306
|
+
}));
|
|
307
|
+
} else {
|
|
308
|
+
for (const name of split.changed) {
|
|
309
|
+
console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const carried = new Set(split.carried);
|
|
313
|
+
priorCompleted = split.carried;
|
|
314
|
+
priorInputs = Object.fromEntries(split.carried.map((name) => [name, inputsNow[name]!]));
|
|
281
315
|
priorOutputs = Object.fromEntries(
|
|
282
|
-
Object.entries(attempt.outputs).filter(([name]) =>
|
|
316
|
+
Object.entries(attempt.outputs).filter(([name]) => carried.has(name)),
|
|
283
317
|
);
|
|
284
|
-
progress = { completed:
|
|
318
|
+
progress = { completed: split.carried };
|
|
285
319
|
priorWaves = attempt.waves ?? [];
|
|
286
320
|
// What steps kept per member (#3459), such as a choudoufu root's wave
|
|
287
321
|
// resume file. Each step decides whether what it kept still applies.
|
|
@@ -311,6 +345,36 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
311
345
|
}
|
|
312
346
|
|
|
313
347
|
const registry = await fanOutRegistry(projectPath, config);
|
|
348
|
+
const env = args.env ?? "local";
|
|
349
|
+
|
|
350
|
+
// A completed component's recorded outputs win over a `--seed-outputs`
|
|
351
|
+
// entry for the same name, as a fresh apply's outputs merge over a seed in
|
|
352
|
+
// the driver: the record holds what this fan-out itself applied. A seed
|
|
353
|
+
// neither gives is read from the deployed estate (#4104), through its
|
|
354
|
+
// capability's `outputs`, as pr-plan reads a dependency outside the change.
|
|
355
|
+
// One that cannot be read leaves its dependents' references unresolved.
|
|
356
|
+
const componentOutputs: Record<string, Record<string, unknown>> = { ...seededOutputs, ...priorOutputs };
|
|
357
|
+
const seeds = (progress ? remainingFanOut(derived.plan, derived.components, progress) : derived.plan).seeds;
|
|
358
|
+
let deployedSeeds: Record<string, string[]>;
|
|
359
|
+
try {
|
|
360
|
+
deployedSeeds = await readDeployedOutputs(
|
|
361
|
+
seeds,
|
|
362
|
+
new Map(derived.components.map((c) => [c.name, c])),
|
|
363
|
+
registry,
|
|
364
|
+
(name) => deployContext({ env }, name),
|
|
365
|
+
componentOutputs,
|
|
366
|
+
);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.error(formatError({
|
|
369
|
+
message: `could not read the deployed outputs of ${seeds.filter((s) => !(s in componentOutputs)).join(", ")}: ${err instanceof Error ? err.message : String(err)}`,
|
|
370
|
+
hint: "Nothing ran. --seed-outputs <file> gives a component's outputs instead of reading them.",
|
|
371
|
+
}));
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
for (const [name, keys] of Object.entries(deployedSeeds)) {
|
|
375
|
+
console.error(formatInfo(`${name}: outputs read from the deployed estate (${keys.length > 0 ? keys.join(", ") : "none yet"})`));
|
|
376
|
+
}
|
|
377
|
+
|
|
314
378
|
// #3061: the commit this run deploys, for the workloads' vcs.ref.head.revision.
|
|
315
379
|
const revision = await getHeadCommit().catch(() => undefined);
|
|
316
380
|
|
|
@@ -319,6 +383,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
319
383
|
// runner, ctrl-c) then still knows that the first two applied, and the
|
|
320
384
|
// repeated command picks up from there instead of redoing them.
|
|
321
385
|
const completedSoFar = new Set(priorCompleted);
|
|
386
|
+
const inputsSoFar: Record<string, string> = { ...priorInputs };
|
|
322
387
|
const failedSoFar: string[] = [];
|
|
323
388
|
const outputsSoFar = { ...priorOutputs };
|
|
324
389
|
let wavesSoFar = priorWaves;
|
|
@@ -330,6 +395,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
330
395
|
completed: [...completedSoFar].sort(),
|
|
331
396
|
failed: [...failedSoFar].sort(),
|
|
332
397
|
outputs: outputsSoFar,
|
|
398
|
+
inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
|
|
333
399
|
...(wavesSoFar.length > 0 ? { waves: wavesSoFar } : {}),
|
|
334
400
|
...(Object.keys(carriedSoFar).length > 0 ? { carried: carriedSoFar } : {}),
|
|
335
401
|
};
|
|
@@ -344,12 +410,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
344
410
|
let result;
|
|
345
411
|
try {
|
|
346
412
|
result = await runFanOut(derived.plan, derived.components, registry, {
|
|
347
|
-
env
|
|
413
|
+
env,
|
|
348
414
|
releaseIdentity: () => (revision ? { revision } : {}),
|
|
349
|
-
|
|
350
|
-
// entry for the same name, as a fresh apply's outputs merge over a seed in
|
|
351
|
-
// the driver: the record holds what this fan-out itself applied.
|
|
352
|
-
componentOutputs: { ...seededOutputs, ...priorOutputs },
|
|
415
|
+
componentOutputs,
|
|
353
416
|
...(gate ? { gate } : {}),
|
|
354
417
|
...(waveGate ? { waveGate } : {}),
|
|
355
418
|
...(waveGate
|
|
@@ -377,6 +440,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
377
440
|
onComponentSettled: (settled, outputs) => {
|
|
378
441
|
if (settled.status === "ok") {
|
|
379
442
|
completedSoFar.add(settled.component);
|
|
443
|
+
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
380
444
|
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
381
445
|
} else if (settled.status === "fail") {
|
|
382
446
|
failedSoFar.push(settled.component);
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The why chain of `graph --intent` (#4023), from work item to production
|
|
3
|
+
* release. Everything runs in a temp repo with no remote, so nothing is
|
|
4
|
+
* pushed: a workspace with a work item, a commit whose trailer links it, a
|
|
5
|
+
* green tag, a real cut, a real `components promote --release` into prod that
|
|
6
|
+
* stops at its release gate, a real `chant approve`, and the promote that
|
|
7
|
+
* follows. Only the capability registry is replaced, so no deploy step
|
|
8
|
+
* reaches a cloud, and the member's graph, so the resource node resolves to
|
|
9
|
+
* its component file without building the member.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
13
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { CapabilityRegistry, type DeployContext } from "../../components/capability";
|
|
18
|
+
import type { DriverComponent } from "../../components/driver";
|
|
19
|
+
import type { ParsedArgs } from "../registry";
|
|
20
|
+
import { appendReleaseRecord, readReleaseLedger } from "../../lifecycle/release-ledger";
|
|
21
|
+
import { readGateLedger } from "../../lifecycle/gate-ledger";
|
|
22
|
+
import { cutRelease } from "../../lifecycle/release-cut";
|
|
23
|
+
import { resetGateOrigin } from "../../lifecycle/gate-origin";
|
|
24
|
+
import { contract, REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
25
|
+
import { intentGraph, type IntentDocument } from "../../workspace/intent";
|
|
26
|
+
import intentSchema from "../../workspace/intent.schema.json";
|
|
27
|
+
import type { ReleaseArtifact } from "../../lifecycle/release-store";
|
|
28
|
+
import type { CompiledTarget, TargetCompiler } from "../../components/release-compile";
|
|
29
|
+
|
|
30
|
+
const DIGEST = `sha256:${"0123456789abcdef".repeat(4)}`;
|
|
31
|
+
|
|
32
|
+
const api: DriverComponent = {
|
|
33
|
+
name: "api",
|
|
34
|
+
deploy: [
|
|
35
|
+
{ phase: "Publish", steps: [{ kind: "publish-image", from: "archive:api.tar" }] },
|
|
36
|
+
{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
vi.mock("../../config", async () => {
|
|
41
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
42
|
+
return { ...actual, loadChantConfig: async () => ({ config: {} }) };
|
|
43
|
+
});
|
|
44
|
+
vi.mock("../../components/cli-support", () => ({
|
|
45
|
+
isComponentList: () => false,
|
|
46
|
+
resolveComponentTargets: async () => ({ success: true, targets: [api] }),
|
|
47
|
+
}));
|
|
48
|
+
vi.mock("../../components/fan-out-support", () => ({
|
|
49
|
+
fanOutRegistry: async () => {
|
|
50
|
+
const registry = new CapabilityRegistry();
|
|
51
|
+
registry.register({ kind: "publish-image", async run(_ctx: DeployContext) { return { digest: DIGEST, uri: `reg/api@${DIGEST}` }; } } as never);
|
|
52
|
+
registry.register({ kind: "apply", async run() { return {}; } } as never);
|
|
53
|
+
return registry;
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
|
|
57
|
+
vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
|
|
58
|
+
|
|
59
|
+
const { runComponentsPromote } = await import("./promote");
|
|
60
|
+
const { recordGateApproval } = await import("./approve");
|
|
61
|
+
|
|
62
|
+
function git(dir: string, ...args: string[]): string {
|
|
63
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
64
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
65
|
+
return r.stdout.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A compiler that compiles each environment to its own template and builds one image. */
|
|
69
|
+
const compiler: TargetCompiler = {
|
|
70
|
+
async compile(env): Promise<CompiledTarget> {
|
|
71
|
+
return {
|
|
72
|
+
env,
|
|
73
|
+
params: [],
|
|
74
|
+
components: ["api"],
|
|
75
|
+
buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
|
|
76
|
+
buildInputs: { api: ["api"] },
|
|
77
|
+
componentFiles: { api: "api.component.ts" },
|
|
78
|
+
templates: [{ component: "api", path: "dist/api.template.json", content: `{"env":"${env}"}\n` }],
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
|
|
82
|
+
return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: DIGEST }));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function args(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
87
|
+
return {
|
|
88
|
+
command: "components", path: "promote", format: "", fix: false, watch: false, verbose: false, help: false, live: false,
|
|
89
|
+
migrateFrom: "staging", migrateTo: "prod", actor: "bob", runId: "run-9", ...overrides,
|
|
90
|
+
} as ParsedArgs;
|
|
91
|
+
}
|
|
92
|
+
const ctx = (a: Partial<ParsedArgs>) => ({ args: args(a), plugins: [] }) as never;
|
|
93
|
+
|
|
94
|
+
const REF = join(REPO, "reference-workspace");
|
|
95
|
+
const KIND_FILES: Record<string, string> = {
|
|
96
|
+
"work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
|
|
97
|
+
"work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
|
|
98
|
+
"decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
|
|
99
|
+
"decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
|
|
100
|
+
};
|
|
101
|
+
const W001 = `---\n${JSON.stringify({ schema: 1, id: "W-001", title: "Ship the api to prod", state: "open", implements: [], needs: [], constrains: ["path:api.component.ts"], evidence: [], opened_on: "2026-10-16", supersedes: [] }, null, 2)}\n---\n\n# W-001\n`;
|
|
102
|
+
const DECLARATION = JSON.stringify({ name: "shop", schema: 1, members: [{ name: "api", dir: ".", kind: "other", because: "the api and its component" }] });
|
|
103
|
+
const intent = contract(intentSchema);
|
|
104
|
+
type Result = Exclude<IntentDocument, { error: unknown }>;
|
|
105
|
+
|
|
106
|
+
describe("the why chain from work item to production release (#4023)", () => {
|
|
107
|
+
const cwd = process.cwd();
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
process.chdir(cwd);
|
|
110
|
+
resetGateOrigin();
|
|
111
|
+
vi.restoreAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test(
|
|
115
|
+
"the trace from the api's resource node walks W-001, the linked commit, its green tag, the cut, the release gate approval and the prod deploy",
|
|
116
|
+
async () => {
|
|
117
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
118
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
119
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
120
|
+
await withTestDir(async (dir) => {
|
|
121
|
+
git(dir, "init", "-q", "-b", "main");
|
|
122
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
123
|
+
git(dir, "config", "user.name", "T");
|
|
124
|
+
const files: Record<string, string> = {
|
|
125
|
+
"chant.workspace.json": DECLARATION,
|
|
126
|
+
"chant.config.ts": `export default { environments: ["dev", "staging", "prod"] };\n`,
|
|
127
|
+
"api.component.ts": "export const api = {};\n",
|
|
128
|
+
"work/W-001-ship.md": W001,
|
|
129
|
+
...KIND_FILES,
|
|
130
|
+
};
|
|
131
|
+
for (const [path, text] of Object.entries(files)) {
|
|
132
|
+
mkdirSync(join(dir, path, ".."), { recursive: true });
|
|
133
|
+
writeFileSync(join(dir, path), text);
|
|
134
|
+
}
|
|
135
|
+
git(dir, "add", ".");
|
|
136
|
+
git(dir, "commit", "-q", "-m", "the api and W-001");
|
|
137
|
+
// The work: a commit whose trailer names W-001, made green.
|
|
138
|
+
writeFileSync(join(dir, "api.component.ts"), "export const api = { port: 8081 };\n");
|
|
139
|
+
git(dir, "commit", "-q", "-am", "serve the api on 8081", "--trailer", "Chant-Record: work:W-001");
|
|
140
|
+
const sha = git(dir, "rev-parse", "HEAD");
|
|
141
|
+
git(dir, "tag", `ci/green/${sha}`, sha);
|
|
142
|
+
process.chdir(dir);
|
|
143
|
+
|
|
144
|
+
// The release is cut from the green commit, and promoted to prod through its release gate.
|
|
145
|
+
const cut = await cutRelease({ cwd: dir, compiler, label: "v1.0.0", actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
|
|
146
|
+
const releaseDigest = cut.release.releaseDigest;
|
|
147
|
+
await appendReleaseRecord({ component: "api", env: "staging", digest: DIGEST, gitSha: sha, runId: "run-1", timestamp: "2026-10-16T10:30:00.000Z", actor: "ci" }, { cwd: dir });
|
|
148
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
149
|
+
expect((await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" })).ok).toBe(true);
|
|
150
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
151
|
+
const [deploy] = (await readReleaseLedger("prod", { cwd: dir })).records;
|
|
152
|
+
const [resolution] = (await readGateLedger("release", { cwd: dir })).resolutions;
|
|
153
|
+
|
|
154
|
+
const walk = async (region: string): Promise<Result> => {
|
|
155
|
+
const { doc } = await intentGraph({
|
|
156
|
+
cwd: dir,
|
|
157
|
+
region,
|
|
158
|
+
kinds: [join(dir, "work/work.kind.mjs")],
|
|
159
|
+
resolveNode: async (_cwd, _at, member, id) => (member === "api" && id === "api/api" ? { file: "api.component.ts", line: null } : null),
|
|
160
|
+
});
|
|
161
|
+
intent.expectValid(doc);
|
|
162
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
163
|
+
return doc;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// From the resource node: who asked for it, and who let it ship, in one read.
|
|
167
|
+
const doc = await walk("api/api");
|
|
168
|
+
expect(doc.nodes[0]).toMatchObject({ kind: "region", path: "api.component.ts", node: "api/api" });
|
|
169
|
+
const trace = doc.trace!;
|
|
170
|
+
expect(trace.reasons).toEqual([]);
|
|
171
|
+
const c = trace.commits.find((x) => x.sha === sha)!;
|
|
172
|
+
expect(c.work).toEqual({ state: "linked", items: [{ id: "work:W-001", node: "record:work/W-001", source: { ref: sha, field: "Chant-Record", value: "work:W-001" } }], problems: [] });
|
|
173
|
+
expect(c.tag).toMatchObject({ state: "green", green: { tag: `ci/green/${sha}` } });
|
|
174
|
+
expect(c.releases).toEqual([{ release: releaseDigest, link: "cut-from", source: { ref: `chant/lifecycle:_releases/${releaseDigest.replace(/:/g, "_")}.json`, field: "commit", value: sha } }]);
|
|
175
|
+
expect(c.deploys.map((d) => [d.env, d.link])).toEqual([["prod", "built-from"], ["staging", "built-from"]]);
|
|
176
|
+
|
|
177
|
+
const [release] = trace.releases;
|
|
178
|
+
expect(trace.releases).toHaveLength(1);
|
|
179
|
+
expect(release).toMatchObject({ releaseDigest, label: "v1.0.0", commit: sha, tag: { state: "green" }, cuts: [{ label: "v1.0.0", actor: "alice" }] });
|
|
180
|
+
// The gate approval: who, on which channel, when, and the change set the promote stored for it (#4022).
|
|
181
|
+
expect(release.gates).toEqual([
|
|
182
|
+
expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", resolvedBy: "alice", channel: "cli", at: resolution.timestamp, planDigest: resolution.planDigest, changeSet: resolution.planDigest, empty: false }),
|
|
183
|
+
]);
|
|
184
|
+
expect(release.gates[0].changeSetSource).toEqual({ ref: `chant/lifecycle:_change-sets/${resolution.planDigest!.replace(/:/g, "_")}.json`, field: "planDigest", value: resolution.planDigest });
|
|
185
|
+
|
|
186
|
+
// The prod deploy record, admitted by that resolution.
|
|
187
|
+
const prod = trace.deploys.find((d) => d.env === "prod")!;
|
|
188
|
+
expect(release.deploys).toEqual([prod.id]);
|
|
189
|
+
expect(prod).toMatchObject({ component: "api", runId: deploy.runId, gitSha: sha, release: releaseDigest, approver: "alice", admittedBy: { op: "release", gate: "prod-release", resolvedBy: "alice", at: resolution.timestamp } });
|
|
190
|
+
// Staging was a plain deploy of the same commit: it names no release.
|
|
191
|
+
expect(trace.deploys.find((d) => d.env === "staging")).toMatchObject({ release: null, admittedBy: null });
|
|
192
|
+
|
|
193
|
+
// The same chain from the commit itself.
|
|
194
|
+
const byCommit = await walk(sha);
|
|
195
|
+
expect(byCommit.trace!.commits).toEqual([c]);
|
|
196
|
+
expect(byCommit.trace!.releases).toEqual(trace.releases);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
240_000,
|
|
200
|
+
);
|
|
201
|
+
});
|