@intentius/chant 0.124.26 → 0.124.28
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/README.md +1 -1
- package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/dist/__golden__/wave-loop/op-held-on.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/dist/__golden__/wave-loop/op-per-run.json +7 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
- package/dist/__golden__/wave-loop/op-sealed.json +3 -0
- package/dist/__golden__/wave-loop/op-spend.json +3 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/dist/change-set-graph.d.ts +56 -0
- package/dist/change-set-graph.d.ts.map +1 -0
- package/dist/change-set-graph.js +128 -0
- package/dist/change-set.d.ts +52 -3
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +35 -4
- package/dist/chant-version.d.ts +9 -0
- package/dist/chant-version.d.ts.map +1 -0
- package/dist/chant-version.js +16 -0
- package/dist/cli/commands/build.d.ts +8 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -8
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.js +30 -2
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +13 -2
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.js +17 -8
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +19 -4
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +28 -0
- package/dist/cli/handlers/change-set.d.ts +10 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.js +51 -1
- package/dist/cli/handlers/fan-out.d.ts +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +14 -3
- package/dist/cli/handlers/pr.d.ts +8 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +148 -24
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +256 -21
- package/dist/cli/handlers/release.d.ts +34 -0
- package/dist/cli/handlers/release.d.ts.map +1 -0
- package/dist/cli/handlers/release.js +203 -0
- package/dist/cli/handlers/run.d.ts +28 -2
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +170 -13
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +88 -13
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +49 -3
- package/dist/cli/registry.d.ts +29 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +29 -0
- package/dist/cli/run-events.d.ts.map +1 -0
- package/dist/cli/run-events.js +47 -0
- package/dist/cli/version.d.ts +3 -5
- package/dist/cli/version.d.ts.map +1 -1
- package/dist/cli/version.js +3 -13
- package/dist/codegen/docs-in-site.d.ts +71 -0
- package/dist/codegen/docs-in-site.d.ts.map +1 -0
- package/dist/codegen/docs-in-site.js +177 -0
- package/dist/codegen/docs-types.d.ts +14 -1
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +18 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/docs.js +40 -7
- package/dist/codegen/rehype-base-url.mjs +15 -9
- package/dist/components/auto-release.d.ts +4 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +2 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +10 -9
- package/dist/components/driver.d.ts +42 -9
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +81 -54
- package/dist/components/fan-out-run.d.ts +7 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +35 -26
- package/dist/components/pr-run.d.ts +11 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +20 -1
- package/dist/components/promote-order.d.ts +38 -0
- package/dist/components/promote-order.d.ts.map +1 -0
- package/dist/components/promote-order.js +62 -0
- package/dist/components/promote.d.ts +15 -2
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +17 -0
- package/dist/components/release-compile.d.ts +133 -0
- package/dist/components/release-compile.d.ts.map +1 -0
- package/dist/components/release-compile.js +376 -0
- package/dist/components/release-gate.d.ts +125 -0
- package/dist/components/release-gate.d.ts.map +1 -0
- package/dist/components/release-gate.js +282 -0
- package/dist/components/release-promote.d.ts +96 -0
- package/dist/components/release-promote.d.ts.map +1 -0
- package/dist/components/release-promote.js +204 -0
- package/dist/components/run-progress.d.ts +7 -0
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +3 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lifecycle/change-set-store.d.ts +67 -0
- package/dist/lifecycle/change-set-store.d.ts.map +1 -0
- package/dist/lifecycle/change-set-store.js +96 -0
- package/dist/lifecycle/commit-sha.d.ts +13 -0
- package/dist/lifecycle/commit-sha.d.ts.map +1 -0
- package/dist/lifecycle/commit-sha.js +21 -0
- package/dist/lifecycle/gate-change-set.d.ts +69 -0
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
- package/dist/lifecycle/gate-change-set.js +106 -0
- package/dist/lifecycle/gate-ledger.d.ts +49 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +18 -0
- package/dist/lifecycle/release-cut.d.ts +107 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -0
- package/dist/lifecycle/release-cut.js +273 -0
- package/dist/lifecycle/release-ledger.d.ts +54 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +53 -20
- package/dist/lifecycle/release-store.d.ts +190 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -0
- package/dist/lifecycle/release-store.js +318 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +2 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.js +4 -2
- package/dist/op/activities/source-release.d.ts.map +1 -1
- package/dist/op/activities/source-release.js +7 -3
- package/dist/op/activities/source-rollback.d.ts.map +1 -1
- package/dist/op/activities/source-rollback.js +7 -3
- package/dist/op/activity-registry.d.ts +6 -3
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.js +7 -5
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +14 -7
- package/dist/op/gate-name.d.ts +11 -2
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-name.js +12 -3
- package/dist/op/gate-run-locator.d.ts +22 -0
- package/dist/op/gate-run-locator.d.ts.map +1 -1
- package/dist/op/gate-run-locator.js +31 -0
- package/dist/op/gate.d.ts +27 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +57 -3
- package/dist/op/local-executor.d.ts +36 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +158 -52
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -0
- package/dist/op/run-live.d.ts +2 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/run-live.js +1 -1
- package/dist/op/runtime.d.ts +46 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +12 -2
- package/dist/point-decision.d.ts +2 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +9 -2
- package/dist/run-event-store.d.ts +77 -0
- package/dist/run-event-store.d.ts.map +1 -0
- package/dist/run-event-store.js +204 -0
- package/dist/run-event-stream.d.ts +132 -0
- package/dist/run-event-stream.d.ts.map +1 -0
- package/dist/run-event-stream.js +238 -0
- package/dist/run-event.d.ts +267 -0
- package/dist/run-event.d.ts.map +1 -0
- package/dist/run-event.js +207 -0
- package/dist/wave-runtime.mjs +225 -242
- package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
- package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/run-event-check.js +59 -0
- package/dist/workspace/change-set-show.d.ts +56 -0
- package/dist/workspace/change-set-show.d.ts.map +1 -0
- package/dist/workspace/change-set-show.js +54 -0
- package/dist/workspace/change-set-show.schema.json +102 -0
- package/dist/workspace/change-set.schema.json +28 -1
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes-cli.js +10 -1
- package/dist/workspace/changes.d.ts +37 -0
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/changes.js +49 -2
- package/dist/workspace/changes.schema.json +135 -0
- package/dist/workspace/ci-green-cli.d.ts +19 -4
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +75 -12
- package/dist/workspace/ci-green-forge.d.ts +20 -3
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +6 -4
- package/dist/workspace/ci-green-workflow.d.ts +11 -0
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +6 -0
- package/dist/workspace/ci-green.d.ts +273 -13
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +449 -38
- package/dist/workspace/ci-last-green.schema.json +32 -2
- package/dist/workspace/ci-list.schema.json +525 -0
- package/dist/workspace/commit-links.d.ts +85 -0
- package/dist/workspace/commit-links.d.ts.map +1 -0
- package/dist/workspace/commit-links.js +163 -0
- package/dist/workspace/declaration.d.ts +19 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +1 -1
- package/dist/workspace/declaration.schema.json +12 -0
- package/dist/workspace/identity-gate-rule.d.ts +37 -0
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +72 -7
- package/dist/workspace/identity.d.ts +12 -0
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/identity.js +13 -1
- package/dist/workspace/intent.d.ts +6 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +51 -1
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +2 -0
- package/dist/workspace/ls.schema.json +3 -1
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +6 -0
- package/dist/workspace/promotion-order.d.ts +41 -0
- package/dist/workspace/promotion-order.d.ts.map +1 -0
- package/dist/workspace/promotion-order.js +95 -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 +4 -1
- package/dist/workspace/releases.d.ts +72 -0
- package/dist/workspace/releases.d.ts.map +1 -0
- package/dist/workspace/releases.js +94 -0
- package/dist/workspace/releases.schema.json +391 -0
- package/dist/workspace/run-event.schema.json +288 -0
- package/dist/workspace/status-gates.d.ts +26 -1
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +31 -2
- package/dist/workspace/status-stewards.d.ts +4 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +2 -0
- package/dist/workspace/status.d.ts +39 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +48 -1
- package/dist/workspace/status.schema.json +70 -1
- package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
- package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
- package/dist/workspace/trust/ci-tag-statement.js +123 -0
- package/dist/workspace/trust/op-run-statement.d.ts +91 -0
- package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/op-run-statement.js +143 -0
- package/dist/workspace/trust/rotation.d.ts +18 -2
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.js +65 -16
- package/dist/workspace/trust/seal.d.ts +11 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +9 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/ssh-commit.js +10 -10
- package/package.json +1 -1
- package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/src/__golden__/wave-loop/fan-out-only.json +6 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/src/__golden__/wave-loop/op-cost-member.json +3 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/src/__golden__/wave-loop/op-held-on.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/src/__golden__/wave-loop/op-per-run.json +7 -0
- package/src/__golden__/wave-loop/op-pr-review.json +7 -0
- package/src/__golden__/wave-loop/op-sealed.json +3 -0
- package/src/__golden__/wave-loop/op-spend.json +3 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/src/change-set-graph.test.ts +72 -0
- package/src/change-set-graph.ts +133 -0
- package/src/change-set.ts +76 -6
- package/src/chant-version.ts +16 -0
- package/src/cli/commands/build.ts +19 -8
- package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
- package/src/cli/commands/check-lexicon-docs.ts +39 -2
- package/src/cli/commands/check-lexicon.ts +14 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
- package/src/cli/commands/init-lexicon.test.ts +25 -0
- package/src/cli/commands/init-lexicon.ts +18 -8
- package/src/cli/compiled-cli.e2e.test.ts +31 -6
- package/src/cli/handlers/approve-release.test.ts +80 -0
- package/src/cli/handlers/approve.ts +18 -2
- package/src/cli/handlers/build.test.ts +52 -0
- package/src/cli/handlers/build.ts +29 -0
- package/src/cli/handlers/change-set.ts +49 -1
- package/src/cli/handlers/fan-out.test.ts +48 -0
- package/src/cli/handlers/fan-out.ts +15 -3
- package/src/cli/handlers/operator.test.ts +37 -0
- package/src/cli/handlers/pr.test.ts +194 -0
- package/src/cli/handlers/pr.ts +168 -34
- package/src/cli/handlers/promote.test.ts +389 -5
- package/src/cli/handlers/promote.ts +279 -20
- package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
- package/src/cli/handlers/release.e2e.test.ts +85 -0
- package/src/cli/handlers/release.ts +206 -0
- package/src/cli/handlers/run-auto-release.test.ts +71 -0
- package/src/cli/handlers/run-events-cmd.test.ts +92 -0
- package/src/cli/handlers/run.test.ts +30 -0
- package/src/cli/handlers/run.ts +176 -15
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +88 -13
- package/src/cli/mcp/never-over-mcp.test.ts +2 -0
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +37 -3
- package/src/cli/registry.ts +29 -3
- package/src/cli/run-events.ts +54 -0
- package/src/cli/version.ts +3 -13
- package/src/codegen/docs-in-site.test.ts +166 -0
- package/src/codegen/docs-in-site.ts +182 -0
- package/src/codegen/docs-site/AgentPrompt.astro +26 -1
- package/src/codegen/docs-types.ts +14 -1
- package/src/codegen/docs.ts +60 -7
- package/src/codegen/rehype-base-url.d.mts +3 -1
- package/src/codegen/rehype-base-url.mjs +15 -9
- package/src/codegen/rehype-base-url.test.ts +34 -0
- package/src/components/auto-release.test.ts +21 -0
- package/src/components/auto-release.ts +6 -1
- package/src/components/cli-support.test.ts +82 -0
- package/src/components/cli-support.ts +16 -9
- package/src/components/driver.ts +129 -56
- package/src/components/fan-out-run.ts +56 -35
- package/src/components/pr-run.ts +30 -1
- package/src/components/promote-order.test.ts +90 -0
- package/src/components/promote-order.ts +75 -0
- package/src/components/promote.test.ts +10 -1
- package/src/components/promote.ts +26 -1
- package/src/components/release-compile.test.ts +147 -0
- package/src/components/release-compile.ts +445 -0
- package/src/components/release-gate.test.ts +215 -0
- package/src/components/release-gate.ts +341 -0
- package/src/components/release-promote.test.ts +159 -0
- package/src/components/release-promote.ts +232 -0
- package/src/components/run-progress.ts +7 -0
- package/src/components/wave-plan.ts +1 -1
- package/src/config.ts +7 -1
- package/src/index.ts +2 -0
- package/src/lifecycle/change-set-store.test.ts +217 -0
- package/src/lifecycle/change-set-store.ts +112 -0
- package/src/lifecycle/commit-sha.ts +23 -0
- package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
- package/src/lifecycle/gate-change-set.ts +133 -0
- package/src/lifecycle/gate-ledger.test.ts +33 -0
- package/src/lifecycle/gate-ledger.ts +66 -1
- package/src/lifecycle/release-cut-signed.test.ts +180 -0
- package/src/lifecycle/release-cut.test.ts +411 -0
- package/src/lifecycle/release-cut.ts +367 -0
- package/src/lifecycle/release-ledger.test.ts +14 -0
- package/src/lifecycle/release-ledger.ts +85 -17
- package/src/lifecycle/release-store.ts +423 -0
- package/src/lifecycle/run-ledger.test.ts +10 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/op/activities/change-coverage.ts +4 -1
- package/src/op/activities/source-release.ts +8 -3
- package/src/op/activities/source-rollback.ts +8 -4
- package/src/op/activity-registry.ts +7 -5
- package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
- package/src/op/discover-root.test.ts +39 -0
- package/src/op/discover.ts +15 -8
- package/src/op/gate-name.ts +13 -3
- package/src/op/gate-release.test.ts +126 -0
- package/src/op/gate-run-locator.test.ts +110 -0
- package/src/op/gate-run-locator.ts +35 -0
- package/src/op/gate.ts +88 -6
- package/src/op/local-executor.ts +207 -52
- package/src/op/op-waves-run.ts +4 -0
- package/src/op/run-events.test.ts +166 -0
- package/src/op/run-live.ts +1 -1
- package/src/op/runtime.ts +39 -0
- package/src/op/runtimes/local.test.ts +16 -0
- package/src/op/runtimes/local.ts +13 -2
- package/src/point-decision.ts +12 -2
- package/src/run-event-store.test.ts +144 -0
- package/src/run-event-store.ts +213 -0
- package/src/run-event-stream.test.ts +175 -0
- package/src/run-event-stream.ts +306 -0
- package/src/run-event.test.ts +277 -0
- package/src/run-event.ts +451 -0
- package/src/wave-loop-golden.test.ts +9 -0
- package/src/workspace/__fixtures__/run-event-check.ts +58 -0
- package/src/workspace/ancestry-index.test.ts +93 -0
- package/src/workspace/change-set-show.schema.json +102 -0
- package/src/workspace/change-set-show.ts +72 -0
- package/src/workspace/change-set.schema.json +28 -1
- package/src/workspace/changes-cli.ts +10 -1
- package/src/workspace/changes.schema.json +135 -0
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/changes.ts +84 -2
- package/src/workspace/ci-green-cli.ts +75 -13
- package/src/workspace/ci-green-forge.ts +24 -4
- package/src/workspace/ci-green-runs.test.ts +242 -0
- package/src/workspace/ci-green-signed.test.ts +302 -0
- package/src/workspace/ci-green-workflow.test.ts +16 -0
- package/src/workspace/ci-green-workflow.ts +12 -0
- package/src/workspace/ci-green.test.ts +131 -2
- package/src/workspace/ci-green.ts +624 -42
- package/src/workspace/ci-last-green.schema.json +32 -2
- package/src/workspace/ci-list.schema.json +525 -0
- package/src/workspace/commit-links.test.ts +279 -0
- package/src/workspace/commit-links.ts +183 -0
- package/src/workspace/declaration.schema.json +12 -0
- package/src/workspace/declaration.ts +22 -2
- package/src/workspace/identity-gate-rule.ts +91 -7
- package/src/workspace/identity.ts +14 -1
- package/src/workspace/intent.ts +48 -1
- package/src/workspace/ls.schema.json +3 -1
- package/src/workspace/ls.ts +6 -0
- package/src/workspace/member-pipeline.test.ts +17 -0
- package/src/workspace/member-pipeline.ts +9 -0
- package/src/workspace/promotion-order.ts +97 -0
- package/src/workspace/read-contract.e2e.test.ts +4 -1
- package/src/workspace/reason-codes.test.ts +2 -1
- package/src/workspace/reason-codes.ts +4 -1
- package/src/workspace/releases.schema.json +391 -0
- package/src/workspace/releases.test.ts +108 -0
- package/src/workspace/releases.ts +122 -0
- package/src/workspace/run-event.schema.json +288 -0
- package/src/workspace/status-contract.test.ts +52 -1
- package/src/workspace/status-gates.ts +56 -1
- package/src/workspace/status-stewards.ts +6 -0
- package/src/workspace/status.schema.json +70 -1
- package/src/workspace/status.ts +80 -1
- package/src/workspace/trust/ci-tag-statement.ts +151 -0
- package/src/workspace/trust/op-run-statement.ts +148 -0
- package/src/workspace/trust/rotation.ts +61 -14
- package/src/workspace/trust/seal.ts +10 -0
- package/src/workspace/trust/ssh-commit.ts +10 -9
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiling a release (#4026, ws-104 question 5): what one environment
|
|
3
|
+
* compiles to, and the artifacts built once.
|
|
4
|
+
*
|
|
5
|
+
* A template is synthesized for one environment per invocation: `--env` sets
|
|
6
|
+
* `CHANT_ENV` before the project is imported, and `*.component.ts` files are
|
|
7
|
+
* imported with a plain `import()`, which caches the module. So each
|
|
8
|
+
* environment is compiled in a child process of its own
|
|
9
|
+
* ({@link spawnTargetCompiler}), running `chant release compile --env <env>`,
|
|
10
|
+
* which calls {@link compileEnvironment} in that process and prints the
|
|
11
|
+
* result as JSON. The artifacts are built in one more child, for the first
|
|
12
|
+
* environment in which each component is enabled ({@link buildReleaseArtifacts}).
|
|
13
|
+
*
|
|
14
|
+
* What counts as a template is data, as the build and publish kinds are in
|
|
15
|
+
* ./promote.ts: a literal path in a deploy step's `template`, `manifest` or
|
|
16
|
+
* `values` field ({@link TEMPLATE_FIELDS}), read after the environment's build
|
|
17
|
+
* command has run. A path with the `archive:` prefix names the file after it.
|
|
18
|
+
* A path that names a directory, such as a chart, is not a template.
|
|
19
|
+
*
|
|
20
|
+
* A project with no components (#4046) is compiled the way `chant build --env
|
|
21
|
+
* <env> -o <output>` builds it ({@link compileBuildOutput}): every file that
|
|
22
|
+
* build writes is a template with `component: null`, and the target's params
|
|
23
|
+
* are the build's `buildParams`. The files are kept in memory rather than read
|
|
24
|
+
* back from disk, so a file an earlier build left behind is never recorded.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
28
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
29
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
30
|
+
import { loadChantConfig, type ChantConfig } from "../config";
|
|
31
|
+
import { resolveCliBuildParams } from "../cli/build-params-cli";
|
|
32
|
+
import { discoverComponents } from "./discover";
|
|
33
|
+
import { withoutDisabled } from "./cli-support";
|
|
34
|
+
import { applyConfigDefaults } from "./config-defaults";
|
|
35
|
+
import { fanOutRegistry } from "./fan-out-support";
|
|
36
|
+
import { ARCHIVE_STEP_KINDS, BUILD_STEP_KINDS, PUBLISH_STEP_KINDS } from "./promote";
|
|
37
|
+
import { DriverRunFailure, runInterpretDriver, type DriverComponent, type DriverGate, type DriverPhase, type DriverStep } from "./driver";
|
|
38
|
+
import type { CapabilityRegistry } from "./capability";
|
|
39
|
+
import { memoryGateLedgerPort } from "../op/gate";
|
|
40
|
+
import type { ReleaseArtifact, ReleaseParam } from "../lifecycle/release-store";
|
|
41
|
+
|
|
42
|
+
/** The deploy step fields whose literal file paths are a component's templates. */
|
|
43
|
+
export const TEMPLATE_FIELDS: readonly string[] = ["template", "manifest", "values"];
|
|
44
|
+
|
|
45
|
+
/** The prefix a step may write before an archive-relative template path. */
|
|
46
|
+
const ARCHIVE_PREFIX = "archive:";
|
|
47
|
+
|
|
48
|
+
/** One template a component's deploy reads, as the step names it. */
|
|
49
|
+
export interface TemplateRef {
|
|
50
|
+
component: string;
|
|
51
|
+
phase: string;
|
|
52
|
+
kind: string;
|
|
53
|
+
field: string;
|
|
54
|
+
/** The value as written in the step. */
|
|
55
|
+
value: string;
|
|
56
|
+
/** The file it names, relative to the member's directory as written (the `archive:` prefix removed). */
|
|
57
|
+
path: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A compiled template: its path and its bytes. */
|
|
61
|
+
export interface CompiledTemplate {
|
|
62
|
+
component: string | null;
|
|
63
|
+
path: string;
|
|
64
|
+
content: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What one environment compiles to. */
|
|
68
|
+
export interface CompiledTarget {
|
|
69
|
+
env: string;
|
|
70
|
+
params: ReleaseParam[];
|
|
71
|
+
/** The components enabled in this environment, by name. */
|
|
72
|
+
components: string[];
|
|
73
|
+
/** Each component's build-time steps, as declared for this environment, in composition order. */
|
|
74
|
+
buildSteps: Record<string, unknown[]>;
|
|
75
|
+
/** For each component, the files its build steps read, relative to the member's directory. Used to tell whether an artifact's source changed. */
|
|
76
|
+
buildInputs: Record<string, string[]>;
|
|
77
|
+
/** For each component, the file that declares it, relative to the member's directory. */
|
|
78
|
+
componentFiles: Record<string, string>;
|
|
79
|
+
templates: CompiledTemplate[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Compiles a member for one environment, and builds its artifacts. The cut talks to a compiler only, so a test can hand it fixtures. */
|
|
83
|
+
export interface TargetCompiler {
|
|
84
|
+
compile(env: string): Promise<CompiledTarget>;
|
|
85
|
+
buildArtifacts(env: string, components: readonly string[]): Promise<ReleaseArtifact[]>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
|
|
89
|
+
return (step as { kind?: unknown }).kind === "gate";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
|
|
93
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Every step of a composition with the phase it sits in, nested phases included, in order. */
|
|
97
|
+
function stepsOf(phases: readonly DriverPhase[]): Array<{ phase: string; step: DriverStep }> {
|
|
98
|
+
const out: Array<{ phase: string; step: DriverStep }> = [];
|
|
99
|
+
const walk = (phase: DriverPhase): void => {
|
|
100
|
+
for (const step of phase.steps) {
|
|
101
|
+
if (isPhase(step)) walk(step);
|
|
102
|
+
else if (!isGate(step)) out.push({ phase: phase.phase, step });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
phases.forEach(walk);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isLiteralPath = (v: unknown): v is string =>
|
|
110
|
+
typeof v === "string" && v !== "" && !v.startsWith("@") && !v.startsWith("$");
|
|
111
|
+
|
|
112
|
+
/** The templates a component's deploy reads: literal paths in {@link TEMPLATE_FIELDS} of its steps other than build and publish steps. */
|
|
113
|
+
export function templateRefs(component: DriverComponent): TemplateRef[] {
|
|
114
|
+
const out: TemplateRef[] = [];
|
|
115
|
+
for (const { phase, step } of stepsOf(component.deploy)) {
|
|
116
|
+
if (BUILD_STEP_KINDS.includes(step.kind) || PUBLISH_STEP_KINDS.includes(step.kind)) continue;
|
|
117
|
+
for (const field of TEMPLATE_FIELDS) {
|
|
118
|
+
const raw = step[field];
|
|
119
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
120
|
+
for (const value of values) {
|
|
121
|
+
if (!isLiteralPath(value)) continue;
|
|
122
|
+
const path = value.startsWith(ARCHIVE_PREFIX) ? value.slice(ARCHIVE_PREFIX.length) : value;
|
|
123
|
+
out.push({ component: component.name, phase, kind: step.kind, field, value, path });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A component's build-time steps ({@link BUILD_STEP_KINDS}), each as declared, in composition order. */
|
|
131
|
+
export function buildStepsOf(component: DriverComponent): DriverStep[] {
|
|
132
|
+
return stepsOf(component.deploy).map((s) => s.step).filter((s) => BUILD_STEP_KINDS.includes(s.kind));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The literal paths a component's archive steps read: `docker-build`'s `context` and `dockerfile`, `zip-package`'s `source`, `jvm-build`'s `path`. */
|
|
136
|
+
export function buildInputPaths(component: DriverComponent): string[] {
|
|
137
|
+
const out: string[] = [];
|
|
138
|
+
for (const step of buildStepsOf(component)) {
|
|
139
|
+
if (step.kind === "docker-build") {
|
|
140
|
+
if (isLiteralPath(step.context)) {
|
|
141
|
+
out.push(step.context);
|
|
142
|
+
out.push(isLiteralPath(step.dockerfile) ? `${step.context}/${step.dockerfile}` : `${step.context}/Dockerfile`);
|
|
143
|
+
}
|
|
144
|
+
} else if (step.kind === "zip-package" && isLiteralPath(step.source)) out.push(step.source);
|
|
145
|
+
else if (step.kind === "jvm-build" && isLiteralPath(step.path)) out.push(step.path);
|
|
146
|
+
}
|
|
147
|
+
return [...new Set(out)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A component's composition with only its archive steps ({@link ARCHIVE_STEP_KINDS}) left, gates and every other step taken out. */
|
|
151
|
+
export function withOnlyArchiveSteps(component: DriverComponent): DriverComponent | undefined {
|
|
152
|
+
const keep = (phase: DriverPhase): DriverPhase | undefined => {
|
|
153
|
+
const steps: DriverPhase["steps"] = [];
|
|
154
|
+
for (const step of phase.steps) {
|
|
155
|
+
if (isPhase(step)) {
|
|
156
|
+
const nested = keep(step);
|
|
157
|
+
if (nested) steps.push(nested);
|
|
158
|
+
} else if (!isGate(step) && ARCHIVE_STEP_KINDS.includes(step.kind)) steps.push(step);
|
|
159
|
+
}
|
|
160
|
+
return steps.length > 0 ? { phase: phase.phase, steps } : undefined;
|
|
161
|
+
};
|
|
162
|
+
const deploy = component.deploy.map(keep).filter((p): p is DriverPhase => p !== undefined);
|
|
163
|
+
return deploy.length > 0 ? { name: component.name, dependsOn: [], deploy } : undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** How one environment is compiled: the build command, and the parameter inputs `chant build` takes. */
|
|
167
|
+
export interface CompileInputs {
|
|
168
|
+
/** The member's directory. */
|
|
169
|
+
projectDir: string;
|
|
170
|
+
/** A shell command run in the member's directory before the templates are read, with `CHANT_ENV` set to the environment. */
|
|
171
|
+
buildCommand?: string;
|
|
172
|
+
/** `--param name=value` values, the same for every environment. */
|
|
173
|
+
params?: Record<string, string>;
|
|
174
|
+
/** `--params-file`, with `{env}` replaced by the environment's name. */
|
|
175
|
+
paramsFile?: string;
|
|
176
|
+
/** For a project with no components: the build root, relative to the member's directory. Defaults to `chant.config.ts` `sourceDir`, then the member's directory, as `chant lifecycle` resolves it. */
|
|
177
|
+
src?: string;
|
|
178
|
+
/** For a project with no components: the build's output file, relative to the member's directory, as `chant build -o` takes it. Defaults to {@link DEFAULT_BUILD_OUTPUT}. */
|
|
179
|
+
output?: string;
|
|
180
|
+
/** For a project with no components: build only this lexicon, as `chant build --lexicon` does. */
|
|
181
|
+
lexicon?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Where a project with no components writes its build output when the cut
|
|
186
|
+
* names no `-o`. YAML, since the k8s and helm serializers write YAML whatever
|
|
187
|
+
* the extension, and a JSON output converts to it.
|
|
188
|
+
*/
|
|
189
|
+
export const DEFAULT_BUILD_OUTPUT = "dist/build.yaml";
|
|
190
|
+
|
|
191
|
+
/** Thrown when an environment can't be compiled. */
|
|
192
|
+
export class ReleaseCompileError extends Error {
|
|
193
|
+
constructor(message: string) {
|
|
194
|
+
super(message);
|
|
195
|
+
this.name = "ReleaseCompileError";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function loadComponents(
|
|
200
|
+
inputs: CompileInputs,
|
|
201
|
+
env: string,
|
|
202
|
+
): Promise<{ config: ChantConfig; params: ReleaseParam[]; declared: number; components: Array<{ component: DriverComponent; file: string }> }> {
|
|
203
|
+
const { config } = await loadChantConfig(inputs.projectDir).catch(() => ({ config: {} as ChantConfig }));
|
|
204
|
+
const resolved = resolveCliBuildParams(config.buildParams, {
|
|
205
|
+
...(inputs.params ? { cli: inputs.params } : {}),
|
|
206
|
+
...(inputs.paramsFile ? { paramsFile: inputs.paramsFile.replaceAll("{env}", env) } : {}),
|
|
207
|
+
});
|
|
208
|
+
if (!resolved.success) throw new ReleaseCompileError(`build parameters for "${env}" did not resolve:\n${resolved.errors.join("\n")}`);
|
|
209
|
+
const discovered = await discoverComponents(inputs.projectDir, { buildParams: resolved.provenance });
|
|
210
|
+
if (discovered.errors.length > 0) throw new ReleaseCompileError(discovered.errors.map((e) => e.message).join("\n"));
|
|
211
|
+
const all = [...discovered.components.values()];
|
|
212
|
+
const plain = all.map(({ component }) => ({
|
|
213
|
+
name: component.name,
|
|
214
|
+
dependsOn: component.dependsOn,
|
|
215
|
+
deploy: component.deploy as DriverPhase[],
|
|
216
|
+
...(component.rollback ? { rollback: component.rollback as DriverPhase[] } : {}),
|
|
217
|
+
}));
|
|
218
|
+
const enabled = withoutDisabled(all.map((d) => d.component), plain);
|
|
219
|
+
const fileOf = new Map(all.map((d) => [d.component.name, d.filePath]));
|
|
220
|
+
return {
|
|
221
|
+
config,
|
|
222
|
+
params: resolved.provenance.map(({ name, value, source }) => ({ name, value, source })),
|
|
223
|
+
declared: all.length,
|
|
224
|
+
components: enabled.map((c) => ({ component: applyConfigDefaults(c, config), file: fileOf.get(c.name)! })),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compile the member for `env` in this process: run the build command, resolve
|
|
230
|
+
* the build parameters, discover the components enabled under them, and read
|
|
231
|
+
* the templates their deploys name. Call it once per process, with
|
|
232
|
+
* `CHANT_ENV` already set to `env`, since component modules are cached.
|
|
233
|
+
*/
|
|
234
|
+
export async function compileEnvironment(inputs: CompileInputs, env: string): Promise<CompiledTarget> {
|
|
235
|
+
if (inputs.buildCommand) {
|
|
236
|
+
const run = spawnSync(inputs.buildCommand, {
|
|
237
|
+
cwd: inputs.projectDir,
|
|
238
|
+
shell: true,
|
|
239
|
+
stdio: ["ignore", 2, 2],
|
|
240
|
+
env: { ...process.env, CHANT_ENV: env },
|
|
241
|
+
});
|
|
242
|
+
if (run.status !== 0) throw new ReleaseCompileError(`the build command for "${env}" exited ${run.status ?? "with a signal"}: ${inputs.buildCommand}`);
|
|
243
|
+
}
|
|
244
|
+
const { config, params, declared, components } = await loadComponents(inputs, env);
|
|
245
|
+
if (declared === 0) return compileBuildOutput(inputs, env, config);
|
|
246
|
+
const rel = (p: string): string => (isAbsolute(p) ? relative(inputs.projectDir, p) : p);
|
|
247
|
+
const templates: CompiledTemplate[] = [];
|
|
248
|
+
const seen = new Set<string>();
|
|
249
|
+
for (const { component } of components) {
|
|
250
|
+
for (const ref of templateRefs(component)) {
|
|
251
|
+
const key = `${ref.component}\u0000${ref.path}`;
|
|
252
|
+
if (seen.has(key)) continue;
|
|
253
|
+
seen.add(key);
|
|
254
|
+
const file = resolve(inputs.projectDir, ref.path);
|
|
255
|
+
if (!existsSync(file)) {
|
|
256
|
+
throw new ReleaseCompileError(
|
|
257
|
+
`component "${ref.component}": its ${ref.kind} step in phase "${ref.phase}" reads ${ref.field} "${ref.value}", ` +
|
|
258
|
+
`and no such file exists after the build for "${env}"`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (!statSync(file).isFile()) continue;
|
|
262
|
+
templates.push({ component: ref.component, path: ref.path, content: readFileSync(file, "utf-8") });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
env,
|
|
267
|
+
params,
|
|
268
|
+
components: components.map((c) => c.component.name),
|
|
269
|
+
buildSteps: Object.fromEntries(components.map(({ component }) => [component.name, buildStepsOf(component)])),
|
|
270
|
+
buildInputs: Object.fromEntries(components.map(({ component }) => [component.name, buildInputPaths(component)])),
|
|
271
|
+
componentFiles: Object.fromEntries(components.map(({ component, file }) => [component.name, rel(file)])),
|
|
272
|
+
templates,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Compile a project with no components for `env` (#4046, ws-104 question 5):
|
|
278
|
+
* build it as `chant build <src> --env <env> -o <output>` would, and record
|
|
279
|
+
* each file that build writes as a template with `component: null`, by its
|
|
280
|
+
* path relative to the member's directory. The params are the build's own
|
|
281
|
+
* `buildParams`. Nothing is written to disk.
|
|
282
|
+
*/
|
|
283
|
+
export async function compileBuildOutput(inputs: CompileInputs, env: string, config: ChantConfig): Promise<CompiledTarget> {
|
|
284
|
+
if (!inputs.src && config.stacks && config.stacks.length > 0) {
|
|
285
|
+
throw new ReleaseCompileError(
|
|
286
|
+
`this project declares ${config.stacks.length} stack(s) in chant.config.ts and no component, and a release of a project with no components compiles one build root; ` +
|
|
287
|
+
"name it with --src <dir>",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const projectDir = resolve(inputs.projectDir);
|
|
291
|
+
const root = resolve(projectDir, inputs.src ?? config.sourceDir ?? ".");
|
|
292
|
+
const output = resolve(projectDir, inputs.output ?? DEFAULT_BUILD_OUTPUT);
|
|
293
|
+
const rel = relative(projectDir, output);
|
|
294
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
295
|
+
throw new ReleaseCompileError(`the build output "${inputs.output}" is outside the member's directory; name a file inside it`);
|
|
296
|
+
}
|
|
297
|
+
// Loaded here: a project with components never needs the lexicon plugins in this process.
|
|
298
|
+
const { loadPlugins, resolveProjectLexicons } = await import("../cli/plugins");
|
|
299
|
+
const { buildCommand, printWarnings, resolveBuildFormat } = await import("../cli/commands/build");
|
|
300
|
+
const plugins = await loadPlugins(await resolveProjectLexicons(projectDir, inputs.lexicon ? { lexicon: inputs.lexicon } : {}));
|
|
301
|
+
const serializers = plugins.map((p) => p.serializer).filter((s) => !inputs.lexicon || s.name === inputs.lexicon);
|
|
302
|
+
if (serializers.length === 0) {
|
|
303
|
+
throw new ReleaseCompileError(
|
|
304
|
+
inputs.lexicon ? `no serializer found for lexicon "${inputs.lexicon}"` : "this project has no component and no lexicon, so there is nothing to compile a release from",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const files = new Map<string, string>();
|
|
308
|
+
const result = await buildCommand({
|
|
309
|
+
path: root,
|
|
310
|
+
output,
|
|
311
|
+
format: resolveBuildFormat(undefined, output).format,
|
|
312
|
+
serializers,
|
|
313
|
+
plugins,
|
|
314
|
+
env,
|
|
315
|
+
...(inputs.params ? { params: inputs.params } : {}),
|
|
316
|
+
...(inputs.paramsFile ? { paramsFile: inputs.paramsFile.replaceAll("{env}", env) } : {}),
|
|
317
|
+
writeFile: (path, content) => files.set(path, content),
|
|
318
|
+
});
|
|
319
|
+
printWarnings(result.warnings);
|
|
320
|
+
if (!result.success) {
|
|
321
|
+
throw new ReleaseCompileError(`the build for "${env}" failed:\n${result.errors.join("\n")}`);
|
|
322
|
+
}
|
|
323
|
+
if (result.fileCount === 0 && result.resourceCount === 0) {
|
|
324
|
+
throw new ReleaseCompileError(`no source files were found under ${relative(projectDir, root) || "."}, so there is nothing to compile a release from`);
|
|
325
|
+
}
|
|
326
|
+
const templates: CompiledTemplate[] = [];
|
|
327
|
+
for (const [path, content] of files) {
|
|
328
|
+
const p = relative(projectDir, path);
|
|
329
|
+
if (p.startsWith("..") || isAbsolute(p)) {
|
|
330
|
+
throw new ReleaseCompileError(`the build for "${env}" writes ${path}, outside the member's directory, so the release can't record it`);
|
|
331
|
+
}
|
|
332
|
+
templates.push({ component: null, path: p.split("\\").join("/"), content });
|
|
333
|
+
}
|
|
334
|
+
templates.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
335
|
+
return {
|
|
336
|
+
env,
|
|
337
|
+
params: (result.buildParams ?? []).map(({ name, value, source }) => ({ name, value, source })),
|
|
338
|
+
components: [],
|
|
339
|
+
buildSteps: {},
|
|
340
|
+
buildInputs: {},
|
|
341
|
+
componentFiles: {},
|
|
342
|
+
templates,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Build the artifacts of `names` once, for `env`: run each component's archive
|
|
348
|
+
* steps, and nothing else, through the driver. `docker-build` gives an `image`
|
|
349
|
+
* entry, `zip-package` and `jvm-build` an `asset` entry, each with the path it
|
|
350
|
+
* wrote and its digest.
|
|
351
|
+
*/
|
|
352
|
+
export async function buildReleaseArtifacts(
|
|
353
|
+
inputs: CompileInputs,
|
|
354
|
+
env: string,
|
|
355
|
+
names: readonly string[],
|
|
356
|
+
registry?: CapabilityRegistry,
|
|
357
|
+
): Promise<ReleaseArtifact[]> {
|
|
358
|
+
const { config, components } = await loadComponents(inputs, env);
|
|
359
|
+
const wanted = new Set(names);
|
|
360
|
+
const targets = components
|
|
361
|
+
.filter(({ component }) => wanted.has(component.name))
|
|
362
|
+
.map(({ component }) => withOnlyArchiveSteps(component))
|
|
363
|
+
.filter((c): c is DriverComponent => c !== undefined);
|
|
364
|
+
if (targets.length === 0) return [];
|
|
365
|
+
const reg = registry ?? (await fanOutRegistry(inputs.projectDir, config));
|
|
366
|
+
let run;
|
|
367
|
+
try {
|
|
368
|
+
run = await runInterpretDriver(targets, reg, { env, gates: memoryGateLedgerPort() });
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (err instanceof DriverRunFailure) {
|
|
371
|
+
const failed = err.result.results.find((r) => r.status === "fail");
|
|
372
|
+
const step = failed?.records.find((r) => r.status === "fail");
|
|
373
|
+
throw new ReleaseCompileError(`building the artifacts of "${failed?.component ?? "?"}" failed${step?.error ? `: ${step.error}` : ""}`);
|
|
374
|
+
}
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
const artifacts: ReleaseArtifact[] = [];
|
|
378
|
+
for (const result of run.results) {
|
|
379
|
+
for (const record of result.records) {
|
|
380
|
+
if (record.status !== "ok" || !ARCHIVE_STEP_KINDS.includes(record.kind)) continue;
|
|
381
|
+
const out = record.output as { archivePath?: unknown; digest?: unknown } | undefined;
|
|
382
|
+
if (typeof out?.archivePath !== "string" || typeof out.digest !== "string") {
|
|
383
|
+
throw new ReleaseCompileError(`component "${result.component}": its ${record.kind} step returned no archive path and digest`);
|
|
384
|
+
}
|
|
385
|
+
artifacts.push({ component: result.component, kind: record.kind === "docker-build" ? "image" : "asset", path: out.archivePath, digest: out.digest });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return artifacts;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** How {@link spawnTargetCompiler} starts this chant. */
|
|
392
|
+
export function ownChantCommand(): string[] {
|
|
393
|
+
const main = process.argv[1] ?? "";
|
|
394
|
+
return [process.execPath, ...process.execArgv, main];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function runChild(command: readonly string[], args: readonly string[], cwd: string, env: string): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
398
|
+
return new Promise((resolvePromise, reject) => {
|
|
399
|
+
const [bin, ...lead] = command;
|
|
400
|
+
const child = spawn(bin, [...lead, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CHANT_ENV: env } });
|
|
401
|
+
const out: Buffer[] = [];
|
|
402
|
+
const err: Buffer[] = [];
|
|
403
|
+
child.stdout.on("data", (c: Buffer) => out.push(c));
|
|
404
|
+
child.stderr.on("data", (c: Buffer) => {
|
|
405
|
+
err.push(c);
|
|
406
|
+
process.stderr.write(c);
|
|
407
|
+
});
|
|
408
|
+
child.on("error", reject);
|
|
409
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: Buffer.concat(out).toString("utf-8"), stderr: Buffer.concat(err).toString("utf-8") }));
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The compiler the CLI uses: each environment in a child process of this same
|
|
415
|
+
* chant (`chant release compile --env <env> --json`), with `CHANT_ENV` set to
|
|
416
|
+
* that environment before anything imports the project.
|
|
417
|
+
*/
|
|
418
|
+
export function spawnTargetCompiler(inputs: CompileInputs, command: readonly string[] = ownChantCommand()): TargetCompiler {
|
|
419
|
+
const common = (): string[] => {
|
|
420
|
+
const args: string[] = [];
|
|
421
|
+
if (inputs.src) args.push("--src", inputs.src);
|
|
422
|
+
if (inputs.output) args.push("--output", inputs.output);
|
|
423
|
+
if (inputs.lexicon) args.push("--lexicon", inputs.lexicon);
|
|
424
|
+
for (const [k, v] of Object.entries(inputs.params ?? {})) args.push("--param", `${k}=${v}`);
|
|
425
|
+
if (inputs.paramsFile) args.push("--params-file", inputs.paramsFile);
|
|
426
|
+
return args;
|
|
427
|
+
};
|
|
428
|
+
const call = async (env: string, extra: string[]): Promise<unknown> => {
|
|
429
|
+
const run = await runChild(command, ["release", "compile", "--env", env, ...common(), ...extra, "--json"], inputs.projectDir, env);
|
|
430
|
+
if (run.code !== 0) throw new ReleaseCompileError(`compiling "${env}" failed (exit ${run.code})`);
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(run.stdout);
|
|
433
|
+
} catch {
|
|
434
|
+
throw new ReleaseCompileError(`compiling "${env}" printed something that is not JSON`);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
async compile(env) {
|
|
439
|
+
return (await call(env, inputs.buildCommand ? ["--build", inputs.buildCommand] : [])) as CompiledTarget;
|
|
440
|
+
},
|
|
441
|
+
async buildArtifacts(env, components) {
|
|
442
|
+
return ((await call(env, ["--artifacts", "--component", components.join(",")])) as { artifacts: ReleaseArtifact[] }).artifacts;
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release gate's change set (#4045, ws-104 questions 2 and 6): what a
|
|
3
|
+
* promote under a release plans for an environment, whether it is empty, and
|
|
4
|
+
* the document's binding to the release and the environment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { CapabilityRegistry } from "./capability";
|
|
9
|
+
import type { DriverComponent } from "./driver";
|
|
10
|
+
import {
|
|
11
|
+
chantComponentPart,
|
|
12
|
+
emptyResolution,
|
|
13
|
+
emptyVerifier,
|
|
14
|
+
gateNamesOf,
|
|
15
|
+
planReleaseChangeSet,
|
|
16
|
+
provisionalMembers,
|
|
17
|
+
RELEASE_GATE_OP,
|
|
18
|
+
} from "./release-gate";
|
|
19
|
+
import { composeChangeSet, isEmptyChangeSet, verifyChangeSetDigest, type ChangeSetDocument } from "../change-set";
|
|
20
|
+
import { makeRelease, type Release } from "../lifecycle/release-store";
|
|
21
|
+
import type { ReleaseRecord } from "../lifecycle/release-ledger";
|
|
22
|
+
|
|
23
|
+
const sha = (c: string) => `sha256:${c.repeat(64)}`;
|
|
24
|
+
|
|
25
|
+
function release(prodTemplate: string, opts: { commit?: string; flag?: string } = {}): Release {
|
|
26
|
+
return makeRelease({
|
|
27
|
+
member: null,
|
|
28
|
+
commit: (opts.commit ?? "a").repeat(40),
|
|
29
|
+
artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: sha("1") }],
|
|
30
|
+
targets: [
|
|
31
|
+
{ env: "staging", templates: [{ component: "api", path: "dist/api.json", digest: sha("5") }], params: [{ name: "flag", value: "off", source: "default" }] },
|
|
32
|
+
{ env: "prod", templates: [{ component: "api", path: "dist/api.json", digest: prodTemplate }], params: [{ name: "flag", value: opts.flag ?? "off", source: "default" }] },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const api: DriverComponent = {
|
|
38
|
+
name: "api",
|
|
39
|
+
deploy: [
|
|
40
|
+
{ phase: "Publish", steps: [{ kind: "recorded-digest", digest: sha("1") }] },
|
|
41
|
+
{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.json" }] },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const record = (over: Partial<ReleaseRecord> = {}): ReleaseRecord => ({
|
|
46
|
+
version: 1,
|
|
47
|
+
component: "api",
|
|
48
|
+
env: "prod",
|
|
49
|
+
digest: "sha256:published",
|
|
50
|
+
gitSha: "a".repeat(40),
|
|
51
|
+
runId: "run-1",
|
|
52
|
+
timestamp: "2026-10-01T00:00:00.000Z",
|
|
53
|
+
actor: "ci",
|
|
54
|
+
...over,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const registry = () => {
|
|
58
|
+
const r = new CapabilityRegistry();
|
|
59
|
+
r.register({ kind: "recorded-digest", async run() { return {}; } } as never);
|
|
60
|
+
r.register({ kind: "apply", async run() { return {}; } } as never);
|
|
61
|
+
return r;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe("gateNamesOf", () => {
|
|
65
|
+
test("lists each gate a composition declares once, nested phases included", () => {
|
|
66
|
+
const web: DriverComponent = { name: "web", deploy: [{ phase: "A", steps: [{ phase: "B", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "gate", gate: "dba" }] }] }] };
|
|
67
|
+
expect(gateNamesOf([api, web])).toEqual(["dba", "prod-release"]);
|
|
68
|
+
expect(gateNamesOf([{ name: "x", deploy: [{ phase: "A", steps: [{ kind: "apply" }] }] }])).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("chantComponentPart", () => {
|
|
73
|
+
const r1 = release(sha("7"));
|
|
74
|
+
|
|
75
|
+
test("a component the environment already runs at the same artifact, templates and parameters has no entries", () => {
|
|
76
|
+
const part = chantComponentPart({ component: api, release: r1, env: "prod", digest: "sha256:published", current: { record: record({ releaseDigest: r1.releaseDigest }), release: r1 } });
|
|
77
|
+
expect(part.entries).toEqual([]);
|
|
78
|
+
expect(part.member.holes).toEqual([]);
|
|
79
|
+
expect(isEmptyChangeSet(composeChangeSet([part]))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a changed template, artifact or parameter is an update naming each one", () => {
|
|
83
|
+
const r2 = release(sha("8"), { commit: "b", flag: "on" });
|
|
84
|
+
const part = chantComponentPart({ component: api, release: r2, env: "prod", digest: "sha256:rebuilt", current: { record: record({ releaseDigest: r1.releaseDigest }), release: r1 } });
|
|
85
|
+
expect(part.entries).toHaveLength(1);
|
|
86
|
+
expect(part.entries[0]).toMatchObject({ address: "api", action: "update", type: "chant:component", scope: "prod" });
|
|
87
|
+
expect(part.entries[0].attributes).toEqual([
|
|
88
|
+
{ path: "artifact", before: "sha256:published", after: "sha256:rebuilt" },
|
|
89
|
+
{ path: "template:dist/api.json", before: sha("7"), after: sha("8") },
|
|
90
|
+
{ path: "param:flag", before: '"off"', after: '"on"' },
|
|
91
|
+
]);
|
|
92
|
+
expect(isEmptyChangeSet(composeChangeSet([part]))).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a prod-only flag flip leaves staging's change set empty", () => {
|
|
96
|
+
const flipped = release(sha("7"), { commit: "b", flag: "on" });
|
|
97
|
+
const staging = chantComponentPart({
|
|
98
|
+
component: api,
|
|
99
|
+
release: flipped,
|
|
100
|
+
env: "staging",
|
|
101
|
+
digest: "sha256:published",
|
|
102
|
+
current: { record: record({ env: "staging", releaseDigest: r1.releaseDigest }), release: r1 },
|
|
103
|
+
});
|
|
104
|
+
expect(staging.entries).toEqual([]);
|
|
105
|
+
const prod = chantComponentPart({ component: api, release: flipped, env: "prod", digest: "sha256:published", current: { record: record({ releaseDigest: r1.releaseDigest }), release: r1 } });
|
|
106
|
+
expect(prod.entries[0].attributes.map((a) => a.path)).toEqual(["param:flag"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a component never deployed to the environment is a create", () => {
|
|
110
|
+
const part = chantComponentPart({ component: api, release: r1, env: "prod", digest: "sha256:published", current: undefined });
|
|
111
|
+
expect(part.entries[0]).toMatchObject({ action: "create" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a deploy that names no release, or one that can't be read, is a hole", () => {
|
|
115
|
+
const none = chantComponentPart({ component: api, release: r1, env: "prod", digest: "sha256:published", current: { record: record(), release: null } });
|
|
116
|
+
expect(none.member.holes[0].reason).toMatch(/names no release/);
|
|
117
|
+
const unread = chantComponentPart({ component: api, release: r1, env: "prod", digest: "sha256:published", current: { record: record({ releaseDigest: sha("9") }), release: null } });
|
|
118
|
+
expect(unread.member.holes[0].reason).toMatch(/can't be read/);
|
|
119
|
+
expect(isEmptyChangeSet(composeChangeSet([none]))).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("planReleaseChangeSet", () => {
|
|
124
|
+
const r1 = release(sha("7"));
|
|
125
|
+
const current = new Map([["api", { record: record({ releaseDigest: r1.releaseDigest }), release: r1 }]]);
|
|
126
|
+
const plan = (rel: Release, env: string) =>
|
|
127
|
+
planReleaseChangeSet({ release: rel, env, components: [api], digests: new Map([["api", "sha256:published"]]), registry: registry(), current });
|
|
128
|
+
|
|
129
|
+
test("the document names the release and the environment it was planned for, and both are in its digest", async () => {
|
|
130
|
+
const doc = await plan(r1, "prod");
|
|
131
|
+
expect(doc.releaseDigest).toBe(r1.releaseDigest);
|
|
132
|
+
expect(doc.environment).toBe("prod");
|
|
133
|
+
expect(verifyChangeSetDigest(doc)).toBe(true);
|
|
134
|
+
expect(verifyChangeSetDigest({ ...doc, environment: "staging" })).toBe(false);
|
|
135
|
+
expect(verifyChangeSetDigest({ ...doc, releaseDigest: sha("9") })).toBe(false);
|
|
136
|
+
const other = release(sha("7"), { commit: "c" });
|
|
137
|
+
expect((await plan(other, "prod")).digest).not.toBe(doc.digest);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a document with neither field hashes as it always has", () => {
|
|
141
|
+
const doc = composeChangeSet([]);
|
|
142
|
+
expect(doc.releaseDigest).toBeUndefined();
|
|
143
|
+
expect(verifyChangeSetDigest(doc)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a capability that plans gives the member; one that returns no part is a hole", async () => {
|
|
147
|
+
const reg = registry();
|
|
148
|
+
reg.register({ kind: "tf", async run() { return {}; }, async plan() { return { member: "estate", planDigest: sha("3") }; } } as never);
|
|
149
|
+
const tf: DriverComponent = { name: "infra", deploy: [{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "tf" }] }] };
|
|
150
|
+
const doc = await planReleaseChangeSet({ release: r1, env: "prod", components: [tf], digests: new Map(), registry: reg, current: new Map() });
|
|
151
|
+
expect(doc.members.map((m) => m.member)).toEqual(["estate"]);
|
|
152
|
+
expect(doc.members[0].holes[0].reason).toMatch(/describes no changes/);
|
|
153
|
+
expect(isEmptyChangeSet(doc)).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a plan that throws is a failed member", async () => {
|
|
157
|
+
const reg = registry();
|
|
158
|
+
reg.register({ kind: "tf", async run() { return {}; }, async plan() { throw new Error("no credentials"); } } as never);
|
|
159
|
+
const tf: DriverComponent = { name: "infra", deploy: [{ phase: "Apply", steps: [{ kind: "tf" }] }] };
|
|
160
|
+
const doc = await planReleaseChangeSet({ release: r1, env: "prod", components: [tf], digests: new Map(), registry: reg, current: new Map() });
|
|
161
|
+
expect(doc.members[0]).toMatchObject({ member: "infra", status: "failed", error: "no credentials" });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("empty change sets", () => {
|
|
166
|
+
const r1 = release(sha("7"));
|
|
167
|
+
const emptyDoc = (): ChangeSetDocument =>
|
|
168
|
+
composeChangeSet(
|
|
169
|
+
[chantComponentPart({ component: api, release: r1, env: "prod", digest: "sha256:published", current: { record: record({ releaseDigest: r1.releaseDigest }), release: r1 } })],
|
|
170
|
+
{ planned: { releaseDigest: r1.releaseDigest, environment: "prod" } },
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
test("isEmptyChangeSet needs planned members, no holes and only no-op or read entries", () => {
|
|
174
|
+
const doc = emptyDoc();
|
|
175
|
+
expect(isEmptyChangeSet(doc)).toBe(true);
|
|
176
|
+
expect(isEmptyChangeSet({ ...doc, members: [] })).toBe(false);
|
|
177
|
+
expect(isEmptyChangeSet({ ...doc, members: [{ ...doc.members[0], provisional: true }] })).toBe(false);
|
|
178
|
+
expect(isEmptyChangeSet({ ...doc, members: [{ ...doc.members[0], status: "failed" }] })).toBe(false);
|
|
179
|
+
const entry = { member: "api", lexicon: "chant", planner: "chant" as const, address: "x", type: "t", attributes: [] };
|
|
180
|
+
expect(isEmptyChangeSet({ ...doc, entries: [{ ...entry, action: "no-op" }, { ...entry, action: "read" }] })).toBe(true);
|
|
181
|
+
expect(isEmptyChangeSet({ ...doc, entries: [{ ...entry, action: "update" }] })).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("provisionalMembers names each provisional member", () => {
|
|
185
|
+
const doc = emptyDoc();
|
|
186
|
+
expect(provisionalMembers(doc)).toEqual([]);
|
|
187
|
+
expect(provisionalMembers({ ...doc, members: [{ ...doc.members[0], provisional: true }] })).toEqual(["api"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("emptyVerifier reads the stored document back: found, for this release and environment, and empty", async () => {
|
|
191
|
+
const doc = emptyDoc();
|
|
192
|
+
const planned = { releaseDigest: r1.releaseDigest, environment: "prod" };
|
|
193
|
+
expect(await emptyVerifier(async () => ({ status: "found", document: doc }), planned)(doc.digest)).toBe(true);
|
|
194
|
+
expect(await emptyVerifier(async () => ({ status: "missing" }), planned)(doc.digest)).toBe(false);
|
|
195
|
+
expect(await emptyVerifier(async () => ({ status: "found", document: doc }), { ...planned, environment: "staging" })(doc.digest)).toBe(false);
|
|
196
|
+
const changed = { ...doc, entries: [{ member: "api", lexicon: "chant", planner: "chant" as const, address: "api", type: "t", action: "update" as const, attributes: [] }] };
|
|
197
|
+
expect(await emptyVerifier(async () => ({ status: "found", document: changed }), planned)(doc.digest)).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("emptyResolution is an `empty` pass by the actor, bound to the document, the release and the environment", () => {
|
|
201
|
+
const doc = emptyDoc();
|
|
202
|
+
expect(emptyResolution({ gate: "prod-release", doc, actor: "bob", timestamp: "2026-10-16T00:00:00.000Z" })).toEqual({
|
|
203
|
+
op: RELEASE_GATE_OP,
|
|
204
|
+
gate: "prod-release",
|
|
205
|
+
resolvedBy: "bob",
|
|
206
|
+
timestamp: "2026-10-16T00:00:00.000Z",
|
|
207
|
+
note: `the change set of release ${r1.releaseDigest} for "prod" is empty`,
|
|
208
|
+
planDigest: doc.digest,
|
|
209
|
+
environment: "prod",
|
|
210
|
+
releaseDigest: r1.releaseDigest,
|
|
211
|
+
empty: true,
|
|
212
|
+
});
|
|
213
|
+
expect(() => emptyResolution({ gate: "g", doc: composeChangeSet([]), actor: "bob", timestamp: "t" })).toThrow(/names no release/);
|
|
214
|
+
});
|
|
215
|
+
});
|