@intentius/chant 0.124.27 → 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/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 +8 -0
- 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 +204 -199
- 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/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/commit-run-gate-deploy-join.test.ts +117 -0
- 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 +9 -0
- 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
|
@@ -9,12 +9,19 @@ import { CapabilityRegistry, type DeployContext } from "../../components/capabil
|
|
|
9
9
|
import type { DriverComponent } from "../../components/driver";
|
|
10
10
|
import type { ReleaseRecord } from "../../lifecycle/release-ledger";
|
|
11
11
|
import type { ParsedArgs } from "../registry";
|
|
12
|
+
import { makeRelease } from "../../lifecycle/release-store";
|
|
13
|
+
import { contentDigest } from "../../content-digest";
|
|
12
14
|
|
|
13
15
|
const appendReleaseRecordMock = vi.fn();
|
|
14
16
|
const readReleaseLedgerMock = vi.fn();
|
|
15
17
|
const pushLifecycleMock = vi.fn();
|
|
16
18
|
const resolveTargetsMock = vi.fn();
|
|
17
19
|
const ran: string[] = [];
|
|
20
|
+
const stepInputs: Array<{ kind: string; input: unknown }> = [];
|
|
21
|
+
let releaseFixture: { release: import("../../lifecycle/release-store").Release; label: string | null } | null = null;
|
|
22
|
+
let storedTemplate: string | null = null;
|
|
23
|
+
const writtenTemplates: unknown[] = [];
|
|
24
|
+
let configEnvironments: unknown[] | undefined;
|
|
18
25
|
let publishedDigest = "sha256:api";
|
|
19
26
|
let failApplyFor: string | undefined;
|
|
20
27
|
|
|
@@ -35,7 +42,7 @@ vi.mock("../../lifecycle/release-ledger", async () => {
|
|
|
35
42
|
|
|
36
43
|
vi.mock("../../config", async () => {
|
|
37
44
|
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
38
|
-
return { ...actual, loadChantConfig: async () => ({ config: {} }) };
|
|
45
|
+
return { ...actual, loadChantConfig: async () => ({ config: configEnvironments ? { environments: configEnvironments } : {} }) };
|
|
39
46
|
});
|
|
40
47
|
|
|
41
48
|
vi.mock("../../components/cli-support", () => ({
|
|
@@ -47,13 +54,25 @@ vi.mock("../../components/fan-out-support", () => ({
|
|
|
47
54
|
fanOutRegistry: async () => {
|
|
48
55
|
const registry = new CapabilityRegistry();
|
|
49
56
|
const step = (kind: string, output: (ctx: DeployContext) => unknown) =>
|
|
50
|
-
registry.register({ kind, async run(ctx: DeployContext) { ran.push(`${ctx.env}:${kind}`); return output(ctx); } } as never);
|
|
57
|
+
registry.register({ kind, async run(ctx: DeployContext, input: unknown) { ran.push(`${ctx.env}:${kind}`); stepInputs.push({ kind, input }); return output(ctx); } } as never);
|
|
51
58
|
step("docker-build", () => ({ digest: "sha256:rebuilt" }));
|
|
52
59
|
step("publish-image", () => ({ digest: publishedDigest, uri: `reg/api@${publishedDigest}` }));
|
|
53
60
|
step("apply", (ctx) => {
|
|
54
61
|
if (ctx.component === failApplyFor) throw new Error("apply failed");
|
|
55
62
|
return {};
|
|
56
63
|
});
|
|
64
|
+
// #4045: a planner whose member is provisional.
|
|
65
|
+
registry.register({
|
|
66
|
+
kind: "plan-provisional",
|
|
67
|
+
async run() { return {}; },
|
|
68
|
+
async plan() {
|
|
69
|
+
return {
|
|
70
|
+
member: "estate",
|
|
71
|
+
planDigest: "sha256:" + "3".repeat(64),
|
|
72
|
+
changeSet: { member: { member: "estate", lexicon: "terraform", planner: "terraform", status: "planned", planDigest: "sha256:" + "3".repeat(64), provisional: true, holes: [] }, entries: [] },
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
} as never);
|
|
57
76
|
return registry;
|
|
58
77
|
},
|
|
59
78
|
}));
|
|
@@ -62,14 +81,60 @@ vi.mock("../../components/fan-out-support", () => ({
|
|
|
62
81
|
vi.mock("../../op/gate", async () => {
|
|
63
82
|
const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
|
|
64
83
|
const port = actual.memoryGateLedgerPort();
|
|
65
|
-
return { ...actual, gitGateLedgerPort: () => port, gateIsSealed: async (gate: string) => sealedGates.has(gate) };
|
|
84
|
+
return { ...actual, gitGateLedgerPort: () => releaseGatePort ?? port, gateIsSealed: async (gate: string) => sealedGates.has(gate) };
|
|
66
85
|
});
|
|
67
86
|
const sealedGates = new Set<string>();
|
|
87
|
+
/** #4045: a fresh ledger per release gate test, so their approvals don't carry between tests. */
|
|
88
|
+
let releaseGatePort: ReturnType<typeof import("../../op/gate").memoryGateLedgerPort> | null = null;
|
|
89
|
+
|
|
90
|
+
// #4045: stored change sets live in memory, keyed by digest.
|
|
91
|
+
const storedChangeSets = new Map<string, import("../../change-set").ChangeSetDocument>();
|
|
92
|
+
let storeFails = false;
|
|
93
|
+
vi.mock("../../lifecycle/gate-change-set", () => ({
|
|
94
|
+
storeGateChangeSet: async ({ doc }: { doc: import("../../change-set").ChangeSetDocument }) => {
|
|
95
|
+
if (storeFails) throw new Error("the branch is read-only");
|
|
96
|
+
const written = !storedChangeSets.has(doc.digest);
|
|
97
|
+
storedChangeSets.set(doc.digest, doc);
|
|
98
|
+
return { digest: doc.digest, written };
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
vi.mock("../../lifecycle/change-set-store", async () => {
|
|
102
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/change-set-store")>("../../lifecycle/change-set-store");
|
|
103
|
+
return {
|
|
104
|
+
...actual,
|
|
105
|
+
readStoredChangeSet: async (digest: string) => {
|
|
106
|
+
const doc = storedChangeSets.get(digest);
|
|
107
|
+
return doc ? actual.parseStoredChangeSet(JSON.stringify(doc), digest) : { status: "missing" };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const knownReleases = new Map<string, import("../../lifecycle/release-store").Release>();
|
|
112
|
+
const removedTemplates: unknown[] = [];
|
|
113
|
+
|
|
114
|
+
// #4026: the release store holds one release and one template's bytes.
|
|
115
|
+
vi.mock("../../lifecycle/release-store", async () => {
|
|
116
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/release-store")>("../../lifecycle/release-store");
|
|
117
|
+
return {
|
|
118
|
+
...actual,
|
|
119
|
+
resolveRelease: async (ref: string) => (releaseFixture && (ref === releaseFixture.label || ref === releaseFixture.release.releaseDigest) ? releaseFixture : null),
|
|
120
|
+
readReleaseTemplate: async () => storedTemplate,
|
|
121
|
+
readRelease: async (digest: string) => knownReleases.get(digest) ?? null,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
vi.mock("../../components/release-promote", async () => {
|
|
125
|
+
const actual = await vi.importActual<typeof import("../../components/release-promote")>("../../components/release-promote");
|
|
126
|
+
return {
|
|
127
|
+
...actual,
|
|
128
|
+
writePinnedTemplates: (_dir: string, files: unknown[]) => writtenTemplates.push(...files),
|
|
129
|
+
removePinnedTemplates: (_dir: string, files: unknown[]) => removedTemplates.push(...files),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
68
132
|
|
|
69
133
|
// The run handler pulls in every runtime; the promote needs only its exit code.
|
|
70
134
|
vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
|
|
71
135
|
|
|
72
136
|
const { runComponentsPromote, runComponentsRollback, runComponentsRedeploy } = await import("./promote");
|
|
137
|
+
const { checkRunEventLines, stepTrail } = await import("../../workspace/__fixtures__/run-event-check");
|
|
73
138
|
|
|
74
139
|
const api: DriverComponent = {
|
|
75
140
|
name: "api",
|
|
@@ -116,7 +181,17 @@ let logs: string[];
|
|
|
116
181
|
|
|
117
182
|
beforeEach(() => {
|
|
118
183
|
sealedGates.clear();
|
|
184
|
+
releaseGatePort = null;
|
|
185
|
+
storedChangeSets.clear();
|
|
186
|
+
storeFails = false;
|
|
187
|
+
knownReleases.clear();
|
|
188
|
+
removedTemplates.length = 0;
|
|
119
189
|
ran.length = 0;
|
|
190
|
+
stepInputs.length = 0;
|
|
191
|
+
writtenTemplates.length = 0;
|
|
192
|
+
releaseFixture = null;
|
|
193
|
+
storedTemplate = null;
|
|
194
|
+
configEnvironments = undefined;
|
|
120
195
|
publishedDigest = "sha256:api";
|
|
121
196
|
failApplyFor = undefined;
|
|
122
197
|
errors = [];
|
|
@@ -135,9 +210,52 @@ afterEach(() => {
|
|
|
135
210
|
});
|
|
136
211
|
|
|
137
212
|
describe("chant components promote", () => {
|
|
138
|
-
test("--
|
|
213
|
+
test("--to is required", async () => {
|
|
139
214
|
expect(await runComponentsPromote(ctx({ migrateTo: undefined }))).toBe(1);
|
|
140
|
-
expect(errors.join("\n")).toMatch(/--
|
|
215
|
+
expect(errors.join("\n")).toMatch(/--to <env> is required/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("--from is required when the member declares no environments, and nothing is refused", async () => {
|
|
219
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: undefined }))).toBe(1);
|
|
220
|
+
expect(errors.join("\n")).toMatch(/--from <env> is required: this member declares no environments/);
|
|
221
|
+
errors.length = 0;
|
|
222
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: "dev", migrateTo: "prod" }))).toBe(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("the declared order defaults --from to the previous environment (#4030)", async () => {
|
|
226
|
+
configEnvironments = ["dev", { name: "staging", canary: true }, "prod"];
|
|
227
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: undefined }))).toBe(0);
|
|
228
|
+
expect(readReleaseLedgerMock).toHaveBeenCalledWith("staging");
|
|
229
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ env: "prod", promotedFrom: { env: "staging" } });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a promotion that skips an environment is refused with a code and a fix, and deploys nothing (#4030)", async () => {
|
|
233
|
+
configEnvironments = ["dev", "staging", "prod"];
|
|
234
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: "dev", migrateTo: "prod" }))).toBe(1);
|
|
235
|
+
const out = errors.join("\n");
|
|
236
|
+
expect(out).toMatch(/promoting dev to prod skips staging/);
|
|
237
|
+
expect(out).toMatch(/\[promote-skips-environment\]/);
|
|
238
|
+
expect(out).toMatch(/--allow-skip/);
|
|
239
|
+
expect(ran).toEqual([]);
|
|
240
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("--allow-skip promotes past a declared environment (#4030)", async () => {
|
|
244
|
+
configEnvironments = ["dev", "staging", "prod"];
|
|
245
|
+
readReleaseLedgerMock.mockResolvedValue({ records: [{ ...staging, env: "dev" }], malformed: 0 });
|
|
246
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: "dev", migrateTo: "prod", allowSkip: true }))).toBe(0);
|
|
247
|
+
expect(readReleaseLedgerMock).toHaveBeenCalledWith("dev");
|
|
248
|
+
expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("the first environment of the order has nothing to default --from to (#4030)", async () => {
|
|
252
|
+
configEnvironments = ["dev", "staging", "prod"];
|
|
253
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: undefined, migrateTo: "dev" }))).toBe(1);
|
|
254
|
+
expect(errors.join("\n")).toMatch(/\[promote-no-previous-environment\]/);
|
|
255
|
+
errors.length = 0;
|
|
256
|
+
expect(await runComponentsPromote(ctx({ migrateFrom: undefined, migrateTo: "qa" }))).toBe(1);
|
|
257
|
+
expect(errors.join("\n")).toMatch(/\[promote-to-not-in-order\]/);
|
|
258
|
+
expect(ran).toEqual([]);
|
|
141
259
|
});
|
|
142
260
|
|
|
143
261
|
test("deploys the staging digest to prod without building, and records the promotion", async () => {
|
|
@@ -255,6 +373,203 @@ describe("chant components promote", () => {
|
|
|
255
373
|
});
|
|
256
374
|
});
|
|
257
375
|
|
|
376
|
+
describe("chant components promote --release (#4026)", () => {
|
|
377
|
+
const prodTemplate = `{"env":"prod"}\n`;
|
|
378
|
+
const withTemplate: DriverComponent = {
|
|
379
|
+
...api,
|
|
380
|
+
deploy: [...api.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] }],
|
|
381
|
+
};
|
|
382
|
+
const release = makeRelease({
|
|
383
|
+
member: null,
|
|
384
|
+
commit: "abc123".padEnd(40, "0"),
|
|
385
|
+
artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:api" }],
|
|
386
|
+
targets: [
|
|
387
|
+
{ env: "staging", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"staging"}\n`) }], params: [] },
|
|
388
|
+
{ env: "prod", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(prodTemplate) }], params: [] },
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
beforeEach(() => {
|
|
393
|
+
resolveTargetsMock.mockResolvedValue({ success: true, targets: [withTemplate] });
|
|
394
|
+
releaseFixture = { release, label: "v1.0.0" };
|
|
395
|
+
storedTemplate = prodTemplate;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("deploys the template the release recorded for --to, and names the release on the record", async () => {
|
|
399
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
400
|
+
expect(ran).toEqual(["prod:publish-image", "prod:apply"]);
|
|
401
|
+
const prodPath = `.chant/release-templates/${release.targets.find((t) => t.env === "prod")!.templates[0].digest.replace(/:/g, "_")}/api.template.json`;
|
|
402
|
+
expect(stepInputs.find((s) => s.kind === "apply")?.input).toMatchObject({ template: prodPath });
|
|
403
|
+
expect(writtenTemplates).toEqual([{ path: prodPath, content: prodTemplate }]);
|
|
404
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ env: "prod", digest: "sha256:api", releaseDigest: release.releaseDigest });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("refuses a template whose digest differs from the target's, and deploys nothing", async () => {
|
|
408
|
+
storedTemplate = `{"env":"prod","edited":true}\n`;
|
|
409
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(1);
|
|
410
|
+
expect(errors.join("\n")).toMatch(/hashes to sha256:[0-9a-f]{64}, but the release's target for "prod" records/);
|
|
411
|
+
expect(errors.join("\n")).toMatch(/nothing was deployed/);
|
|
412
|
+
expect(ran).toEqual([]);
|
|
413
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("refuses a release it can't find, and one with no target for --to", async () => {
|
|
417
|
+
expect(await runComponentsPromote(ctx({ release: "v9" }))).toBe(1);
|
|
418
|
+
expect(errors.join("\n")).toMatch(/no release is labelled or digested "v9"/);
|
|
419
|
+
errors.length = 0;
|
|
420
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0", migrateTo: "qa" }))).toBe(1);
|
|
421
|
+
expect(errors.join("\n")).toMatch(/has no target for "qa"/);
|
|
422
|
+
expect(ran).toEqual([]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("a promote without --release reads the checkout's template as before", async () => {
|
|
426
|
+
expect(await runComponentsPromote(ctx({}))).toBe(0);
|
|
427
|
+
expect(stepInputs.find((s) => s.kind === "apply")?.input).toMatchObject({ template: "dist/api.template.json" });
|
|
428
|
+
expect(appendReleaseRecordMock.mock.calls[0][0].releaseDigest).toBeUndefined();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("chant components promote --release binds its gate (#4045)", () => {
|
|
433
|
+
const prodTemplate = `{"env":"prod"}\n`;
|
|
434
|
+
const stagingTemplate = `{"env":"staging"}\n`;
|
|
435
|
+
const gated: DriverComponent = {
|
|
436
|
+
...api,
|
|
437
|
+
deploy: [
|
|
438
|
+
...api.deploy.slice(0, 2),
|
|
439
|
+
{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
const releaseAt = (commit: string) =>
|
|
443
|
+
makeRelease({
|
|
444
|
+
member: null,
|
|
445
|
+
commit: commit.padEnd(40, "0"),
|
|
446
|
+
artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:api" }],
|
|
447
|
+
targets: [
|
|
448
|
+
{ env: "staging", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(stagingTemplate) }], params: [] },
|
|
449
|
+
{ env: "prod", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(prodTemplate) }], params: [] },
|
|
450
|
+
],
|
|
451
|
+
});
|
|
452
|
+
const r1 = releaseAt("abc123");
|
|
453
|
+
const r2 = releaseAt("def456");
|
|
454
|
+
const dev: ReleaseRecord = { ...staging, env: "dev" };
|
|
455
|
+
let ledgers: Record<string, ReleaseRecord[]>;
|
|
456
|
+
|
|
457
|
+
beforeEach(async () => {
|
|
458
|
+
const { memoryGateLedgerPort } = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
|
|
459
|
+
releaseGatePort = memoryGateLedgerPort();
|
|
460
|
+
resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
|
|
461
|
+
releaseFixture = { release: r1, label: "v1.0.0" };
|
|
462
|
+
storedTemplate = prodTemplate;
|
|
463
|
+
knownReleases.set(r1.releaseDigest, r1);
|
|
464
|
+
knownReleases.set(r2.releaseDigest, r2);
|
|
465
|
+
ledgers = { dev: [dev], staging: [staging], prod: [] };
|
|
466
|
+
readReleaseLedgerMock.mockImplementation(async (env: string) => ({ records: ledgers[env] ?? [], malformed: 0 }));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const onlyStored = () => [...storedChangeSets.values()];
|
|
470
|
+
|
|
471
|
+
test("a non-empty change set stops at a release gate that binds the release, the environment and the stored document", async () => {
|
|
472
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
473
|
+
expect(ran).not.toContain("prod:apply");
|
|
474
|
+
const [doc] = onlyStored();
|
|
475
|
+
expect(doc).toMatchObject({ releaseDigest: r1.releaseDigest, environment: "prod" });
|
|
476
|
+
expect(doc.entries[0]).toMatchObject({ address: "api", action: "create" });
|
|
477
|
+
expect(releaseGatePort!.appended).toEqual([
|
|
478
|
+
expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", planDigest: doc.digest, releaseDigest: r1.releaseDigest }),
|
|
479
|
+
]);
|
|
480
|
+
expect(errors.join("\n")).toMatch(/chant approve release prod-release --env prod/);
|
|
481
|
+
expect(errors.join("\n")).toMatch(/release gate: change set jcs1-sha256:[0-9a-f]{64} for "prod", 1 entry/);
|
|
482
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
483
|
+
expect(removedTemplates).toHaveLength(1);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("an approval of that release passes the gate, and the deploy record names the release and the gate's resolution", async () => {
|
|
487
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
488
|
+
const pending = releaseGatePort!.appended[0];
|
|
489
|
+
await releaseGatePort!.appendResolution!({
|
|
490
|
+
op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
|
|
491
|
+
planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
|
|
492
|
+
});
|
|
493
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
494
|
+
expect(ran).toContain("prod:apply");
|
|
495
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({
|
|
496
|
+
releaseDigest: r1.releaseDigest,
|
|
497
|
+
approver: "alice",
|
|
498
|
+
admission: { op: "release", gate: "prod-release" },
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("an approval for one release can't be used by another", async () => {
|
|
503
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
504
|
+
const pending = releaseGatePort!.appended[0];
|
|
505
|
+
await releaseGatePort!.appendResolution!({
|
|
506
|
+
op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
|
|
507
|
+
planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
|
|
508
|
+
});
|
|
509
|
+
releaseFixture = { release: r2, label: "v1.0.1" };
|
|
510
|
+
ran.length = 0;
|
|
511
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.1" }))).toBe(3);
|
|
512
|
+
expect(ran).not.toContain("prod:apply");
|
|
513
|
+
expect(releaseGatePort!.appended.at(-1)).toMatchObject({ releaseDigest: r2.releaseDigest });
|
|
514
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("an approval for one environment can't be used for another", async () => {
|
|
518
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
519
|
+
const pending = releaseGatePort!.appended[0];
|
|
520
|
+
await releaseGatePort!.appendResolution!({
|
|
521
|
+
op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
|
|
522
|
+
planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
|
|
523
|
+
});
|
|
524
|
+
storedTemplate = stagingTemplate;
|
|
525
|
+
ran.length = 0;
|
|
526
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0", migrateFrom: "dev", migrateTo: "staging" }))).toBe(3);
|
|
527
|
+
expect(ran).not.toContain("staging:apply");
|
|
528
|
+
expect(releaseGatePort!.appended.at(-1)).toMatchObject({ environment: "staging", releaseDigest: r1.releaseDigest });
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("an empty change set passes without an approval: the promote writes an empty resolution and a deploy record naming the release", async () => {
|
|
532
|
+
ledgers.prod = [{ ...staging, env: "prod", releaseDigest: r1.releaseDigest, timestamp: "2026-01-03T00:00:00.000Z" }];
|
|
533
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
534
|
+
expect(ran).toContain("prod:apply");
|
|
535
|
+
const [doc] = onlyStored();
|
|
536
|
+
expect(doc.entries).toEqual([]);
|
|
537
|
+
expect(releaseGatePort!.appended).toEqual([]);
|
|
538
|
+
expect(releaseGatePort!.resolved).toEqual([
|
|
539
|
+
expect.objectContaining({ op: "release", gate: "prod-release", resolvedBy: "bob", empty: true, planDigest: doc.digest, environment: "prod", releaseDigest: r1.releaseDigest }),
|
|
540
|
+
]);
|
|
541
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ env: "prod", releaseDigest: r1.releaseDigest, approver: "bob", admission: { op: "release", gate: "prod-release" } });
|
|
542
|
+
expect(errors.join("\n")).toMatch(/empty: its gates pass without an approval/);
|
|
543
|
+
// Run again: the pass stands, and no second line is written.
|
|
544
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
545
|
+
expect(releaseGatePort!.resolved).toHaveLength(1);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("an empty pass counts only once the stored document reads back empty", async () => {
|
|
549
|
+
ledgers.prod = [{ ...staging, env: "prod", releaseDigest: r1.releaseDigest, timestamp: "2026-01-03T00:00:00.000Z" }];
|
|
550
|
+
storeFails = true;
|
|
551
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
|
|
552
|
+
expect(errors.join("\n")).toMatch(/could not store the change set/);
|
|
553
|
+
expect(ran).not.toContain("prod:apply");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("a change set with a provisional member is refused before anything deploys", async () => {
|
|
557
|
+
resolveTargetsMock.mockResolvedValue({
|
|
558
|
+
success: true,
|
|
559
|
+
targets: [{ ...gated, deploy: [...gated.deploy, { phase: "Plan", steps: [{ kind: "plan-provisional" }] }] }],
|
|
560
|
+
});
|
|
561
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(1);
|
|
562
|
+
expect(errors.join("\n")).toMatch(/provisional members \(estate\)/);
|
|
563
|
+
expect(ran).toEqual([]);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("a release promote whose compositions declare no gate binds nothing and stores no change set", async () => {
|
|
567
|
+
resolveTargetsMock.mockResolvedValue({ success: true, targets: [{ ...gated, deploy: [...gated.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "apply", template: "dist/api.template.json" }] }] }] });
|
|
568
|
+
expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
|
|
569
|
+
expect(storedChangeSets.size).toBe(0);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
258
573
|
describe("chant components rollback", () => {
|
|
259
574
|
const prod = (digest: string, timestamp: string, runId: string): ReleaseRecord =>
|
|
260
575
|
({ ...staging, env: "prod", digest, timestamp, runId });
|
|
@@ -396,3 +711,72 @@ describe("chant components redeploy", () => {
|
|
|
396
711
|
expect(errors.join("\n")).toMatch(/redeploy failed at "api"/);
|
|
397
712
|
});
|
|
398
713
|
});
|
|
714
|
+
|
|
715
|
+
describe("--events-json on promote, rollback and redeploy (#4041)", () => {
|
|
716
|
+
const prod = (digest: string, timestamp: string, runId: string): ReleaseRecord =>
|
|
717
|
+
({ ...staging, env: "prod", digest, timestamp, runId });
|
|
718
|
+
let out: string[];
|
|
719
|
+
|
|
720
|
+
beforeEach(() => {
|
|
721
|
+
out = [];
|
|
722
|
+
vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
/** The stream's events, checked, and the release record's run id beside them. */
|
|
726
|
+
// Only the stream's lines: anything else a module writes to stdout is not this test's.
|
|
727
|
+
const streamed = () => checkRunEventLines(out.filter((l) => l.startsWith('{"$schema"')));
|
|
728
|
+
|
|
729
|
+
test("promote streams the deploy as a component run named by the release record's run id", async () => {
|
|
730
|
+
expect(await runComponentsPromote(ctx({ eventsJson: true }))).toBe(0);
|
|
731
|
+
const events = streamed();
|
|
732
|
+
expect(events[0]).toMatchObject({ type: "run-start", kind: "component", name: "promote api in prod", runId: "run-9", waves: [["api"]] });
|
|
733
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ runId: "run-9" });
|
|
734
|
+
expect(stepTrail(events)).toEqual([
|
|
735
|
+
["step-start", "publish-image"], ["step-settle", "publish-image", "ok"],
|
|
736
|
+
["step-start", "apply"], ["step-settle", "apply", "ok"],
|
|
737
|
+
]);
|
|
738
|
+
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("rollback streams a gated run: the gate's step settles gated, and the run with it", async () => {
|
|
742
|
+
readReleaseLedgerMock.mockResolvedValue({
|
|
743
|
+
records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
|
|
744
|
+
malformed: 0,
|
|
745
|
+
});
|
|
746
|
+
const gated: DriverComponent = {
|
|
747
|
+
...api,
|
|
748
|
+
deploy: [...api.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "gate", gate: "rollback-ok" }, { kind: "apply" }] }],
|
|
749
|
+
};
|
|
750
|
+
resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
|
|
751
|
+
expect(await runComponentsRollback(ctx({ migrateFrom: undefined, migrateTo: undefined, path: "rollback", extraPositional: "prod", component: "api", eventsJson: true }))).toBe(3);
|
|
752
|
+
const events = streamed();
|
|
753
|
+
expect(events[0]).toMatchObject({ name: "rollback api in prod" });
|
|
754
|
+
// A rollback deploys the recorded digest in place of the publish step.
|
|
755
|
+
expect(stepTrail(events)).toEqual([
|
|
756
|
+
["step-start", "recorded-digest"], ["step-settle", "recorded-digest", "ok"],
|
|
757
|
+
["step-start", "gate:rollback-ok"], ["gate-waiting", "gate:rollback-ok"], ["step-settle", "gate:rollback-ok", "gated"],
|
|
758
|
+
["step-settle", "apply", "skipped:not-run"],
|
|
759
|
+
]);
|
|
760
|
+
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "gated" });
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("redeploy streams a failed run with the failing step's error", async () => {
|
|
764
|
+
readReleaseLedgerMock.mockResolvedValue({
|
|
765
|
+
records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
|
|
766
|
+
malformed: 0,
|
|
767
|
+
});
|
|
768
|
+
failApplyFor = "api";
|
|
769
|
+
expect(await runComponentsRedeploy(ctx({ migrateFrom: undefined, migrateTo: undefined, path: "redeploy", extraPositional: "prod", component: "api", eventsJson: true }))).toBe(1);
|
|
770
|
+
const events = streamed();
|
|
771
|
+
expect(events[0]).toMatchObject({ name: "redeploy api in prod" });
|
|
772
|
+
expect(events.find((e) => e.type === "step-settle" && e.status === "failed")).toMatchObject({ step: "apply", error: "apply failed" });
|
|
773
|
+
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("--events-json beside --progress-json is refused before anything deploys", async () => {
|
|
777
|
+
expect(await runComponentsPromote(ctx({ eventsJson: true, progressJson: true }))).toBe(1);
|
|
778
|
+
expect(errors.join("\n")).toMatch(/--events-json and --progress-json both stream to stdout/);
|
|
779
|
+
expect(ran).toEqual([]);
|
|
780
|
+
expect(out.filter((l) => l.startsWith('{"$schema"'))).toEqual([]);
|
|
781
|
+
});
|
|
782
|
+
});
|