@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
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
import { resolve } from "node:path";
|
|
18
18
|
import { readFileSync } from "node:fs";
|
|
19
19
|
import { loadChantConfig, type ChantConfig } from "../../config";
|
|
20
|
+
import { promotionOrder } from "../../workspace/promotion-order";
|
|
21
|
+
import { resolvePromotionSource } from "../../components/promote-order";
|
|
20
22
|
import { fetchLifecycle, getHeadCommit, pushLifecycle } from "../../lifecycle/git";
|
|
21
23
|
import {
|
|
22
24
|
appendReleaseRecord,
|
|
25
|
+
latestPerComponent,
|
|
23
26
|
readReleaseLedger,
|
|
24
27
|
resolveRunId,
|
|
25
28
|
type ReleaseRecord,
|
|
@@ -30,6 +33,7 @@ import { applyConfigDefaults } from "../../components/config-defaults";
|
|
|
30
33
|
import { fanOutRegistry } from "../../components/fan-out-support";
|
|
31
34
|
import { renderDriverHuman } from "../../components/driver-output";
|
|
32
35
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
36
|
+
import { openCliRunEvents, refusesBothStreams } from "../run-events";
|
|
33
37
|
import {
|
|
34
38
|
parseDigestPins,
|
|
35
39
|
planPromotion,
|
|
@@ -41,20 +45,23 @@ import {
|
|
|
41
45
|
rollbackRecord,
|
|
42
46
|
redeployRecord,
|
|
43
47
|
gateApprover,
|
|
48
|
+
gateAdmission,
|
|
44
49
|
type DeployRunInfo,
|
|
45
50
|
type PromotionItem,
|
|
46
51
|
type PromotionPlan,
|
|
47
52
|
} from "../../components/promote";
|
|
48
53
|
import type { DriverComponent } from "../../components/driver";
|
|
49
54
|
import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
|
|
50
|
-
import { approveCommand, describeGateMismatch, gateIsSealed } from "../../op/gate";
|
|
55
|
+
import { approveCommand, describeGateMismatch, gateIsSealed, gitGateLedgerPort, type GateLedgerPort } from "../../op/gate";
|
|
56
|
+
import type { ReleaseGateBinding } from "../../components/driver";
|
|
57
|
+
import type { ChangeSetDocument } from "../../change-set";
|
|
51
58
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
52
59
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
53
60
|
import { GATED_EXIT_CODE } from "./run";
|
|
54
61
|
import type { CommandContext } from "../registry";
|
|
55
62
|
|
|
56
63
|
const PROMOTE_USAGE =
|
|
57
|
-
"chant components promote --
|
|
64
|
+
"chant components promote --to <env> [--from <env>] [--allow-skip] [--release <label|digest>] [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
|
|
58
65
|
const ROLLBACK_USAGE =
|
|
59
66
|
"chant components rollback <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
|
|
60
67
|
const REDEPLOY_USAGE =
|
|
@@ -62,6 +69,13 @@ const REDEPLOY_USAGE =
|
|
|
62
69
|
|
|
63
70
|
type Verb = "promote" | "rollback" | "redeploy";
|
|
64
71
|
|
|
72
|
+
/** The release a promote deploys under (#4026), and its target for the environment promoted to. */
|
|
73
|
+
interface UnderRelease {
|
|
74
|
+
release: import("../../lifecycle/release-store").Release;
|
|
75
|
+
label: string | null;
|
|
76
|
+
target: import("../../lifecycle/release-store").ReleaseTarget;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
function renderPlan(verb: Verb, plan: PromotionPlan, removed: Map<string, string[]>): void {
|
|
66
80
|
console.error(formatBold(verb === "promote" ? `promote ${plan.from} -> ${plan.to}` : `${verb} ${plan.to}`));
|
|
67
81
|
for (const item of plan.items) {
|
|
@@ -129,6 +143,115 @@ async function readLedger(env: string): Promise<ReleaseRecord[]> {
|
|
|
129
143
|
return ledger.records;
|
|
130
144
|
}
|
|
131
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Plan, store and bind the release gate for a promote under a release
|
|
148
|
+
* (#4045, ws-104 questions 2 and 6). Undefined when no promoted composition
|
|
149
|
+
* declares a gate, so there is nothing to bind. A number is the exit code of
|
|
150
|
+
* a refusal already printed: a provisional member, or a member that failed
|
|
151
|
+
* to plan. On an empty change set, each gate gets an `empty: true`
|
|
152
|
+
* resolution the run then counts once it reads the stored document back.
|
|
153
|
+
*/
|
|
154
|
+
async function bindReleaseGate(input: {
|
|
155
|
+
plan: PromotionPlan;
|
|
156
|
+
prepared: DriverComponent[];
|
|
157
|
+
under: UnderRelease;
|
|
158
|
+
registry: Awaited<ReturnType<typeof fanOutRegistry>>;
|
|
159
|
+
port: GateLedgerPort;
|
|
160
|
+
seededOutputs: Record<string, Record<string, unknown>>;
|
|
161
|
+
actor: string;
|
|
162
|
+
projectPath: string;
|
|
163
|
+
head: string | undefined;
|
|
164
|
+
}): Promise<{ binding: ReleaseGateBinding; doc: ChangeSetDocument; empty: boolean } | number | undefined> {
|
|
165
|
+
const { plan, under, port } = input;
|
|
166
|
+
const gate = await import("../../components/release-gate");
|
|
167
|
+
const gates = gate.gateNamesOf(input.prepared);
|
|
168
|
+
if (gates.length === 0) return undefined;
|
|
169
|
+
const env = plan.to;
|
|
170
|
+
const releaseDigest = under.release.releaseDigest;
|
|
171
|
+
|
|
172
|
+
// What the environment runs now, to compare each component with.
|
|
173
|
+
const { readRelease } = await import("../../lifecycle/release-store");
|
|
174
|
+
const latest = latestPerComponent((await readLedger(env)).filter((r) => r.env === env));
|
|
175
|
+
const releases = new Map<string, import("../../lifecycle/release-store").Release | null>();
|
|
176
|
+
const current = new Map<string, import("../../components/release-gate").CurrentDeploy>();
|
|
177
|
+
for (const item of plan.items) {
|
|
178
|
+
const record = latest.get(item.component);
|
|
179
|
+
if (!record) continue;
|
|
180
|
+
const named = record.releaseDigest;
|
|
181
|
+
if (named !== undefined && !releases.has(named)) releases.set(named, await readRelease(named).catch(() => null));
|
|
182
|
+
current.set(item.component, { record, release: named !== undefined ? (releases.get(named) ?? null) : null });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const doc = await gate.planReleaseChangeSet({
|
|
186
|
+
release: under.release,
|
|
187
|
+
env,
|
|
188
|
+
components: input.prepared,
|
|
189
|
+
digests: new Map(plan.items.map((i) => [i.component, i.digest])),
|
|
190
|
+
registry: input.registry,
|
|
191
|
+
current,
|
|
192
|
+
seededOutputs: input.seededOutputs,
|
|
193
|
+
});
|
|
194
|
+
const provisional = gate.provisionalMembers(doc);
|
|
195
|
+
if (provisional.length > 0) {
|
|
196
|
+
console.error(formatError({
|
|
197
|
+
message: `the change set of release ${under.label ?? releaseDigest} for "${env}" has provisional members (${provisional.join(", ")}), which no approval covers; nothing was deployed`,
|
|
198
|
+
hint: "Deploy what they read first, so they plan against real values.",
|
|
199
|
+
}));
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
const failed = doc.members.filter((m) => m.status === "failed");
|
|
203
|
+
if (failed.length > 0) {
|
|
204
|
+
for (const m of failed) console.error(formatError({ message: `${m.member} failed to plan for "${env}"${m.error ? `: ${m.error}` : ""}` }));
|
|
205
|
+
console.error(formatError({ message: "nothing was deployed" }));
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The document is stored before the gate is decided, so the plan a pending
|
|
210
|
+
// gate waits on can be read, and an empty pass can be checked.
|
|
211
|
+
const { storeGateChangeSet } = await import("../../lifecycle/gate-change-set");
|
|
212
|
+
try {
|
|
213
|
+
const stored = await storeGateChangeSet({ doc, cwd: input.projectPath, at: input.head ?? null });
|
|
214
|
+
if (stored.warning) console.error(formatWarning({ message: stored.warning }));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(formatWarning({ message: `could not store the change set ${doc.digest}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
217
|
+
}
|
|
218
|
+
const { isEmptyChangeSet } = await import("../../change-set");
|
|
219
|
+
const empty = isEmptyChangeSet(doc);
|
|
220
|
+
console.error(formatInfo(
|
|
221
|
+
`release gate: change set ${doc.digest} for "${env}"` +
|
|
222
|
+
(empty ? ", empty: its gates pass without an approval" : `, ${doc.summary.entries} entr${doc.summary.entries === 1 ? "y" : "ies"}, ${doc.summary.holes} hole(s)`) +
|
|
223
|
+
` (chant change-set show ${doc.digest})`,
|
|
224
|
+
));
|
|
225
|
+
|
|
226
|
+
if (empty) {
|
|
227
|
+
if (!port.appendResolution) throw new Error("the gate ledger can't record a resolution, so an empty change set can't pass its gates");
|
|
228
|
+
const { resolutions } = await port.read(gate.RELEASE_GATE_OP);
|
|
229
|
+
const timestamp = new Date().toISOString();
|
|
230
|
+
for (const name of gates) {
|
|
231
|
+
const standing = resolutions.some(
|
|
232
|
+
(r) => r.empty === true && r.gate === name && r.environment === env && r.releaseDigest === releaseDigest && r.planDigest === doc.digest,
|
|
233
|
+
);
|
|
234
|
+
if (standing) continue;
|
|
235
|
+
// Pushed with the resolution; the deploy records the promote writes push the same branch.
|
|
236
|
+
await port.appendResolution(
|
|
237
|
+
gate.emptyResolution({ gate: name, doc, actor: input.actor, timestamp, ...(input.head ? { gitSha: input.head } : {}) }),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { readStoredChangeSet } = await import("../../lifecycle/change-set-store");
|
|
243
|
+
return {
|
|
244
|
+
doc,
|
|
245
|
+
empty,
|
|
246
|
+
binding: {
|
|
247
|
+
op: gate.RELEASE_GATE_OP,
|
|
248
|
+
planDigest: doc.digest,
|
|
249
|
+
releaseDigest,
|
|
250
|
+
verifyEmpty: gate.emptyVerifier((digest) => readStoredChangeSet(digest, { cwd: input.projectPath }), { releaseDigest, environment: env }),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
132
255
|
/**
|
|
133
256
|
* Prepare, run and record a plan. Shared by promote, rollback and redeploy,
|
|
134
257
|
* which differ only in how the plan was chosen, whether the publish step runs,
|
|
@@ -141,8 +264,10 @@ async function deployPlan(
|
|
|
141
264
|
project: { projectPath: string; config: ChantConfig; targets: DriverComponent[] },
|
|
142
265
|
actor: string | undefined,
|
|
143
266
|
record: (item: PromotionItem, env: string, run: DeployRunInfo) => ReleaseRecordInput,
|
|
267
|
+
under?: UnderRelease,
|
|
144
268
|
): Promise<number> {
|
|
145
269
|
const { args } = ctx;
|
|
270
|
+
if (refusesBothStreams(args)) return 1;
|
|
146
271
|
const byName = new Map(project.targets.map((c) => [c.name, applyConfigDefaults(c, project.config)]));
|
|
147
272
|
const prepared: DriverComponent[] = [];
|
|
148
273
|
const removed = new Map<string, string[]>();
|
|
@@ -166,6 +291,37 @@ async function deployPlan(
|
|
|
166
291
|
return 1;
|
|
167
292
|
}
|
|
168
293
|
|
|
294
|
+
// #4026: under a release, the deploy reads the templates the release
|
|
295
|
+
// recorded for this environment, checked against their digests.
|
|
296
|
+
let pinnedFiles: import("../../components/release-promote").PinnedTemplate[] = [];
|
|
297
|
+
if (under) {
|
|
298
|
+
const { pinReleaseTemplates } = await import("../../components/release-promote");
|
|
299
|
+
const { readReleaseTemplate } = await import("../../lifecycle/release-store");
|
|
300
|
+
const pinned = await pinReleaseTemplates(prepared, under.release, under.target, (digest) => readReleaseTemplate(digest));
|
|
301
|
+
if ("errors" in pinned) {
|
|
302
|
+
for (const message of pinned.errors) console.error(formatError({ message }));
|
|
303
|
+
console.error(formatError({ message: `nothing was deployed` }));
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
prepared.splice(0, prepared.length, ...pinned.components);
|
|
307
|
+
pinnedFiles = pinned.files;
|
|
308
|
+
for (const item of plan.items) {
|
|
309
|
+
const named = item.source.releaseDigest;
|
|
310
|
+
if (named !== undefined && named !== under.release.releaseDigest) {
|
|
311
|
+
console.error(formatWarning({ message: `${item.component}: the ${plan.from} deploy being promoted came from release ${named}, not ${under.release.releaseDigest}` }));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// #4045: the artifact deployed against the artifacts the release built.
|
|
315
|
+
const { releaseArtifactMismatches } = await import("../../components/release-promote");
|
|
316
|
+
const { readBuildManifest } = await import("../../lifecycle/build-ledger-store");
|
|
317
|
+
const mismatches = await releaseArtifactMismatches(
|
|
318
|
+
under.release,
|
|
319
|
+
plan.items.map((i) => ({ component: i.component, source: i.source })),
|
|
320
|
+
(digest) => readBuildManifest(digest).catch(() => null),
|
|
321
|
+
);
|
|
322
|
+
for (const message of mismatches) console.error(formatWarning({ message }));
|
|
323
|
+
}
|
|
324
|
+
|
|
169
325
|
// The composition deployed is this checkout's. Say so when it is not the
|
|
170
326
|
// commit the artifact was built from.
|
|
171
327
|
const head = await getHeadCommit().catch(() => undefined);
|
|
@@ -178,12 +334,24 @@ async function deployPlan(
|
|
|
178
334
|
}
|
|
179
335
|
}
|
|
180
336
|
|
|
337
|
+
const releaseJson: { release?: { releaseDigest: string; label: string | null; templates: unknown[] } } = under
|
|
338
|
+
? { release: { releaseDigest: under.release.releaseDigest, label: under.label, templates: under.target.templates } }
|
|
339
|
+
: {};
|
|
181
340
|
if (args.dryRun) {
|
|
182
|
-
if (args.json) console.log(JSON.stringify(planJson(plan, removed), null, 2));
|
|
183
|
-
else
|
|
341
|
+
if (args.json) console.log(JSON.stringify({ ...planJson(plan, removed), ...releaseJson }, null, 2));
|
|
342
|
+
else {
|
|
343
|
+
renderPlan(verb, plan, removed);
|
|
344
|
+
if (under) console.error(` under release ${under.label ?? under.release.releaseDigest}: ${under.target.templates.length} template(s) from its "${plan.to}" target`);
|
|
345
|
+
}
|
|
184
346
|
return 0;
|
|
185
347
|
}
|
|
186
348
|
if (!args.json) renderPlan(verb, plan, removed);
|
|
349
|
+
// #4045: the pinned templates are written for this run and removed after it.
|
|
350
|
+
const { writePinnedTemplates, removePinnedTemplates } = await import("../../components/release-promote");
|
|
351
|
+
const tidy = () => {
|
|
352
|
+
if (pinnedFiles.length > 0) removePinnedTemplates(project.projectPath, pinnedFiles);
|
|
353
|
+
};
|
|
354
|
+
if (pinnedFiles.length > 0) writePinnedTemplates(project.projectPath, pinnedFiles);
|
|
187
355
|
|
|
188
356
|
const seededOutputs: Record<string, Record<string, unknown>> = {};
|
|
189
357
|
for (const file of args.seedOutputs ?? []) {
|
|
@@ -191,35 +359,78 @@ async function deployPlan(
|
|
|
191
359
|
Object.assign(seededOutputs, JSON.parse(readFileSync(resolve(file), "utf8")));
|
|
192
360
|
} catch (err) {
|
|
193
361
|
console.error(formatError({ message: `--seed-outputs: could not read "${file}": ${err instanceof Error ? err.message : String(err)}` }));
|
|
362
|
+
tidy();
|
|
194
363
|
return 1;
|
|
195
364
|
}
|
|
196
365
|
}
|
|
197
366
|
|
|
198
367
|
const registry = await fanOutRegistry(project.projectPath, project.config);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
368
|
+
const port = gitGateLedgerPort();
|
|
369
|
+
// #4045: under a release, every gate binds the release's change set for this environment.
|
|
370
|
+
let releaseGate: ReleaseGateBinding | undefined;
|
|
371
|
+
let changeSetJson: { changeSet: string; empty: boolean } | undefined;
|
|
372
|
+
if (under) {
|
|
373
|
+
try {
|
|
374
|
+
const bound = await bindReleaseGate({ plan, prepared, under, registry, port, seededOutputs, actor: actor!, projectPath: project.projectPath, head });
|
|
375
|
+
if (typeof bound === "number") {
|
|
376
|
+
tidy();
|
|
377
|
+
return bound;
|
|
378
|
+
}
|
|
379
|
+
if (bound) {
|
|
380
|
+
releaseGate = bound.binding;
|
|
381
|
+
changeSetJson = { changeSet: bound.doc.digest, empty: bound.empty };
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
tidy();
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// The run id the release records carry, chosen before the run so that
|
|
389
|
+
// `--events-json` (#4041) names the run by it too.
|
|
390
|
+
const { runId, runOrigin } = resolveRunId(args.runId);
|
|
391
|
+
const events = await openCliRunEvents(args, {
|
|
392
|
+
kind: "component",
|
|
393
|
+
name: `${verb} ${plan.items.map((i) => i.component).join(",")} in ${plan.to}`,
|
|
394
|
+
runId,
|
|
395
|
+
cwd: project.projectPath,
|
|
205
396
|
});
|
|
397
|
+
let run: Awaited<ReturnType<typeof runPromotion>>;
|
|
398
|
+
try {
|
|
399
|
+
run = await runPromotion({
|
|
400
|
+
plan,
|
|
401
|
+
components: prepared,
|
|
402
|
+
registry,
|
|
403
|
+
componentOutputs: seededOutputs,
|
|
404
|
+
gates: port,
|
|
405
|
+
...(releaseGate ? { releaseGate } : {}),
|
|
406
|
+
...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
|
|
407
|
+
...(events ? { onRunEvent: events.emit } : {}),
|
|
408
|
+
});
|
|
409
|
+
} catch (err) {
|
|
410
|
+
events?.settle("failed", err instanceof Error ? err.message : String(err));
|
|
411
|
+
throw err;
|
|
412
|
+
} finally {
|
|
413
|
+
tidy();
|
|
414
|
+
}
|
|
415
|
+
// The driver settles the stream; this only ends one it left open.
|
|
416
|
+
events?.settle(run.status === "fail" ? "failed" : run.status);
|
|
206
417
|
if (!args.json) renderDriverHuman(run);
|
|
207
418
|
|
|
208
419
|
// Record every component that deployed, whatever happened to the rest: a
|
|
209
420
|
// component that reached the environment and is missing from its ledger
|
|
210
421
|
// would be exactly the unrecorded deploy the ledger exists to rule out.
|
|
211
422
|
const deployed = plan.items.filter((item) => run.results.some((r) => r.component === item.component && r.status === "ok"));
|
|
212
|
-
const { runId, runOrigin } = resolveRunId(args.runId);
|
|
213
423
|
const timestamp = new Date().toISOString();
|
|
214
424
|
const recorded: ReleaseRecord[] = [];
|
|
215
425
|
let recordError: string | undefined;
|
|
216
426
|
if (deployed.length > 0) {
|
|
217
427
|
try {
|
|
218
428
|
for (const item of deployed) {
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
);
|
|
429
|
+
const result = run.results.find((r) => r.component === item.component);
|
|
430
|
+
const approver = gateApprover(result);
|
|
431
|
+
const admission = gateAdmission(result);
|
|
432
|
+
const input = record(item, plan.to, { runId, ...(runOrigin ? { runOrigin } : {}), actor: actor!, timestamp, ...(approver ? { approver } : {}), ...(admission ? { admission } : {}) });
|
|
433
|
+
const { record: written } = await appendReleaseRecord(under ? { ...input, releaseDigest: under.release.releaseDigest } : input);
|
|
223
434
|
recorded.push(written);
|
|
224
435
|
}
|
|
225
436
|
await pushLifecycle();
|
|
@@ -229,7 +440,8 @@ async function deployPlan(
|
|
|
229
440
|
}
|
|
230
441
|
|
|
231
442
|
if (args.json) {
|
|
232
|
-
|
|
443
|
+
const withChangeSet = under && changeSetJson ? { release: { ...releaseJson.release!, ...changeSetJson } } : releaseJson;
|
|
444
|
+
console.log(JSON.stringify({ ...planJson(plan, removed), ...withChangeSet, run, recorded }, null, 2));
|
|
233
445
|
} else {
|
|
234
446
|
for (const r of recorded) {
|
|
235
447
|
console.error(formatSuccess(
|
|
@@ -296,10 +508,9 @@ function actorOrRefuse(ctx: CommandContext): { actor: string | undefined } | num
|
|
|
296
508
|
|
|
297
509
|
export async function runComponentsPromote(ctx: CommandContext): Promise<number> {
|
|
298
510
|
const { args } = ctx;
|
|
299
|
-
const from = args.migrateFrom;
|
|
300
511
|
const to = args.migrateTo;
|
|
301
|
-
if (!
|
|
302
|
-
console.error(formatError({ message: "--
|
|
512
|
+
if (!to) {
|
|
513
|
+
console.error(formatError({ message: "--to <env> is required", hint: PROMOTE_USAGE }));
|
|
303
514
|
return 1;
|
|
304
515
|
}
|
|
305
516
|
const who = actorOrRefuse(ctx);
|
|
@@ -308,6 +519,25 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
308
519
|
const project = await loadProject(ctx, args.component ?? "all");
|
|
309
520
|
if (typeof project === "number") return project;
|
|
310
521
|
|
|
522
|
+
// The member's `environments` array is its promotion order (#4030). It
|
|
523
|
+
// defaults --from and refuses a skipped step; a member that declares none
|
|
524
|
+
// has no order, so --from is required and nothing is refused.
|
|
525
|
+
const resolved = resolvePromotionSource({
|
|
526
|
+
order: promotionOrder(project.config.environments),
|
|
527
|
+
to,
|
|
528
|
+
...(args.migrateFrom ? { from: args.migrateFrom } : {}),
|
|
529
|
+
...(args.allowSkip ? { allowSkip: true } : {}),
|
|
530
|
+
});
|
|
531
|
+
if ("error" in resolved) {
|
|
532
|
+
console.error(formatError({ message: `${resolved.error.message} [${resolved.error.code}]`, hint: resolved.error.fix }));
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
const from = resolved.from;
|
|
536
|
+
if (!from) {
|
|
537
|
+
console.error(formatError({ message: "--from <env> is required: this member declares no environments, so there is no promotion order to default it from", hint: PROMOTE_USAGE }));
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
311
541
|
// Without --component, each --digest is <component>=<digest> (#2602): a
|
|
312
542
|
// generated promote job pins every component to the release its own
|
|
313
543
|
// pipeline run recorded, rather than whatever is latest in --from.
|
|
@@ -326,6 +556,35 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
326
556
|
}
|
|
327
557
|
|
|
328
558
|
await fetchLifecycle().catch(() => false);
|
|
559
|
+
|
|
560
|
+
// #4026: --release names the release this promote deploys, by label or digest.
|
|
561
|
+
let under: UnderRelease | undefined;
|
|
562
|
+
if (args.release) {
|
|
563
|
+
const { resolveRelease } = await import("../../lifecycle/release-store");
|
|
564
|
+
const { targetFor, isImplicitRelease } = await import("../../components/release-promote");
|
|
565
|
+
const found = await resolveRelease(args.release);
|
|
566
|
+
if (!found) {
|
|
567
|
+
console.error(formatError({ message: `no release is labelled or digested "${args.release}" in this member`, hint: "List them with: chant release list" }));
|
|
568
|
+
return 1;
|
|
569
|
+
}
|
|
570
|
+
const target = targetFor(found.release, to);
|
|
571
|
+
if ("error" in target) {
|
|
572
|
+
console.error(formatError({ message: target.error }));
|
|
573
|
+
return 1;
|
|
574
|
+
}
|
|
575
|
+
// #4046: a release of a project with no components records build output
|
|
576
|
+
// and no component, so there is no deploy step here to point at it.
|
|
577
|
+
if (isImplicitRelease(found.release)) {
|
|
578
|
+
const name = found.label ?? found.release.releaseDigest;
|
|
579
|
+
console.error(formatError({
|
|
580
|
+
message: `release ${name} is of a project with no components, so promote has no component to deploy`,
|
|
581
|
+
hint: `Write its "${to}" build output with \`chant release export ${name} --env ${to}\`, then run the project's own deploy against those files.`,
|
|
582
|
+
}));
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
under = { release: found.release, label: found.label, target: target.target };
|
|
586
|
+
}
|
|
587
|
+
|
|
329
588
|
const plan = planPromotion({
|
|
330
589
|
from,
|
|
331
590
|
to,
|
|
@@ -339,7 +598,7 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
339
598
|
console.error(formatError({ message: plan.error, hint: PROMOTE_USAGE }));
|
|
340
599
|
return 1;
|
|
341
600
|
}
|
|
342
|
-
return deployPlan(ctx, "promote", plan, project, who.actor, promotionRecord);
|
|
601
|
+
return deployPlan(ctx, "promote", plan, project, who.actor, promotionRecord, under);
|
|
343
602
|
}
|
|
344
603
|
|
|
345
604
|
export async function runComponentsRollback(ctx: CommandContext): Promise<number> {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An implicit release of a project with no components, end to end (#4046):
|
|
3
|
+
* the getting-started example, given two environments and a build parameter
|
|
4
|
+
* set per environment, is cut from the same commit in two clones. Each
|
|
5
|
+
* environment's build output must come out byte for byte the same, and the
|
|
6
|
+
* two cuts must give one release digest. No cluster is touched: the release
|
|
7
|
+
* is exported to files and never deployed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join, relative } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { contract, REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
17
|
+
import releasesSchema from "../../workspace/releases.schema.json";
|
|
18
|
+
import { contentDigest } from "../../content-digest";
|
|
19
|
+
|
|
20
|
+
const CHANT = [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")];
|
|
21
|
+
const EXAMPLE = join(REPO, "examples", "getting-started");
|
|
22
|
+
|
|
23
|
+
function chant(cwd: string, ...args: string[]): { code: number; stdout: string; stderr: string } {
|
|
24
|
+
const env: NodeJS.ProcessEnv = { ...process.env, USER: "tester" };
|
|
25
|
+
for (const k of ["GITHUB_RUN_ID", "CI_PIPELINE_ID", "GITHUB_ACTOR", "GITLAB_USER_LOGIN", "CHANT_ENV", "GITHUB_SHA", "CI"]) delete env[k];
|
|
26
|
+
const r = spawnSync(CHANT[0], [...CHANT.slice(1), ...args], { cwd, encoding: "utf-8", env, timeout: 180_000 });
|
|
27
|
+
return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function git(cwd: string, ...args: string[]): string {
|
|
31
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
32
|
+
if (r.status !== 0) throw new Error(r.stderr);
|
|
33
|
+
return r.stdout.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Every file under `dir`, by its path relative to `dir`, as bytes. */
|
|
37
|
+
function filesUnder(dir: string): Map<string, Buffer> {
|
|
38
|
+
const out = new Map<string, Buffer>();
|
|
39
|
+
const walk = (d: string): void => {
|
|
40
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
41
|
+
const p = join(d, e.name);
|
|
42
|
+
if (e.isDirectory()) walk(p);
|
|
43
|
+
else out.set(relative(dir, p), readFileSync(p));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(dir);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The example as a project of its own with two environments and a parameter that differs between them. */
|
|
51
|
+
function writeProject(dir: string): void {
|
|
52
|
+
cpSync(join(EXAMPLE, "src"), join(dir, "src"), { recursive: true });
|
|
53
|
+
// The deploy Op goes under the build root, so the build writes src/dist/ops/deploy/op.json too: an Op's output goes under its build root.
|
|
54
|
+
cpSync(join(EXAMPLE, "deploy.op.ts"), join(dir, "src", "deploy.op.ts"));
|
|
55
|
+
const web = readFileSync(join(dir, "src", "web.ts"), "utf-8");
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(dir, "src", "web.ts"),
|
|
58
|
+
`import { params } from "@intentius/chant/params";\n${web.replace("replicas: 2,", "replicas: params.replicas as number,")}`,
|
|
59
|
+
);
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(dir, "chant.config.ts"),
|
|
62
|
+
`export default {\n lexicons: ["k8s"],\n sourceDir: "src",\n environments: ["dev", "prod"],\n buildParams: { replicas: { type: "number", default: 2 } },\n};\n`,
|
|
63
|
+
);
|
|
64
|
+
writeFileSync(join(dir, "params.dev.json"), `{ "replicas": 1 }\n`);
|
|
65
|
+
writeFileSync(join(dir, "params.prod.json"), `{ "replicas": 3 }\n`);
|
|
66
|
+
writeFileSync(join(dir, ".gitignore"), "node_modules\ndist/\nout/\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("an implicit release of a project with no components", () => {
|
|
70
|
+
test(
|
|
71
|
+
"two cuts of one commit, in two clones, give byte-identical build output per environment and one release digest",
|
|
72
|
+
() => {
|
|
73
|
+
const root = mkdtempSync(join(tmpdir(), "chant-release-implicit-"));
|
|
74
|
+
try {
|
|
75
|
+
const a = join(root, "a");
|
|
76
|
+
const b = join(root, "b");
|
|
77
|
+
mkdirSync(a);
|
|
78
|
+
writeProject(a);
|
|
79
|
+
git(a, "init", "-q", "-b", "main");
|
|
80
|
+
git(a, "config", "user.email", "t@chant.dev");
|
|
81
|
+
git(a, "config", "user.name", "T");
|
|
82
|
+
git(a, "add", ".");
|
|
83
|
+
git(a, "commit", "-q", "-m", "init");
|
|
84
|
+
const sha = git(a, "rev-parse", "HEAD");
|
|
85
|
+
git(a, "tag", `ci/green/${sha}`);
|
|
86
|
+
git(root, "clone", "-q", a, b);
|
|
87
|
+
git(b, "remote", "remove", "origin");
|
|
88
|
+
for (const d of [a, b]) symlinkSync(join(REPO, "node_modules"), join(d, "node_modules"), "dir");
|
|
89
|
+
|
|
90
|
+
const cuts = [a, b].map((d, i) => {
|
|
91
|
+
const r = chant(d, "release", "cut", ...(i === 0 ? ["v1"] : []), "--params-file", "params.{env}.json", "--json");
|
|
92
|
+
expect(r.code, r.stderr).toBe(0);
|
|
93
|
+
return JSON.parse(r.stdout);
|
|
94
|
+
});
|
|
95
|
+
const [first, second] = cuts;
|
|
96
|
+
expect(second.release.releaseDigest).toBe(first.release.releaseDigest);
|
|
97
|
+
expect(second.release).toEqual(first.release);
|
|
98
|
+
expect(first.stored && second.stored).toBe(true);
|
|
99
|
+
|
|
100
|
+
const release = first.release;
|
|
101
|
+
expect(release.commit).toBe(sha);
|
|
102
|
+
expect(release.artifactDigests).toEqual([]);
|
|
103
|
+
expect(release.targets.map((t: { env: string }) => t.env)).toEqual(["dev", "prod"]);
|
|
104
|
+
for (const t of release.targets) {
|
|
105
|
+
expect(t.templates.map((x: { component: unknown; path: string }) => [x.component, x.path])).toEqual([
|
|
106
|
+
[null, "dist/build.yaml"],
|
|
107
|
+
[null, "src/dist/ops/deploy/op.json"],
|
|
108
|
+
]);
|
|
109
|
+
expect(t.params).toEqual([{ name: "replicas", value: t.env === "prod" ? 3 : 1, source: "params-file" }]);
|
|
110
|
+
}
|
|
111
|
+
// The parameter reaches the output, so the two environments' manifests differ.
|
|
112
|
+
expect(release.targets[0].templates[0].digest).not.toBe(release.targets[1].templates[0].digest);
|
|
113
|
+
// Nothing was written into the member's directory by the cut.
|
|
114
|
+
expect(existsSync(join(a, "dist")) || existsSync(join(a, "src", "dist"))).toBe(false);
|
|
115
|
+
|
|
116
|
+
// Each clone's stored bytes, written out per environment, are the same bytes.
|
|
117
|
+
for (const env of ["dev", "prod"]) {
|
|
118
|
+
const [fromA, fromB] = [a, b].map((d) => {
|
|
119
|
+
const ref = d === a ? "v1" : release.releaseDigest;
|
|
120
|
+
const r = chant(d, "release", "export", ref, "--env", env, "-o", `out/${env}`);
|
|
121
|
+
expect(r.code, r.stderr).toBe(0);
|
|
122
|
+
return filesUnder(join(d, "out", env));
|
|
123
|
+
});
|
|
124
|
+
expect([...fromA.keys()].sort()).toEqual(["dist/build.yaml", "src/dist/ops/deploy/op.json"]);
|
|
125
|
+
expect([...fromB.keys()].sort()).toEqual([...fromA.keys()].sort());
|
|
126
|
+
for (const [path, bytes] of fromA) {
|
|
127
|
+
expect(fromB.get(path)!.equals(bytes), `${env} ${path}`).toBe(true);
|
|
128
|
+
const recorded = release.targets.find((t: { env: string }) => t.env === env).templates.find((x: { path: string }) => x.path === path);
|
|
129
|
+
expect(contentDigest(bytes.toString("utf-8"))).toBe(recorded.digest);
|
|
130
|
+
}
|
|
131
|
+
expect(fromA.get("dist/build.yaml")!.toString("utf-8")).toContain(`replicas: ${env === "prod" ? 3 : 1}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// `release list --json` shows it as it shows any release.
|
|
135
|
+
const list = chant(a, "release", "list", "--json");
|
|
136
|
+
expect(list.code, list.stderr).toBe(0);
|
|
137
|
+
const doc = JSON.parse(list.stdout);
|
|
138
|
+
contract(releasesSchema).expectValid(doc);
|
|
139
|
+
expect(doc.releases).toHaveLength(1);
|
|
140
|
+
expect(doc.releases[0]).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1", artifactDigests: [], environments: [] });
|
|
141
|
+
|
|
142
|
+
// Promote has no component to deploy, and says what to run instead.
|
|
143
|
+
const promote = chant(a, "components", "promote", "--from", "dev", "--to", "prod", "--release", "v1");
|
|
144
|
+
expect(promote.code).toBe(1);
|
|
145
|
+
expect(promote.stderr).toMatch(/release v1 is of a project with no components/);
|
|
146
|
+
expect(promote.stderr).toMatch(/chant release export v1 --env prod/);
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(root, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
480_000,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant release cut` and `chant release list` end to end (#4026): the cut
|
|
3
|
+
* compiles each environment in a child chant with CHANT_ENV set, in a temp
|
|
4
|
+
* repo, and the list prints the read contract's `releases` document. No
|
|
5
|
+
* cloud is touched: the fixture's deploy is never run.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from "vitest";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { contract, REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
15
|
+
import releasesSchema from "../../workspace/releases.schema.json";
|
|
16
|
+
import { contentDigest } from "../../content-digest";
|
|
17
|
+
|
|
18
|
+
const CHANT = [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")];
|
|
19
|
+
|
|
20
|
+
function chant(cwd: string, ...args: string[]): { code: number; stdout: string; stderr: string } {
|
|
21
|
+
const env: NodeJS.ProcessEnv = { ...process.env, USER: "tester" };
|
|
22
|
+
for (const k of ["GITHUB_RUN_ID", "CI_PIPELINE_ID", "GITHUB_ACTOR", "GITLAB_USER_LOGIN", "CHANT_ENV"]) delete env[k];
|
|
23
|
+
const r = spawnSync(CHANT[0], [...CHANT.slice(1), ...args], { cwd, encoding: "utf-8", env, timeout: 120_000 });
|
|
24
|
+
return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function git(cwd: string, ...args: string[]): string {
|
|
28
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
29
|
+
if (r.status !== 0) throw new Error(r.stderr);
|
|
30
|
+
return r.stdout.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const BUILD = `mkdir -p dist && printf '{"env":"%s"}' "$CHANT_ENV" > dist/api.template.json`;
|
|
34
|
+
|
|
35
|
+
describe("chant release cut and list", () => {
|
|
36
|
+
test(
|
|
37
|
+
"cuts a green commit once per environment's compile, refuses a commit that isn't green, and lists the release",
|
|
38
|
+
() => {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-release-cut-"));
|
|
40
|
+
try {
|
|
41
|
+
git(dir, "init", "-q", "-b", "main");
|
|
42
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
43
|
+
git(dir, "config", "user.name", "T");
|
|
44
|
+
writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev", "prod", "pr-*"] };\n`);
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(dir, "api.component.ts"),
|
|
47
|
+
`export const api = { name: "api", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "dist/api.template.json" }] }] };\n`,
|
|
48
|
+
);
|
|
49
|
+
writeFileSync(join(dir, ".gitignore"), "dist/\n");
|
|
50
|
+
git(dir, "add", ".");
|
|
51
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
52
|
+
const sha = git(dir, "rev-parse", "HEAD");
|
|
53
|
+
|
|
54
|
+
const refused = chant(dir, "release", "cut", "v1", "--build", BUILD);
|
|
55
|
+
expect(refused.code).toBe(1);
|
|
56
|
+
expect(refused.stderr).toMatch(/has no ci\/green tag.*\[release-commit-not-green\]/);
|
|
57
|
+
|
|
58
|
+
git(dir, "tag", `ci/green/${sha}`);
|
|
59
|
+
const cut = chant(dir, "release", "cut", "v1", "--build", BUILD, "--json");
|
|
60
|
+
expect(cut.code, cut.stderr).toBe(0);
|
|
61
|
+
const result = JSON.parse(cut.stdout);
|
|
62
|
+
expect(result.label).toBe("v1");
|
|
63
|
+
expect(result.release.commit).toBe(sha);
|
|
64
|
+
expect(result.release.targets).toEqual([
|
|
65
|
+
{ env: "dev", params: [], templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"dev"}`) }] },
|
|
66
|
+
{ env: "prod", params: [], templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"prod"}`) }] },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const again = chant(dir, "release", "cut", "--build", BUILD, "--json");
|
|
70
|
+
expect(again.code, again.stderr).toBe(0);
|
|
71
|
+
expect(JSON.parse(again.stdout)).toMatchObject({ label: "v1", cut: null, release: { releaseDigest: result.release.releaseDigest } });
|
|
72
|
+
|
|
73
|
+
const list = chant(dir, "release", "list", "--json");
|
|
74
|
+
expect(list.code, list.stderr).toBe(0);
|
|
75
|
+
const doc = JSON.parse(list.stdout);
|
|
76
|
+
contract(releasesSchema).expectValid(doc);
|
|
77
|
+
expect(doc.releases).toHaveLength(1);
|
|
78
|
+
expect(doc.releases[0]).toMatchObject({ releaseDigest: result.release.releaseDigest, label: "v1", environments: [] });
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
240_000,
|
|
84
|
+
);
|
|
85
|
+
});
|