@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
|
@@ -8,7 +8,7 @@ import { withTestDir } from "@intentius/chant-test-utils";
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { cutRelease, differingBuildSteps, ReleaseCutError } from "./release-cut";
|
|
11
|
+
import { cutRelease, differingBuildSteps, planRelease, ReleaseCutError } from "./release-cut";
|
|
12
12
|
import {
|
|
13
13
|
appendCut,
|
|
14
14
|
computeReleaseDigest,
|
|
@@ -149,7 +149,16 @@ describe("cutRelease", () => {
|
|
|
149
149
|
expect(await readReleaseTemplate(prod.templates[0].digest, { cwd: dir })).toBe(`{"env":"prod"}\n`);
|
|
150
150
|
const { cuts } = await readCuts({ cwd: dir });
|
|
151
151
|
expect(cuts).toEqual([
|
|
152
|
-
{
|
|
152
|
+
{
|
|
153
|
+
version: 1,
|
|
154
|
+
releaseDigest: release.releaseDigest,
|
|
155
|
+
label: "v1.0.0",
|
|
156
|
+
actor: "alice",
|
|
157
|
+
timestamp: "2026-10-16T10:00:00.000Z",
|
|
158
|
+
runId: `local-${Date.parse("2026-10-16T10:00:00.000Z")}`,
|
|
159
|
+
runOrigin: { forge: "local" },
|
|
160
|
+
signature: null,
|
|
161
|
+
},
|
|
153
162
|
]);
|
|
154
163
|
expect(git(dir, "ls-tree", "-r", "--name-only", "chant/lifecycle").split("\n").filter((p) => p.endsWith("releases.jsonl"))).toEqual([]);
|
|
155
164
|
expect(await resolveRelease("v1.0.0", { cwd: dir })).toEqual({ release, label: "v1.0.0" });
|
|
@@ -409,3 +418,66 @@ describe("differingBuildSteps", () => {
|
|
|
409
418
|
expect(differingBuildSteps([t("dev", { a: [{ x: 1 }], b: [{ y: 1 }] }), t("prod", { a: [{ x: 2 }], b: [{ y: 1 }] }), t("qa", { c: [] })])).toEqual([{ component: "a", envs: ["dev", "prod"] }]);
|
|
410
419
|
});
|
|
411
420
|
});
|
|
421
|
+
|
|
422
|
+
describe("the dry run of a cut (#4027)", () => {
|
|
423
|
+
const plan = (dir: string, compiler: TargetCompiler, label: string | null) =>
|
|
424
|
+
planRelease({ cwd: dir, compiler, label, actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
|
|
425
|
+
|
|
426
|
+
test("compiles every environment and says what it would build, without building, storing or cutting", () =>
|
|
427
|
+
withTestDir(async (dir) => {
|
|
428
|
+
green(dir, initRepo(dir));
|
|
429
|
+
const compiler = fixtureCompiler();
|
|
430
|
+
const p = await plan(dir, compiler, "v1.0.0");
|
|
431
|
+
expect(compiler.compiled).toEqual(["dev", "staging", "prod"]);
|
|
432
|
+
expect(compiler.built).toEqual([]);
|
|
433
|
+
expect(p).toMatchObject({ label: "v1.0.0", releaseDigest: null, alreadyCut: null, build: [{ component: "api", env: "dev" }], reused: [], signatureRequired: false });
|
|
434
|
+
expect(p.targets.map((t) => [t.env, t.templates])).toEqual([
|
|
435
|
+
["dev", [{ component: "api", path: "dist/api.template.json" }]],
|
|
436
|
+
["staging", [{ component: "api", path: "dist/api.template.json" }]],
|
|
437
|
+
["prod", [{ component: "api", path: "dist/api.template.json" }]],
|
|
438
|
+
]);
|
|
439
|
+
expect(spawnSync("git", ["rev-parse", "--verify", "--quiet", "refs/heads/chant/lifecycle"], { cwd: dir }).status).not.toBe(0);
|
|
440
|
+
}));
|
|
441
|
+
|
|
442
|
+
test("knows the digest when every artifact is carried, and that it is already cut", () =>
|
|
443
|
+
withTestDir(async (dir) => {
|
|
444
|
+
green(dir, initRepo(dir));
|
|
445
|
+
const first = await cut(dir, fixtureCompiler(), "v1.0.0");
|
|
446
|
+
const sha = commitFile(dir, "README.md", "docs only\n");
|
|
447
|
+
green(dir, sha);
|
|
448
|
+
const p = await plan(dir, fixtureCompiler(), "v1.1.0");
|
|
449
|
+
expect(p.build).toEqual([]);
|
|
450
|
+
expect(p.reused).toEqual([{ component: "api", from: first.release.releaseDigest }]);
|
|
451
|
+
const second = await cut(dir, fixtureCompiler(), "v1.1.0");
|
|
452
|
+
expect(p.releaseDigest).toBe(second.release.releaseDigest);
|
|
453
|
+
expect((await plan(dir, fixtureCompiler(), "v1.1.0")).alreadyCut).toBe(second.release.releaseDigest);
|
|
454
|
+
expect((await plan(dir, fixtureCompiler(), null)).alreadyCut).toBe(second.release.releaseDigest);
|
|
455
|
+
}));
|
|
456
|
+
|
|
457
|
+
test("refuses a label another commit's release holds, and one that is not a label", () =>
|
|
458
|
+
withTestDir(async (dir) => {
|
|
459
|
+
green(dir, initRepo(dir));
|
|
460
|
+
await cut(dir, fixtureCompiler(), "v1.0.0");
|
|
461
|
+
green(dir, commitFile(dir, "api/Dockerfile", "FROM busybox\n"));
|
|
462
|
+
const taken = await plan(dir, fixtureCompiler(), "v1.0.0").catch((e: unknown) => e);
|
|
463
|
+
expect(taken).toBeInstanceOf(ReleaseLabelError);
|
|
464
|
+
expect((taken as ReleaseLabelError).code).toBe("release-label-taken");
|
|
465
|
+
expect((taken as ReleaseLabelError).message).toMatch(/cut from commit [0-9a-f]{12}/);
|
|
466
|
+
expect(((await plan(dir, fixtureCompiler(), "-bad").catch((e: unknown) => e)) as ReleaseLabelError).code).toBe("release-label-invalid");
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
test("lists each component's publish steps with the environments whose deploy runs them", () =>
|
|
470
|
+
withTestDir(async (dir) => {
|
|
471
|
+
green(dir, initRepo(dir));
|
|
472
|
+
const base = fixtureCompiler();
|
|
473
|
+
const compiler: TargetCompiler = {
|
|
474
|
+
...base,
|
|
475
|
+
async compile(env) {
|
|
476
|
+
const t = await base.compile(env);
|
|
477
|
+
return { ...t, publishSteps: { api: [{ kind: "publish-image", from: "archive", to: "$env.registry" }] } };
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
const p = await plan(dir, compiler, null);
|
|
481
|
+
expect(p.publishSteps).toEqual([{ component: "api", kind: "publish-image", to: "$env.registry", envs: ["dev", "staging", "prod"] }]);
|
|
482
|
+
}));
|
|
483
|
+
});
|
|
@@ -43,10 +43,14 @@ import { resolveMemberLedger } from "./member-ledger";
|
|
|
43
43
|
import { resolveCiRun } from "./release-ledger";
|
|
44
44
|
import {
|
|
45
45
|
appendCut,
|
|
46
|
+
invalidLabel,
|
|
47
|
+
labelsByDigest,
|
|
46
48
|
makeRelease,
|
|
47
49
|
persistRelease,
|
|
48
50
|
readAllReleases,
|
|
49
51
|
readCuts,
|
|
52
|
+
readRelease,
|
|
53
|
+
ReleaseLabelError,
|
|
50
54
|
type Release,
|
|
51
55
|
type ReleaseArtifact,
|
|
52
56
|
type ReleaseCut,
|
|
@@ -136,15 +140,10 @@ export async function greenState(
|
|
|
136
140
|
sha: string,
|
|
137
141
|
green: import("../workspace/declaration").CiGreen | null = null,
|
|
138
142
|
): Promise<{ state: "green" | "revoked" | "none"; ignored: Array<{ tag: string; status: string; reason: string }> }> {
|
|
139
|
-
const {
|
|
140
|
-
|
|
141
|
-
? (tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${green.branch}^{commit}`]) ??
|
|
142
|
-
tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/heads/${green.branch}^{commit}`]) ??
|
|
143
|
-
tryGit(top, ["rev-parse", "HEAD"]))
|
|
144
|
-
: null;
|
|
145
|
-
let tags: ReturnType<typeof readCiTags>;
|
|
143
|
+
const { judgedCiTags } = await import("./release-revocation");
|
|
144
|
+
let tags: Awaited<ReturnType<typeof judgedCiTags>>;
|
|
146
145
|
try {
|
|
147
|
-
tags =
|
|
146
|
+
tags = await judgedCiTags(top, green);
|
|
148
147
|
} catch (err) {
|
|
149
148
|
const { WorkspaceReadError } = await import("../workspace/declaration");
|
|
150
149
|
if (err instanceof WorkspaceReadError) throw new ReleaseCutError("declaration-invalid", err.message);
|
|
@@ -186,15 +185,28 @@ async function previousRelease(top: string, commit: string, cwd: string): Promis
|
|
|
186
185
|
return candidates[0]?.r;
|
|
187
186
|
}
|
|
188
187
|
|
|
188
|
+
/** What every cut and every dry run reads and compiles before it decides. */
|
|
189
|
+
interface Prepared {
|
|
190
|
+
cwd: string;
|
|
191
|
+
commit: string;
|
|
192
|
+
envs: readonly string[];
|
|
193
|
+
compiled: CompiledTarget[];
|
|
194
|
+
releaseRule: ReturnType<typeof import("../workspace/identity").releaseRuleFrom>;
|
|
195
|
+
/** The components to build, by the environment they are built in. */
|
|
196
|
+
toBuild: Map<string, string[]>;
|
|
197
|
+
/** The artifacts carried from an earlier release. */
|
|
198
|
+
carried: ReleaseArtifact[];
|
|
199
|
+
reused: CutResult["reused"];
|
|
200
|
+
member: string | null;
|
|
201
|
+
}
|
|
202
|
+
|
|
189
203
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* to the same artifacts, and `ReleaseLabelError` (./release-store.ts) when the
|
|
195
|
-
* label is taken or the release already carries another one.
|
|
204
|
+
* The checks and the compile a cut makes before it builds or writes: the
|
|
205
|
+
* commit is green and not revoked, the cut is signed when the release gate
|
|
206
|
+
* asks, the tree is clean, every environment compiles with the same build
|
|
207
|
+
* steps, and which artifacts are carried from an earlier release.
|
|
196
208
|
*/
|
|
197
|
-
|
|
209
|
+
async function prepare(req: CutRequest): Promise<Prepared> {
|
|
198
210
|
const cwd = resolve(req.cwd);
|
|
199
211
|
const top = tryGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
200
212
|
if (!top) throw new ReleaseCutError("not-a-git-repository", "a release is cut from a commit, and this directory is not in a git repository");
|
|
@@ -296,11 +308,22 @@ export async function cutRelease(req: CutRequest): Promise<CutResult> {
|
|
|
296
308
|
}
|
|
297
309
|
toBuild.set(target.env, [...(toBuild.get(target.env) ?? []), component]);
|
|
298
310
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
311
|
+
const ledger = await resolveMemberLedger(cwd);
|
|
312
|
+
return {
|
|
313
|
+
cwd,
|
|
314
|
+
commit,
|
|
315
|
+
envs,
|
|
316
|
+
compiled,
|
|
317
|
+
releaseRule,
|
|
318
|
+
toBuild,
|
|
319
|
+
carried: artifacts,
|
|
320
|
+
reused,
|
|
321
|
+
member: ledger.members.length > 0 ? ledger.members.join("/") : null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
303
324
|
|
|
325
|
+
/** The targets and template bytes of a compile, each template by its content digest. */
|
|
326
|
+
function targetsOf(compiled: readonly CompiledTarget[]): { targets: ReleaseTarget[]; templates: TemplateContent[] } {
|
|
304
327
|
const templates: TemplateContent[] = [];
|
|
305
328
|
const targets: ReleaseTarget[] = compiled.map((t) => ({
|
|
306
329
|
env: t.env,
|
|
@@ -311,9 +334,121 @@ export async function cutRelease(req: CutRequest): Promise<CutResult> {
|
|
|
311
334
|
return { component: tpl.component, path: tpl.path, digest };
|
|
312
335
|
}),
|
|
313
336
|
}));
|
|
337
|
+
return { targets, templates };
|
|
338
|
+
}
|
|
314
339
|
|
|
315
|
-
|
|
316
|
-
|
|
340
|
+
/** What a dry run of the cut found (#4027): what the cut would do, with nothing built, signed or written. */
|
|
341
|
+
export interface CutPlan {
|
|
342
|
+
commit: string;
|
|
343
|
+
member: string | null;
|
|
344
|
+
label: string | null;
|
|
345
|
+
/** The release's digest when no artifact needs building, so it is known now; null when a build decides it. */
|
|
346
|
+
releaseDigest: string | null;
|
|
347
|
+
/** The digest already cut under `label`, or this digest when it is already cut: the cut would then write no new release. */
|
|
348
|
+
alreadyCut: string | null;
|
|
349
|
+
/** Each environment's target: its params, and its templates by component and path. */
|
|
350
|
+
targets: Array<{ env: string; params: ReleaseTarget["params"]; templates: Array<{ component: string | null; path: string }> }>;
|
|
351
|
+
/** The components whose artifacts would be built, and the environment each is built in. */
|
|
352
|
+
build: Array<{ component: string; env: string }>;
|
|
353
|
+
/** The components whose artifacts would be carried from an earlier release. */
|
|
354
|
+
reused: CutResult["reused"];
|
|
355
|
+
/** Each component's publish steps, with the environments whose deploy runs them. */
|
|
356
|
+
publishSteps: Array<{ component: string; kind: string; to: string | null; envs: string[] }>;
|
|
357
|
+
/** Whether the release gate in identity.gates requires a signed cut. */
|
|
358
|
+
signatureRequired: boolean;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The dry run of {@link cutRelease} (#4027): the same checks and compile,
|
|
363
|
+
* then what the cut would build, carry and record, without building an
|
|
364
|
+
* artifact, signing, or writing anything. Throws what the cut throws before
|
|
365
|
+
* it builds, and `ReleaseLabelError` when the label is invalid or names a
|
|
366
|
+
* release of another commit.
|
|
367
|
+
*/
|
|
368
|
+
export async function planRelease(req: CutRequest): Promise<CutPlan> {
|
|
369
|
+
const p = await prepare(req);
|
|
370
|
+
const { targets } = targetsOf(p.compiled);
|
|
371
|
+
const build: CutPlan["build"] = [];
|
|
372
|
+
for (const env of p.envs) for (const component of p.toBuild.get(env) ?? []) build.push({ component, env });
|
|
373
|
+
const releaseDigest = build.length === 0 ? makeRelease({ member: p.member, commit: p.commit, artifactDigests: p.carried, targets }).releaseDigest : null;
|
|
374
|
+
|
|
375
|
+
const { cuts } = await readCuts({ cwd: p.cwd });
|
|
376
|
+
const labels = labelsByDigest(cuts);
|
|
377
|
+
let alreadyCut: string | null = null;
|
|
378
|
+
if (req.label !== null) {
|
|
379
|
+
const why = invalidLabel(req.label);
|
|
380
|
+
if (why) throw new ReleaseLabelError("release-label-invalid", why);
|
|
381
|
+
const named = [...labels].find(([, l]) => l === req.label)?.[0];
|
|
382
|
+
if (named !== undefined) {
|
|
383
|
+
const other = await readRelease(named, { cwd: p.cwd });
|
|
384
|
+
const sameCommit = other?.commit === p.commit;
|
|
385
|
+
if (!sameCommit || (releaseDigest !== null && named !== releaseDigest)) {
|
|
386
|
+
throw new ReleaseLabelError(
|
|
387
|
+
"release-label-taken",
|
|
388
|
+
`the label "${req.label}" already names release ${named}${other && !sameCommit ? `, cut from commit ${other.commit.slice(0, 12)}` : ""}`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
alreadyCut = named;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (releaseDigest !== null && alreadyCut === null && cuts.some((c) => c.releaseDigest === releaseDigest)) {
|
|
395
|
+
const own = labels.get(releaseDigest);
|
|
396
|
+
if (own !== undefined && req.label !== null && own !== req.label) {
|
|
397
|
+
throw new ReleaseLabelError("release-already-labelled", `release ${releaseDigest} is already labelled "${own}", and a release takes one label`);
|
|
398
|
+
}
|
|
399
|
+
alreadyCut = releaseDigest;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const steps = new Map<string, CutPlan["publishSteps"][number]>();
|
|
403
|
+
for (const t of p.compiled) {
|
|
404
|
+
for (const [component, list] of Object.entries(t.publishSteps ?? {})) {
|
|
405
|
+
for (const step of list as Array<{ kind?: unknown; to?: unknown }>) {
|
|
406
|
+
const kind = typeof step.kind === "string" ? step.kind : "publish";
|
|
407
|
+
const to = typeof step.to === "string" ? step.to : null;
|
|
408
|
+
const key = JSON.stringify([component, kind, to]);
|
|
409
|
+
const entry = steps.get(key) ?? { component, kind, to, envs: [] };
|
|
410
|
+
if (!entry.envs.includes(t.env)) entry.envs.push(t.env);
|
|
411
|
+
steps.set(key, entry);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
commit: p.commit,
|
|
418
|
+
member: p.member,
|
|
419
|
+
label: req.label,
|
|
420
|
+
releaseDigest,
|
|
421
|
+
alreadyCut,
|
|
422
|
+
targets: targets.map((t) => ({ env: t.env, params: t.params, templates: t.templates.map(({ component, path }) => ({ component, path })) })),
|
|
423
|
+
build,
|
|
424
|
+
reused: p.reused,
|
|
425
|
+
publishSteps: [...steps.values()],
|
|
426
|
+
signatureRequired: p.releaseRule.requirement !== null,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Cut a release of the member at `req.cwd` from its checkout's `HEAD`. Writes
|
|
432
|
+
* the release and the cut to the local `chant/lifecycle` branch and does not
|
|
433
|
+
* push. Throws {@link ReleaseCutError} before anything is written when the
|
|
434
|
+
* commit isn't green, the tree is dirty, or an environment can't be compiled
|
|
435
|
+
* to the same artifacts, and `ReleaseLabelError` (./release-store.ts) when the
|
|
436
|
+
* label is taken or the release already carries another one.
|
|
437
|
+
*
|
|
438
|
+
* The cut line names its run (#4027): the CI run's id and origin in CI, and
|
|
439
|
+
* outside CI an id minted from the cut's time, `local-<epoch ms>`, with the
|
|
440
|
+
* origin `{ forge: "local" }`.
|
|
441
|
+
*/
|
|
442
|
+
export async function cutRelease(req: CutRequest): Promise<CutResult> {
|
|
443
|
+
const p = await prepare(req);
|
|
444
|
+
const { cwd, commit, compiled, releaseRule, reused } = p;
|
|
445
|
+
const artifacts = [...p.carried];
|
|
446
|
+
for (const env of p.envs) {
|
|
447
|
+
const names = p.toBuild.get(env);
|
|
448
|
+
if (names) artifacts.push(...(await req.compiler.buildArtifacts(env, names)));
|
|
449
|
+
}
|
|
450
|
+
const { targets, templates } = targetsOf(compiled);
|
|
451
|
+
const release = makeRelease({ member: p.member, commit, artifactDigests: artifacts, targets });
|
|
317
452
|
const timestamp = (req.now ?? (() => new Date()))().toISOString();
|
|
318
453
|
const signed = { releaseDigest: release.releaseDigest, label: req.label, actor: req.actor, timestamp };
|
|
319
454
|
let signature: string | null = null;
|
|
@@ -352,7 +487,8 @@ export async function cutRelease(req: CutRequest): Promise<CutResult> {
|
|
|
352
487
|
label: req.label,
|
|
353
488
|
actor: req.actor,
|
|
354
489
|
timestamp,
|
|
355
|
-
|
|
490
|
+
runId: ci?.runId ?? `local-${Date.parse(timestamp)}`,
|
|
491
|
+
runOrigin: ci?.runOrigin ?? { forge: "local" },
|
|
356
492
|
signature,
|
|
357
493
|
},
|
|
358
494
|
{ cwd },
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A flag change is a commit, so it is gated like any change (#4024, ws-104):
|
|
3
|
+
* the release cut from it carries the flag's new value as a param whose
|
|
4
|
+
* source names the flag record, keeps the previous release's artifact
|
|
5
|
+
* digests when no artifact source changed, and its change set for the
|
|
6
|
+
* environment whose value moved shows the difference while the others are
|
|
7
|
+
* empty. On the fixture in `../workspace/__fixtures__/flag-workspace.ts`,
|
|
8
|
+
* with a compiler that resolves each environment's parameters as a build
|
|
9
|
+
* does and compiles them into the template.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
15
|
+
import { cutRelease } from "./release-cut";
|
|
16
|
+
import type { ReleaseArtifact, Release } from "./release-store";
|
|
17
|
+
import type { ReleaseRecord } from "./release-ledger";
|
|
18
|
+
import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
|
|
19
|
+
import { chantComponentPart } from "../components/release-gate";
|
|
20
|
+
import type { DriverComponent } from "../components/driver";
|
|
21
|
+
import { composeChangeSet, isEmptyChangeSet } from "../change-set";
|
|
22
|
+
import type { BuildParamsConfig } from "../build-params";
|
|
23
|
+
import { resolveFlaggedBuildParams } from "../flag-params";
|
|
24
|
+
import { cleanScratch, commitAll, git } from "../workspace/__fixtures__/contract-repo";
|
|
25
|
+
import { flagRecord, flagWorkspace } from "../workspace/__fixtures__/flag-workspace";
|
|
26
|
+
|
|
27
|
+
afterAll(cleanScratch);
|
|
28
|
+
|
|
29
|
+
const SHOP: BuildParamsConfig = { newCheckout: { type: "boolean", default: false } };
|
|
30
|
+
const IMAGE = `sha256:${"a".repeat(64)}`;
|
|
31
|
+
|
|
32
|
+
/** Compiles each environment with the parameters a build resolves there, flags included, into the shop's template. */
|
|
33
|
+
function flagCompiler(memberDir: string): TargetCompiler & { built: string[][] } {
|
|
34
|
+
const built: string[][] = [];
|
|
35
|
+
return {
|
|
36
|
+
built,
|
|
37
|
+
async compile(env): Promise<CompiledTarget> {
|
|
38
|
+
const resolved = await resolveFlaggedBuildParams(memberDir, SHOP, { echo: false }, env);
|
|
39
|
+
if (!resolved.success) throw new Error(resolved.errors.join("\n"));
|
|
40
|
+
const params = resolved.provenance.map(({ name, value, source }) => ({ name, value, source }));
|
|
41
|
+
return {
|
|
42
|
+
env,
|
|
43
|
+
params,
|
|
44
|
+
components: ["shop"],
|
|
45
|
+
buildSteps: { shop: [{ kind: "docker-build", context: "app", into: "shop.tar" }] },
|
|
46
|
+
buildInputs: { shop: ["app", "app/Dockerfile"] },
|
|
47
|
+
componentFiles: { shop: "shop.component.ts" },
|
|
48
|
+
templates: [{ component: "shop", path: "dist/shop.json", content: JSON.stringify({ env, params: Object.fromEntries(params.map((p) => [p.name, p.value])) }) + "\n" }],
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
async buildArtifacts(env, components): Promise<ReleaseArtifact[]> {
|
|
52
|
+
built.push([env, ...components]);
|
|
53
|
+
return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: IMAGE }));
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const shop: DriverComponent = { name: "shop", deploy: [{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/shop.json" }] }] };
|
|
59
|
+
|
|
60
|
+
const deployed = (release: Release, env: string): ReleaseRecord => ({
|
|
61
|
+
version: 1,
|
|
62
|
+
component: "shop",
|
|
63
|
+
env,
|
|
64
|
+
digest: IMAGE,
|
|
65
|
+
gitSha: release.commit,
|
|
66
|
+
runId: "run-1",
|
|
67
|
+
timestamp: "2026-10-16T00:00:00.000Z",
|
|
68
|
+
actor: "ci",
|
|
69
|
+
releaseDigest: release.releaseDigest,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("a flag change is a release of its own (#4024)", () => {
|
|
73
|
+
test("it carries the new value with source flag:<id>, the previous artifact digests, and a change set only where the value moved", async () => {
|
|
74
|
+
const root = flagWorkspace({ "shop/app/Dockerfile": "FROM scratch\n" });
|
|
75
|
+
const member = join(root, "shop");
|
|
76
|
+
const compiler = flagCompiler(member);
|
|
77
|
+
const cut = (label: string) => cutRelease({ cwd: member, compiler, label, actor: "alice", env: {}, now: () => new Date("2026-10-17T10:00:00.000Z") });
|
|
78
|
+
|
|
79
|
+
const first = git(root, "rev-parse", "HEAD");
|
|
80
|
+
git(root, "tag", `ci/green/${first}`, first);
|
|
81
|
+
const v1 = (await cut("v1")).release;
|
|
82
|
+
expect(compiler.built).toEqual([["dev", "shop"]]);
|
|
83
|
+
expect(v1.targets.find((t) => t.env === "prod")!.params).toEqual([{ name: "newCheckout", value: false, source: "flag:new-checkout" }]);
|
|
84
|
+
|
|
85
|
+
// Turning the flag on in prod is a commit to the flag record, and nothing else.
|
|
86
|
+
const flagFile = join(root, "flags", "new-checkout.flag.md");
|
|
87
|
+
writeFileSync(flagFile, flagRecord("new-checkout", { work: "W-002", member: "shop", param: "newCheckout", values: { dev: true, prod: true } }));
|
|
88
|
+
expect(readFileSync(flagFile, "utf-8")).toMatch(/"prod": true/);
|
|
89
|
+
const second = commitAll(root, "turn new-checkout on in prod");
|
|
90
|
+
git(root, "tag", `ci/green/${second}`, second);
|
|
91
|
+
const result = await cut("v2");
|
|
92
|
+
const v2 = result.release;
|
|
93
|
+
|
|
94
|
+
// A config-only release: no artifact was built again, and the digests are the previous release's.
|
|
95
|
+
expect(compiler.built).toEqual([["dev", "shop"]]);
|
|
96
|
+
expect(result.reused).toEqual([{ component: "shop", from: v1.releaseDigest }]);
|
|
97
|
+
expect(v2.artifactDigests).toEqual(v1.artifactDigests);
|
|
98
|
+
expect(v2.commit).toBe(second);
|
|
99
|
+
expect(v2.releaseDigest).not.toBe(v1.releaseDigest);
|
|
100
|
+
expect(v2.targets.find((t) => t.env === "prod")!.params).toEqual([{ name: "newCheckout", value: true, source: "flag:new-checkout" }]);
|
|
101
|
+
|
|
102
|
+
// The flag change alters prod's change set, and leaves dev's and staging's empty.
|
|
103
|
+
const part = (env: string) => chantComponentPart({ component: shop, release: v2, env, digest: IMAGE, current: { record: deployed(v1, env), release: v1 } });
|
|
104
|
+
const prod = part("prod");
|
|
105
|
+
expect(prod.entries).toHaveLength(1);
|
|
106
|
+
expect(prod.entries[0]).toMatchObject({ address: "shop", action: "update", scope: "prod" });
|
|
107
|
+
expect(prod.entries[0].attributes.map((a) => a.path)).toEqual(["template:dist/shop.json", "param:newCheckout"]);
|
|
108
|
+
expect(prod.entries[0].attributes.find((a) => a.path === "param:newCheckout")).toEqual({ path: "param:newCheckout", before: "false", after: "true" });
|
|
109
|
+
expect(isEmptyChangeSet(composeChangeSet([prod]))).toBe(false);
|
|
110
|
+
for (const env of ["dev", "staging"]) expect(isEmptyChangeSet(composeChangeSet([part(env)]))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The join through the release digest (#4043), against a temp repo: two
|
|
3
|
+
* releases of one commit, and deploys of one release in two environments.
|
|
4
|
+
* The end-to-end chain, with a real cut, promote and approval, is in
|
|
5
|
+
* cli/handlers/release-join.e2e.test.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect } from "vitest";
|
|
8
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { joinRelease } from "./release-join";
|
|
13
|
+
import { appendCut, makeRelease, persistRelease } from "./release-store";
|
|
14
|
+
import { appendReleaseRecord } from "./release-ledger";
|
|
15
|
+
|
|
16
|
+
function git(dir: string, ...args: string[]): string {
|
|
17
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
18
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
19
|
+
return r.stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const subject = (commit: string, digest: string) => ({
|
|
23
|
+
member: null,
|
|
24
|
+
commit,
|
|
25
|
+
artifactDigests: [{ component: "api", kind: "image" as const, path: "api.tar", digest }],
|
|
26
|
+
targets: [{ env: "dev", templates: [], params: [] }],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("joinRelease", () => {
|
|
30
|
+
test("a commit joins to every release cut from it, each with its own deploys across environments", async () => {
|
|
31
|
+
await withTestDir(async (dir) => {
|
|
32
|
+
git(dir, "init", "-q", "-b", "main");
|
|
33
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
34
|
+
git(dir, "config", "user.name", "T");
|
|
35
|
+
writeFileSync(join(dir, "README.md"), "x\n");
|
|
36
|
+
git(dir, "add", ".");
|
|
37
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
38
|
+
const sha = git(dir, "rev-parse", "HEAD");
|
|
39
|
+
const one = makeRelease(subject(sha, "sha256:1"));
|
|
40
|
+
const two = makeRelease(subject(sha, "sha256:2"));
|
|
41
|
+
for (const [r, label] of [[one, "v1"], [two, null]] as const) {
|
|
42
|
+
await persistRelease(r, [], { cwd: dir });
|
|
43
|
+
await appendCut({ releaseDigest: r.releaseDigest, label, actor: "alice", timestamp: "2026-10-16T10:00:00.000Z", runOrigin: null, signature: null }, { cwd: dir });
|
|
44
|
+
}
|
|
45
|
+
const rec = (env: string, runId: string, timestamp: string, releaseDigest?: string) =>
|
|
46
|
+
appendReleaseRecord({ component: "api", env, digest: "sha256:1", gitSha: sha, runId, timestamp, actor: "ci", ...(releaseDigest ? { releaseDigest } : {}) }, { cwd: dir });
|
|
47
|
+
await rec("prod", "r3", "2026-10-16T13:00:00.000Z", one.releaseDigest);
|
|
48
|
+
await rec("dev", "r1", "2026-10-16T11:00:00.000Z", one.releaseDigest);
|
|
49
|
+
await rec("dev", "r2", "2026-10-16T12:00:00.000Z"); // a deploy of the commit that named no release
|
|
50
|
+
await rec("dev", "r4", "2026-10-16T14:00:00.000Z", two.releaseDigest);
|
|
51
|
+
|
|
52
|
+
const { releases } = await joinRelease({ commit: sha }, { cwd: dir });
|
|
53
|
+
expect(releases.map((r) => [r.label, r.deploys.map((d) => `${d.env}:${d.runId}`)])).toEqual(
|
|
54
|
+
expect.arrayContaining([["v1", ["dev:r1", "prod:r3"]], [null, ["dev:r4"]]]),
|
|
55
|
+
);
|
|
56
|
+
expect(releases).toHaveLength(2);
|
|
57
|
+
expect(releases.every((r) => r.commit === sha)).toBe(true);
|
|
58
|
+
expect(releases.find((r) => r.label === "v1")!.gates).toEqual({ pending: [], resolutions: [] });
|
|
59
|
+
|
|
60
|
+
// The unlabelled release is reached by its digest, and the record that named no release joins nowhere.
|
|
61
|
+
expect((await joinRelease({ release: two.releaseDigest }, { cwd: dir })).releases[0].deploys.map((d) => d.runId)).toEqual(["r4"]);
|
|
62
|
+
expect((await joinRelease({ deploy: { releaseDigest: undefined } }, { cwd: dir })).releases).toEqual([]);
|
|
63
|
+
expect((await joinRelease({ release: "nope" }, { cwd: dir })).reason).toMatch(/no release is labelled or digested "nope"/);
|
|
64
|
+
const ghost = `jcs1-sha256:${"9".repeat(64)}`;
|
|
65
|
+
expect((await joinRelease({ gate: { releaseDigest: ghost } }, { cwd: dir })).reason).toMatch(/file is not on this checkout/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|