@intentius/chant 0.124.26 → 0.124.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/dist/__golden__/wave-loop/op-held-on.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/dist/__golden__/wave-loop/op-per-run.json +7 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
- package/dist/__golden__/wave-loop/op-sealed.json +3 -0
- package/dist/__golden__/wave-loop/op-spend.json +3 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/dist/change-set-graph.d.ts +56 -0
- package/dist/change-set-graph.d.ts.map +1 -0
- package/dist/change-set-graph.js +128 -0
- package/dist/change-set.d.ts +52 -3
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +35 -4
- package/dist/chant-version.d.ts +9 -0
- package/dist/chant-version.d.ts.map +1 -0
- package/dist/chant-version.js +16 -0
- package/dist/cli/commands/build.d.ts +8 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -8
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.js +30 -2
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +13 -2
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.js +17 -8
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +19 -4
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +28 -0
- package/dist/cli/handlers/change-set.d.ts +10 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.js +51 -1
- package/dist/cli/handlers/fan-out.d.ts +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +14 -3
- package/dist/cli/handlers/pr.d.ts +8 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +148 -24
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +256 -21
- package/dist/cli/handlers/release.d.ts +34 -0
- package/dist/cli/handlers/release.d.ts.map +1 -0
- package/dist/cli/handlers/release.js +203 -0
- package/dist/cli/handlers/run.d.ts +28 -2
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +170 -13
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +88 -13
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +49 -3
- package/dist/cli/registry.d.ts +29 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +29 -0
- package/dist/cli/run-events.d.ts.map +1 -0
- package/dist/cli/run-events.js +47 -0
- package/dist/cli/version.d.ts +3 -5
- package/dist/cli/version.d.ts.map +1 -1
- package/dist/cli/version.js +3 -13
- package/dist/codegen/docs-in-site.d.ts +71 -0
- package/dist/codegen/docs-in-site.d.ts.map +1 -0
- package/dist/codegen/docs-in-site.js +177 -0
- package/dist/codegen/docs-types.d.ts +14 -1
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +18 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/docs.js +40 -7
- package/dist/codegen/rehype-base-url.mjs +15 -9
- package/dist/components/auto-release.d.ts +4 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +2 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +10 -9
- package/dist/components/driver.d.ts +42 -9
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +81 -54
- package/dist/components/fan-out-run.d.ts +7 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +35 -26
- package/dist/components/pr-run.d.ts +11 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +20 -1
- package/dist/components/promote-order.d.ts +38 -0
- package/dist/components/promote-order.d.ts.map +1 -0
- package/dist/components/promote-order.js +62 -0
- package/dist/components/promote.d.ts +15 -2
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +17 -0
- package/dist/components/release-compile.d.ts +133 -0
- package/dist/components/release-compile.d.ts.map +1 -0
- package/dist/components/release-compile.js +376 -0
- package/dist/components/release-gate.d.ts +125 -0
- package/dist/components/release-gate.d.ts.map +1 -0
- package/dist/components/release-gate.js +282 -0
- package/dist/components/release-promote.d.ts +96 -0
- package/dist/components/release-promote.d.ts.map +1 -0
- package/dist/components/release-promote.js +204 -0
- package/dist/components/run-progress.d.ts +7 -0
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +3 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lifecycle/change-set-store.d.ts +67 -0
- package/dist/lifecycle/change-set-store.d.ts.map +1 -0
- package/dist/lifecycle/change-set-store.js +96 -0
- package/dist/lifecycle/commit-sha.d.ts +13 -0
- package/dist/lifecycle/commit-sha.d.ts.map +1 -0
- package/dist/lifecycle/commit-sha.js +21 -0
- package/dist/lifecycle/gate-change-set.d.ts +69 -0
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
- package/dist/lifecycle/gate-change-set.js +106 -0
- package/dist/lifecycle/gate-ledger.d.ts +49 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +18 -0
- package/dist/lifecycle/release-cut.d.ts +107 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -0
- package/dist/lifecycle/release-cut.js +273 -0
- package/dist/lifecycle/release-ledger.d.ts +54 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +53 -20
- package/dist/lifecycle/release-store.d.ts +190 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -0
- package/dist/lifecycle/release-store.js +318 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +2 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.js +4 -2
- package/dist/op/activities/source-release.d.ts.map +1 -1
- package/dist/op/activities/source-release.js +7 -3
- package/dist/op/activities/source-rollback.d.ts.map +1 -1
- package/dist/op/activities/source-rollback.js +7 -3
- package/dist/op/activity-registry.d.ts +6 -3
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.js +7 -5
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +14 -7
- package/dist/op/gate-name.d.ts +11 -2
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-name.js +12 -3
- package/dist/op/gate-run-locator.d.ts +22 -0
- package/dist/op/gate-run-locator.d.ts.map +1 -1
- package/dist/op/gate-run-locator.js +31 -0
- package/dist/op/gate.d.ts +27 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +57 -3
- package/dist/op/local-executor.d.ts +36 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +158 -52
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -0
- package/dist/op/run-live.d.ts +2 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/run-live.js +1 -1
- package/dist/op/runtime.d.ts +46 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +12 -2
- package/dist/point-decision.d.ts +2 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +9 -2
- package/dist/run-event-store.d.ts +77 -0
- package/dist/run-event-store.d.ts.map +1 -0
- package/dist/run-event-store.js +204 -0
- package/dist/run-event-stream.d.ts +132 -0
- package/dist/run-event-stream.d.ts.map +1 -0
- package/dist/run-event-stream.js +238 -0
- package/dist/run-event.d.ts +267 -0
- package/dist/run-event.d.ts.map +1 -0
- package/dist/run-event.js +207 -0
- package/dist/wave-runtime.mjs +225 -242
- package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
- package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/run-event-check.js +59 -0
- package/dist/workspace/change-set-show.d.ts +56 -0
- package/dist/workspace/change-set-show.d.ts.map +1 -0
- package/dist/workspace/change-set-show.js +54 -0
- package/dist/workspace/change-set-show.schema.json +102 -0
- package/dist/workspace/change-set.schema.json +28 -1
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes-cli.js +10 -1
- package/dist/workspace/changes.d.ts +37 -0
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/changes.js +49 -2
- package/dist/workspace/changes.schema.json +135 -0
- package/dist/workspace/ci-green-cli.d.ts +19 -4
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +75 -12
- package/dist/workspace/ci-green-forge.d.ts +20 -3
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +6 -4
- package/dist/workspace/ci-green-workflow.d.ts +11 -0
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +6 -0
- package/dist/workspace/ci-green.d.ts +273 -13
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +449 -38
- package/dist/workspace/ci-last-green.schema.json +32 -2
- package/dist/workspace/ci-list.schema.json +525 -0
- package/dist/workspace/commit-links.d.ts +85 -0
- package/dist/workspace/commit-links.d.ts.map +1 -0
- package/dist/workspace/commit-links.js +163 -0
- package/dist/workspace/declaration.d.ts +19 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +1 -1
- package/dist/workspace/declaration.schema.json +12 -0
- package/dist/workspace/identity-gate-rule.d.ts +37 -0
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +72 -7
- package/dist/workspace/identity.d.ts +12 -0
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/identity.js +13 -1
- package/dist/workspace/intent.d.ts +6 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +51 -1
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +2 -0
- package/dist/workspace/ls.schema.json +3 -1
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +6 -0
- package/dist/workspace/promotion-order.d.ts +41 -0
- package/dist/workspace/promotion-order.d.ts.map +1 -0
- package/dist/workspace/promotion-order.js +95 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +4 -1
- package/dist/workspace/releases.d.ts +72 -0
- package/dist/workspace/releases.d.ts.map +1 -0
- package/dist/workspace/releases.js +94 -0
- package/dist/workspace/releases.schema.json +391 -0
- package/dist/workspace/run-event.schema.json +288 -0
- package/dist/workspace/status-gates.d.ts +26 -1
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +31 -2
- package/dist/workspace/status-stewards.d.ts +4 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +2 -0
- package/dist/workspace/status.d.ts +39 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +48 -1
- package/dist/workspace/status.schema.json +70 -1
- package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
- package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
- package/dist/workspace/trust/ci-tag-statement.js +123 -0
- package/dist/workspace/trust/op-run-statement.d.ts +91 -0
- package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/op-run-statement.js +143 -0
- package/dist/workspace/trust/rotation.d.ts +18 -2
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.js +65 -16
- package/dist/workspace/trust/seal.d.ts +11 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +9 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/ssh-commit.js +10 -10
- package/package.json +1 -1
- package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/src/__golden__/wave-loop/fan-out-only.json +6 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/src/__golden__/wave-loop/op-cost-member.json +3 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/src/__golden__/wave-loop/op-held-on.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/src/__golden__/wave-loop/op-per-run.json +7 -0
- package/src/__golden__/wave-loop/op-pr-review.json +7 -0
- package/src/__golden__/wave-loop/op-sealed.json +3 -0
- package/src/__golden__/wave-loop/op-spend.json +3 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/src/change-set-graph.test.ts +72 -0
- package/src/change-set-graph.ts +133 -0
- package/src/change-set.ts +76 -6
- package/src/chant-version.ts +16 -0
- package/src/cli/commands/build.ts +19 -8
- package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
- package/src/cli/commands/check-lexicon-docs.ts +39 -2
- package/src/cli/commands/check-lexicon.ts +14 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
- package/src/cli/commands/init-lexicon.test.ts +25 -0
- package/src/cli/commands/init-lexicon.ts +18 -8
- package/src/cli/compiled-cli.e2e.test.ts +31 -6
- package/src/cli/handlers/approve-release.test.ts +80 -0
- package/src/cli/handlers/approve.ts +18 -2
- package/src/cli/handlers/build.test.ts +52 -0
- package/src/cli/handlers/build.ts +29 -0
- package/src/cli/handlers/change-set.ts +49 -1
- package/src/cli/handlers/fan-out.test.ts +48 -0
- package/src/cli/handlers/fan-out.ts +15 -3
- package/src/cli/handlers/operator.test.ts +37 -0
- package/src/cli/handlers/pr.test.ts +194 -0
- package/src/cli/handlers/pr.ts +168 -34
- package/src/cli/handlers/promote.test.ts +389 -5
- package/src/cli/handlers/promote.ts +279 -20
- package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
- package/src/cli/handlers/release.e2e.test.ts +85 -0
- package/src/cli/handlers/release.ts +206 -0
- package/src/cli/handlers/run-auto-release.test.ts +71 -0
- package/src/cli/handlers/run-events-cmd.test.ts +92 -0
- package/src/cli/handlers/run.test.ts +30 -0
- package/src/cli/handlers/run.ts +176 -15
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +88 -13
- package/src/cli/mcp/never-over-mcp.test.ts +2 -0
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +37 -3
- package/src/cli/registry.ts +29 -3
- package/src/cli/run-events.ts +54 -0
- package/src/cli/version.ts +3 -13
- package/src/codegen/docs-in-site.test.ts +166 -0
- package/src/codegen/docs-in-site.ts +182 -0
- package/src/codegen/docs-site/AgentPrompt.astro +26 -1
- package/src/codegen/docs-types.ts +14 -1
- package/src/codegen/docs.ts +60 -7
- package/src/codegen/rehype-base-url.d.mts +3 -1
- package/src/codegen/rehype-base-url.mjs +15 -9
- package/src/codegen/rehype-base-url.test.ts +34 -0
- package/src/components/auto-release.test.ts +21 -0
- package/src/components/auto-release.ts +6 -1
- package/src/components/cli-support.test.ts +82 -0
- package/src/components/cli-support.ts +16 -9
- package/src/components/driver.ts +129 -56
- package/src/components/fan-out-run.ts +56 -35
- package/src/components/pr-run.ts +30 -1
- package/src/components/promote-order.test.ts +90 -0
- package/src/components/promote-order.ts +75 -0
- package/src/components/promote.test.ts +10 -1
- package/src/components/promote.ts +26 -1
- package/src/components/release-compile.test.ts +147 -0
- package/src/components/release-compile.ts +445 -0
- package/src/components/release-gate.test.ts +215 -0
- package/src/components/release-gate.ts +341 -0
- package/src/components/release-promote.test.ts +159 -0
- package/src/components/release-promote.ts +232 -0
- package/src/components/run-progress.ts +7 -0
- package/src/components/wave-plan.ts +1 -1
- package/src/config.ts +7 -1
- package/src/index.ts +2 -0
- package/src/lifecycle/change-set-store.test.ts +217 -0
- package/src/lifecycle/change-set-store.ts +112 -0
- package/src/lifecycle/commit-sha.ts +23 -0
- package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
- package/src/lifecycle/gate-change-set.ts +133 -0
- package/src/lifecycle/gate-ledger.test.ts +33 -0
- package/src/lifecycle/gate-ledger.ts +66 -1
- package/src/lifecycle/release-cut-signed.test.ts +180 -0
- package/src/lifecycle/release-cut.test.ts +411 -0
- package/src/lifecycle/release-cut.ts +367 -0
- package/src/lifecycle/release-ledger.test.ts +14 -0
- package/src/lifecycle/release-ledger.ts +85 -17
- package/src/lifecycle/release-store.ts +423 -0
- package/src/lifecycle/run-ledger.test.ts +10 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/op/activities/change-coverage.ts +4 -1
- package/src/op/activities/source-release.ts +8 -3
- package/src/op/activities/source-rollback.ts +8 -4
- package/src/op/activity-registry.ts +7 -5
- package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
- package/src/op/discover-root.test.ts +39 -0
- package/src/op/discover.ts +15 -8
- package/src/op/gate-name.ts +13 -3
- package/src/op/gate-release.test.ts +126 -0
- package/src/op/gate-run-locator.test.ts +110 -0
- package/src/op/gate-run-locator.ts +35 -0
- package/src/op/gate.ts +88 -6
- package/src/op/local-executor.ts +207 -52
- package/src/op/op-waves-run.ts +4 -0
- package/src/op/run-events.test.ts +166 -0
- package/src/op/run-live.ts +1 -1
- package/src/op/runtime.ts +39 -0
- package/src/op/runtimes/local.test.ts +16 -0
- package/src/op/runtimes/local.ts +13 -2
- package/src/point-decision.ts +12 -2
- package/src/run-event-store.test.ts +144 -0
- package/src/run-event-store.ts +213 -0
- package/src/run-event-stream.test.ts +175 -0
- package/src/run-event-stream.ts +306 -0
- package/src/run-event.test.ts +277 -0
- package/src/run-event.ts +451 -0
- package/src/wave-loop-golden.test.ts +9 -0
- package/src/workspace/__fixtures__/run-event-check.ts +58 -0
- package/src/workspace/ancestry-index.test.ts +93 -0
- package/src/workspace/change-set-show.schema.json +102 -0
- package/src/workspace/change-set-show.ts +72 -0
- package/src/workspace/change-set.schema.json +28 -1
- package/src/workspace/changes-cli.ts +10 -1
- package/src/workspace/changes.schema.json +135 -0
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/changes.ts +84 -2
- package/src/workspace/ci-green-cli.ts +75 -13
- package/src/workspace/ci-green-forge.ts +24 -4
- package/src/workspace/ci-green-runs.test.ts +242 -0
- package/src/workspace/ci-green-signed.test.ts +302 -0
- package/src/workspace/ci-green-workflow.test.ts +16 -0
- package/src/workspace/ci-green-workflow.ts +12 -0
- package/src/workspace/ci-green.test.ts +131 -2
- package/src/workspace/ci-green.ts +624 -42
- package/src/workspace/ci-last-green.schema.json +32 -2
- package/src/workspace/ci-list.schema.json +525 -0
- package/src/workspace/commit-links.test.ts +279 -0
- package/src/workspace/commit-links.ts +183 -0
- package/src/workspace/declaration.schema.json +12 -0
- package/src/workspace/declaration.ts +22 -2
- package/src/workspace/identity-gate-rule.ts +91 -7
- package/src/workspace/identity.ts +14 -1
- package/src/workspace/intent.ts +48 -1
- package/src/workspace/ls.schema.json +3 -1
- package/src/workspace/ls.ts +6 -0
- package/src/workspace/member-pipeline.test.ts +17 -0
- package/src/workspace/member-pipeline.ts +9 -0
- package/src/workspace/promotion-order.ts +97 -0
- package/src/workspace/read-contract.e2e.test.ts +4 -1
- package/src/workspace/reason-codes.test.ts +2 -1
- package/src/workspace/reason-codes.ts +4 -1
- package/src/workspace/releases.schema.json +391 -0
- package/src/workspace/releases.test.ts +108 -0
- package/src/workspace/releases.ts +122 -0
- package/src/workspace/run-event.schema.json +288 -0
- package/src/workspace/status-contract.test.ts +52 -1
- package/src/workspace/status-gates.ts +56 -1
- package/src/workspace/status-stewards.ts +6 -0
- package/src/workspace/status.schema.json +70 -1
- package/src/workspace/status.ts +80 -1
- package/src/workspace/trust/ci-tag-statement.ts +151 -0
- package/src/workspace/trust/op-run-statement.ts +148 -0
- package/src/workspace/trust/rotation.ts +61 -14
- package/src/workspace/trust/seal.ts +10 -0
- package/src/workspace/trust/ssh-commit.ts +10 -9
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A promote under a release (#4026, ws-104 question 5): the deploy reads the
|
|
3
|
+
* templates the release recorded for the target environment, not the ones in
|
|
4
|
+
* the promoting checkout.
|
|
5
|
+
*
|
|
6
|
+
* Each template a promoted component's deploy names ({@link templateRefs}) has
|
|
7
|
+
* to be one the release's target records for that component. Its bytes are
|
|
8
|
+
* read from `_releases/templates/` on `chant/lifecycle` and checked against
|
|
9
|
+
* the target's digest, the way a promote checks the publish digest, and a
|
|
10
|
+
* mismatch refuses the promote before anything runs. The bytes are written
|
|
11
|
+
* under `.chant/release-templates/<digest>/` in the member's directory and the
|
|
12
|
+
* step reads them from there. The path is relative and fixed by the digest,
|
|
13
|
+
* so a gate approval bound to the promote's plan still matches on a re-run.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { basename, dirname, join } from "node:path";
|
|
18
|
+
import { contentDigest } from "../content-digest";
|
|
19
|
+
import type { DriverComponent, DriverGate, DriverPhase, DriverStep } from "./driver";
|
|
20
|
+
import { TEMPLATE_FIELDS, templateRefs } from "./release-compile";
|
|
21
|
+
import type { Release, ReleaseTarget } from "../lifecycle/release-store";
|
|
22
|
+
import type { ReleaseRecord } from "../lifecycle/release-ledger";
|
|
23
|
+
import type { BuildArchiveManifest } from "./verbs/build-archive";
|
|
24
|
+
|
|
25
|
+
/** Where a promote under a release writes the recorded templates, relative to the member's directory. */
|
|
26
|
+
export const RELEASE_TEMPLATES_DIR = ".chant/release-templates";
|
|
27
|
+
|
|
28
|
+
/** A template ready to write: where, and its bytes. */
|
|
29
|
+
export interface PinnedTemplate {
|
|
30
|
+
/** Relative to the member's directory. */
|
|
31
|
+
path: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The release's target for `env`, or an error naming the environments it has. */
|
|
36
|
+
export function targetFor(release: Release, env: string): { target: ReleaseTarget } | { error: string } {
|
|
37
|
+
const target = release.targets.find((t) => t.env === env);
|
|
38
|
+
if (target) return { target };
|
|
39
|
+
return {
|
|
40
|
+
error: `release ${release.releaseDigest} has no target for "${env}"; it was compiled for ${release.targets.map((t) => t.env).join(", ") || "no environment"}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether a release is of a project with no components (#4046): no artifact, and every template it records has `component: null`. */
|
|
45
|
+
export function isImplicitRelease(release: Release): boolean {
|
|
46
|
+
const templates = release.targets.flatMap((t) => t.templates);
|
|
47
|
+
return release.artifactDigests.length === 0 && templates.length > 0 && templates.every((t) => t.component === null);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
|
|
51
|
+
return (step as { kind?: unknown }).kind === "gate";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
|
|
55
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Point every template `components` read at the target's recorded bytes.
|
|
60
|
+
* Returns the rewritten compositions and the files to write, or the reasons
|
|
61
|
+
* the promote is refused: a template the target does not record, one whose
|
|
62
|
+
* bytes are missing, or one whose bytes do not hash to the target's digest.
|
|
63
|
+
*/
|
|
64
|
+
export async function pinReleaseTemplates(
|
|
65
|
+
components: readonly DriverComponent[],
|
|
66
|
+
release: Release,
|
|
67
|
+
target: ReleaseTarget,
|
|
68
|
+
readTemplate: (digest: string) => Promise<string | null>,
|
|
69
|
+
): Promise<{ components: DriverComponent[]; files: PinnedTemplate[] } | { errors: string[] }> {
|
|
70
|
+
const errors: string[] = [];
|
|
71
|
+
const files: PinnedTemplate[] = [];
|
|
72
|
+
const out: DriverComponent[] = [];
|
|
73
|
+
for (const component of components) {
|
|
74
|
+
const replace = new Map<string, string>();
|
|
75
|
+
for (const ref of templateRefs(component)) {
|
|
76
|
+
if (replace.has(ref.value)) continue;
|
|
77
|
+
const recorded = target.templates.find((t) => t.component === component.name && t.path === ref.path);
|
|
78
|
+
if (!recorded) {
|
|
79
|
+
errors.push(
|
|
80
|
+
`component "${component.name}" reads ${ref.field} "${ref.value}", which release ${release.releaseDigest} does not record for "${target.env}"; ` +
|
|
81
|
+
`this checkout's deploy is not the one the release was compiled from (commit ${release.commit.slice(0, 12)})`,
|
|
82
|
+
);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const content = await readTemplate(recorded.digest);
|
|
86
|
+
if (content === null) {
|
|
87
|
+
errors.push(`component "${component.name}": the bytes of template "${ref.path}" (${recorded.digest}) are not on the chant/lifecycle branch; fetch it and try again`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const got = contentDigest(content);
|
|
91
|
+
if (got !== recorded.digest) {
|
|
92
|
+
errors.push(
|
|
93
|
+
`component "${component.name}": template "${ref.path}" hashes to ${got}, but the release's target for "${target.env}" records ${recorded.digest}; refusing to deploy it`,
|
|
94
|
+
);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const path = join(RELEASE_TEMPLATES_DIR, recorded.digest.replace(/:/g, "_"), basename(ref.path));
|
|
98
|
+
files.push({ path, content });
|
|
99
|
+
replace.set(ref.value, path);
|
|
100
|
+
}
|
|
101
|
+
const rewrite = (phase: DriverPhase): DriverPhase => ({
|
|
102
|
+
...phase,
|
|
103
|
+
steps: phase.steps.map((step) => {
|
|
104
|
+
if (isPhase(step)) return rewrite(step);
|
|
105
|
+
if (isGate(step)) return step;
|
|
106
|
+
const next: DriverStep = { ...step };
|
|
107
|
+
for (const field of TEMPLATE_FIELDS) {
|
|
108
|
+
const v = next[field];
|
|
109
|
+
if (typeof v === "string" && replace.has(v)) next[field] = replace.get(v);
|
|
110
|
+
else if (Array.isArray(v)) next[field] = v.map((x) => (typeof x === "string" && replace.has(x) ? replace.get(x) : x));
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
out.push({ ...component, deploy: component.deploy.map(rewrite) });
|
|
116
|
+
}
|
|
117
|
+
return errors.length > 0 ? { errors } : { components: out, files };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The files a release's target records, read back by digest and checked, for
|
|
122
|
+
* `chant release export` (#4046). A project with no components has no deploy
|
|
123
|
+
* step for a promote to point at the recorded bytes, so its deploy (an Op such
|
|
124
|
+
* as `chant run deploy`, or a CI job) reads the files this writes instead of
|
|
125
|
+
* building them again. Each path is the one the build wrote, relative to the
|
|
126
|
+
* member's directory; a template whose bytes are missing or don't hash to the
|
|
127
|
+
* target's digest is refused, as {@link pinReleaseTemplates} refuses one.
|
|
128
|
+
*/
|
|
129
|
+
export async function targetFiles(
|
|
130
|
+
release: Release,
|
|
131
|
+
target: ReleaseTarget,
|
|
132
|
+
readTemplate: (digest: string) => Promise<string | null>,
|
|
133
|
+
): Promise<{ files: PinnedTemplate[] } | { errors: string[] }> {
|
|
134
|
+
const errors: string[] = [];
|
|
135
|
+
const files: PinnedTemplate[] = [];
|
|
136
|
+
for (const t of target.templates) {
|
|
137
|
+
const who = t.component === null ? "" : `component "${t.component}": `;
|
|
138
|
+
if (t.path.startsWith("/") || t.path.split("/").includes("..")) {
|
|
139
|
+
errors.push(`${who}template "${t.path}" is not a path inside the member's directory; refusing to write it`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const content = await readTemplate(t.digest);
|
|
143
|
+
if (content === null) {
|
|
144
|
+
errors.push(`${who}the bytes of template "${t.path}" (${t.digest}) are not on the chant/lifecycle branch; fetch it and try again`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const got = contentDigest(content);
|
|
148
|
+
if (got !== t.digest) {
|
|
149
|
+
errors.push(`${who}template "${t.path}" hashes to ${got}, but release ${release.releaseDigest} records ${t.digest} for "${target.env}"; refusing to write it`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const same = files.find((f) => f.path === t.path);
|
|
153
|
+
if (same) {
|
|
154
|
+
if (same.content !== content) errors.push(`two components record different bytes at "${t.path}" for "${target.env}", so they can't both be written there`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
files.push({ path: t.path, content });
|
|
158
|
+
}
|
|
159
|
+
return errors.length > 0 ? { errors } : { files };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Write the pinned templates under `dir`, the member's directory. */
|
|
163
|
+
export function writePinnedTemplates(dir: string, files: readonly PinnedTemplate[]): void {
|
|
164
|
+
for (const f of files) {
|
|
165
|
+
const full = join(dir, f.path);
|
|
166
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
167
|
+
writeFileSync(full, f.content);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Remove the templates {@link writePinnedTemplates} wrote once the promote is
|
|
173
|
+
* done, and {@link RELEASE_TEMPLATES_DIR} with them when nothing else is in
|
|
174
|
+
* it, so a promote leaves the checkout as it found it. A re-run writes them
|
|
175
|
+
* again from the release.
|
|
176
|
+
*/
|
|
177
|
+
export function removePinnedTemplates(dir: string, files: readonly PinnedTemplate[]): void {
|
|
178
|
+
for (const f of files) rmSync(dirname(join(dir, f.path)), { recursive: true, force: true });
|
|
179
|
+
const root = join(dir, RELEASE_TEMPLATES_DIR);
|
|
180
|
+
try {
|
|
181
|
+
if (readdirSync(root).length === 0) rmdirSync(root);
|
|
182
|
+
} catch {
|
|
183
|
+
// Already gone, or not ours to remove.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whether the artifact a promote deploys under a release is one the release
|
|
189
|
+
* built (#4045, the rule #4026 left open). The release records build digests
|
|
190
|
+
* (`artifactDigests`, the digest the archive step returned), and a deploy
|
|
191
|
+
* record carries the publish digest, which is another value for an image, so
|
|
192
|
+
* the two are compared through the build manifest the source deploy record
|
|
193
|
+
* names (`manifestDigest`), whose entries carry build digests at the same
|
|
194
|
+
* archive paths:
|
|
195
|
+
*
|
|
196
|
+
* - a source record that names this release in `releaseDigest` matches;
|
|
197
|
+
* - else, each of the release's artifacts for the component must be in that
|
|
198
|
+
* manifest at its path with the release's digest, and one that differs is
|
|
199
|
+
* a mismatch;
|
|
200
|
+
* - a record with no `manifestDigest`, a manifest that isn't stored, or an
|
|
201
|
+
* artifact path the manifest doesn't hold can't be compared.
|
|
202
|
+
*
|
|
203
|
+
* A mismatch is a warning, not a refusal. The environment promoted from was
|
|
204
|
+
* deployed by its own `run --components`, which builds again, and an image
|
|
205
|
+
* build is not reproducible, so its digest differs from the cut's for the
|
|
206
|
+
* same commit. Refusing would refuse every image promote until a deploy can
|
|
207
|
+
* run the release's own artifacts (#4043).
|
|
208
|
+
*/
|
|
209
|
+
export async function releaseArtifactMismatches(
|
|
210
|
+
release: Release,
|
|
211
|
+
sources: ReadonlyArray<{ component: string; source: ReleaseRecord }>,
|
|
212
|
+
readManifest: (manifestDigest: string) => Promise<BuildArchiveManifest | null>,
|
|
213
|
+
): Promise<string[]> {
|
|
214
|
+
const out: string[] = [];
|
|
215
|
+
for (const { component, source } of sources) {
|
|
216
|
+
if (source.releaseDigest === release.releaseDigest) continue;
|
|
217
|
+
const artifacts = release.artifactDigests.filter((a) => a.component === component);
|
|
218
|
+
if (artifacts.length === 0 || source.manifestDigest === undefined) continue;
|
|
219
|
+
const manifest = await readManifest(source.manifestDigest);
|
|
220
|
+
if (!manifest) continue;
|
|
221
|
+
for (const a of artifacts) {
|
|
222
|
+
const entry = manifest.contents.find((e) => e.path === a.path && e.kind === a.kind);
|
|
223
|
+
if (entry && entry.digest !== a.digest) {
|
|
224
|
+
out.push(
|
|
225
|
+
`${component}: the ${source.env} deploy being promoted was built with ${a.path} at ${entry.digest}, ` +
|
|
226
|
+
`but release ${release.releaseDigest} built it at ${a.digest}, so the artifact deployed is not the release's own`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
@@ -90,6 +90,13 @@ export interface RunDoneEvent {
|
|
|
90
90
|
status: RunProgressStatus;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @deprecated Read `RunEvent` (`../run-event.ts`, exported from
|
|
95
|
+
* `@intentius/chant`) instead: one contract for Op, component and pipeline
|
|
96
|
+
* runs, with the run id, commit and runtime on every event (#4019).
|
|
97
|
+
* `runEventOfProgress` maps each of these onto it. `--progress-json` keeps
|
|
98
|
+
* printing this shape until #4041 moves it to the run-event stream.
|
|
99
|
+
*/
|
|
93
100
|
export type RunProgressEvent =
|
|
94
101
|
| RunStartEvent
|
|
95
102
|
| WaveStartEvent
|
|
@@ -44,7 +44,7 @@ function emptyPart(member: WaveMember): ChangeSetPart {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Every capability step in a composition, nested phases included, gates left out. */
|
|
47
|
-
function capabilitySteps(phases: readonly DriverPhase[]): DriverStep[] {
|
|
47
|
+
export function capabilitySteps(phases: readonly DriverPhase[]): DriverStep[] {
|
|
48
48
|
const out: DriverStep[] = [];
|
|
49
49
|
const walk = (steps: DriverPhase["steps"]): void => {
|
|
50
50
|
for (const step of steps) {
|
package/src/config.ts
CHANGED
|
@@ -22,8 +22,13 @@ import { decideConfigSchema, type DecideConfig } from "./op/decide-config";
|
|
|
22
22
|
* silently querying real AWS. See {@link environmentName}, {@link
|
|
23
23
|
* environmentNames}, {@link environmentEndpoint} below, and
|
|
24
24
|
* `./live-endpoint.ts`'s `applyLiveEndpoint`, the CLI-side consumer.
|
|
25
|
+
*
|
|
26
|
+
* The array's order is the member's promotion order (#4030, see
|
|
27
|
+
* `./workspace/promotion-order.ts`), and `canary: true` marks a step as the
|
|
28
|
+
* canary step of that order. The canary is declared and surfaced; chant does
|
|
29
|
+
* not yet act on it.
|
|
25
30
|
*/
|
|
26
|
-
export type EnvironmentDeclaration = string | { name: string; endpoint?: string };
|
|
31
|
+
export type EnvironmentDeclaration = string | { name: string; endpoint?: string; canary?: boolean };
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* `ownership.env`: a literal environment identity, or a reference to a
|
|
@@ -124,6 +129,7 @@ const EnvironmentEntrySchema = z.union([
|
|
|
124
129
|
z.object({
|
|
125
130
|
name: z.string().min(1),
|
|
126
131
|
endpoint: z.string().min(1).optional(),
|
|
132
|
+
canary: z.boolean().optional(),
|
|
127
133
|
}),
|
|
128
134
|
]);
|
|
129
135
|
|
package/src/index.ts
CHANGED
|
@@ -153,6 +153,8 @@ export type { SymbolDeclaration, SymbolResolver } from "./workspace/symbols";
|
|
|
153
153
|
export * from "./lsp/lexicon-providers";
|
|
154
154
|
export * from "./mcp/types";
|
|
155
155
|
export * from "./lifecycle/index";
|
|
156
|
+
// The run-event contract (#4019): the types and the one writer of a run's stream.
|
|
157
|
+
export * from "./run-event";
|
|
156
158
|
// Op builders — use explicit exports to avoid collision with the core `build` function
|
|
157
159
|
export { Op, phase, activity, gate, kubectlApply, helmInstall, waitForStack,
|
|
158
160
|
gitlabPipeline, lifecycleSnapshot, shell, teardown, OpResource } from "./op/index";
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stored change sets (#4022) against temp git repos: the store, the gate's
|
|
3
|
+
* call that maps and stores, `chant change-set show`'s document and the link
|
|
4
|
+
* a gate in `workspace status` carries. Graph reads are injected here;
|
|
5
|
+
* `gate-change-set.e2e.test.ts` reads a real one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { composeChangeSet, lifecyclePlanPart, type ChangeSetDocument } from "../change-set";
|
|
14
|
+
import type { ChangeSet as LifecycleChangeSet } from "./change-set";
|
|
15
|
+
import { CHANGE_SETS_DIR, changeSetFile, ChangeSetStoreError, persistChangeSet, readStoredChangeSet } from "./change-set-store";
|
|
16
|
+
import { storeGateChangeSet } from "./gate-change-set";
|
|
17
|
+
import { appendPendingGate } from "./gate-ledger";
|
|
18
|
+
import { writeLedgerFiles } from "./git";
|
|
19
|
+
import { clearMemberLedgerCache } from "./member-ledger";
|
|
20
|
+
import { changeSetShowDocument } from "../workspace/change-set-show";
|
|
21
|
+
import { readMemberGates } from "../workspace/status-gates";
|
|
22
|
+
import { loadAjv2020 } from "../workspace/ajv2020";
|
|
23
|
+
import showSchema from "../workspace/change-set-show.schema.json";
|
|
24
|
+
import changeSetSchema from "../workspace/change-set.schema.json";
|
|
25
|
+
|
|
26
|
+
const plan = JSON.parse(readFileSync(join(import.meta.dirname, "..", "__fixtures__", "change-set", "lifecycle-plan.floci.json"), "utf8")) as LifecycleChangeSet;
|
|
27
|
+
const doc = (): ChangeSetDocument => composeChangeSet([lifecyclePlanPart({ member: "queues", plan })]);
|
|
28
|
+
|
|
29
|
+
function git(dir: string, ...args: string[]): string {
|
|
30
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
31
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
32
|
+
return r.stdout.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function initRepo(dir: string): string {
|
|
36
|
+
git(dir, "init", "-q", "-b", "main");
|
|
37
|
+
git(dir, "config", "user.email", "test@chant.dev");
|
|
38
|
+
git(dir, "config", "user.name", "Test");
|
|
39
|
+
writeFileSync(join(dir, "README.md"), "fixture\n");
|
|
40
|
+
git(dir, "add", ".");
|
|
41
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
42
|
+
return git(dir, "rev-parse", "HEAD");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validator(schema: object) {
|
|
46
|
+
const Ajv = loadAjv2020();
|
|
47
|
+
const ajv = new Ajv({ strict: false, allErrors: true }) as unknown as { compile(s: object): ((d: unknown) => boolean) & { errors?: unknown } };
|
|
48
|
+
return ajv.compile(schema);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("persistChangeSet and readStoredChangeSet", () => {
|
|
52
|
+
test("stores a document under its digest once, and reads it back", async () => {
|
|
53
|
+
await withTestDir(async (dir) => {
|
|
54
|
+
initRepo(dir);
|
|
55
|
+
const d = doc();
|
|
56
|
+
expect(await readStoredChangeSet(d.digest, { cwd: dir })).toEqual({ status: "missing" });
|
|
57
|
+
const first = await persistChangeSet(d, { cwd: dir });
|
|
58
|
+
expect(first.written).toBe(true);
|
|
59
|
+
expect(git(dir, "ls-tree", "--name-only", `chant/lifecycle:${CHANGE_SETS_DIR}`)).toBe(changeSetFile(d.digest));
|
|
60
|
+
expect(changeSetFile(d.digest)).not.toContain(":");
|
|
61
|
+
const again = await persistChangeSet(d, { cwd: dir });
|
|
62
|
+
expect(again).toEqual({ commit: null, written: false });
|
|
63
|
+
const read = await readStoredChangeSet(d.digest, { cwd: dir });
|
|
64
|
+
expect(read).toEqual({ status: "found", document: JSON.parse(JSON.stringify(d)) });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("refuses a document that does not hash to its digest", async () => {
|
|
69
|
+
await withTestDir(async (dir) => {
|
|
70
|
+
initRepo(dir);
|
|
71
|
+
const d = doc();
|
|
72
|
+
const forged = { ...d, entries: d.entries.slice(1) };
|
|
73
|
+
await expect(persistChangeSet(forged, { cwd: dir })).rejects.toBeInstanceOf(ChangeSetStoreError);
|
|
74
|
+
await expect(persistChangeSet({ ...d, $schema: "x" } as never, { cwd: dir })).rejects.toBeInstanceOf(ChangeSetStoreError);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a file under a digest that is not that document reads as malformed", async () => {
|
|
79
|
+
await withTestDir(async (dir) => {
|
|
80
|
+
initRepo(dir);
|
|
81
|
+
const d = doc();
|
|
82
|
+
const other = { ...d, entries: d.entries.slice(1) };
|
|
83
|
+
await writeLedgerFiles([{ path: `${CHANGE_SETS_DIR}/${changeSetFile(d.digest)}`, content: JSON.stringify(other), expectPriorSha: null }], "forged", { cwd: dir });
|
|
84
|
+
const read = await readStoredChangeSet(d.digest, { cwd: dir });
|
|
85
|
+
expect(read.status).toBe("malformed");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("storeGateChangeSet", () => {
|
|
91
|
+
test("maps the entries onto the graph it reads, and stores in the member's ledger", async () => {
|
|
92
|
+
await withTestDir(async (dir) => {
|
|
93
|
+
initRepo(dir);
|
|
94
|
+
writeFileSync(join(dir, "chant.workspace.json"), JSON.stringify({ name: "acme", schema: 1, members: [{ name: "delivery", dir: "delivery", kind: "chant" }] }));
|
|
95
|
+
mkdirSync(join(dir, "delivery"));
|
|
96
|
+
clearMemberLedgerCache();
|
|
97
|
+
const d = doc();
|
|
98
|
+
const reads: string[] = [];
|
|
99
|
+
const stored = await storeGateChangeSet({
|
|
100
|
+
doc: d,
|
|
101
|
+
cwd: join(dir, "delivery"),
|
|
102
|
+
at: "c".repeat(40),
|
|
103
|
+
readGraph: async (at) => {
|
|
104
|
+
reads.push(at);
|
|
105
|
+
return { ids: ["jobs", "events", "dlq"] };
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
expect(stored).toEqual({ digest: d.digest, written: true, graph: { member: "delivery", at: "c".repeat(40) } });
|
|
109
|
+
expect(reads).toEqual([join(dir, "delivery")]);
|
|
110
|
+
expect(git(dir, "ls-tree", "-r", "--name-only", "chant/lifecycle")).toBe(`_members/delivery/${CHANGE_SETS_DIR}/${changeSetFile(d.digest)}`);
|
|
111
|
+
|
|
112
|
+
// Stored already: the graph is not read again.
|
|
113
|
+
expect(await storeGateChangeSet({ doc: d, cwd: join(dir, "delivery"), readGraph: async () => { throw new Error("read again"); } })).toEqual({ digest: d.digest, written: false });
|
|
114
|
+
|
|
115
|
+
// chant change-set show, from the workspace root with --member, and from the member's directory.
|
|
116
|
+
const shown = await changeSetShowDocument({ cwd: dir, digest: d.digest, member: ["delivery"] });
|
|
117
|
+
expect(validator(showSchema)(shown)).toBe(true);
|
|
118
|
+
if ("error" in shown) throw new Error(shown.error.message);
|
|
119
|
+
expect(shown.member).toBe("delivery");
|
|
120
|
+
expect(shown.reason).toBeNull();
|
|
121
|
+
expect(validator(changeSetSchema)(shown.changeSet)).toBe(true);
|
|
122
|
+
expect(shown.changeSet!.entries.map((e) => [e.address, e.node])).toEqual([
|
|
123
|
+
["alerts", null],
|
|
124
|
+
["dlq", "delivery/dlq"],
|
|
125
|
+
["events", "delivery/events"],
|
|
126
|
+
["jobs", "delivery/jobs"],
|
|
127
|
+
]);
|
|
128
|
+
const fromMember = await changeSetShowDocument({ cwd: join(dir, "delivery"), digest: d.digest });
|
|
129
|
+
expect(fromMember).toEqual(shown);
|
|
130
|
+
// The flat ledger holds nothing under that digest.
|
|
131
|
+
const flat = await changeSetShowDocument({ cwd: dir, digest: d.digest });
|
|
132
|
+
expect(flat).toMatchObject({ member: null, changeSet: null, reason: null });
|
|
133
|
+
expect(validator(showSchema)(flat)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a graph that can't be read stores the document with null nodes and says why", async () => {
|
|
138
|
+
await withTestDir(async (dir) => {
|
|
139
|
+
initRepo(dir);
|
|
140
|
+
clearMemberLedgerCache();
|
|
141
|
+
const d = doc();
|
|
142
|
+
const stored = await storeGateChangeSet({ doc: d, cwd: dir, readGraph: async () => ({ error: "chant graph --format ir exited 1" }) });
|
|
143
|
+
expect(stored.written).toBe(true);
|
|
144
|
+
expect(stored.graph).toBeNull();
|
|
145
|
+
expect(stored.warning).toContain("exited 1");
|
|
146
|
+
const read = await readStoredChangeSet(d.digest, { cwd: dir });
|
|
147
|
+
if (read.status !== "found") throw new Error(read.status);
|
|
148
|
+
expect(read.document.graph).toBeNull();
|
|
149
|
+
expect(read.document.entries.every((e) => e.node === null)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("show reports a malformed file and a checkout with no git", async () => {
|
|
154
|
+
await withTestDir(async (dir) => {
|
|
155
|
+
const outside = await changeSetShowDocument({ cwd: dir, digest: doc().digest });
|
|
156
|
+
expect(outside).toMatchObject({ error: { code: "not-a-git-repository" } });
|
|
157
|
+
expect(validator(showSchema)(outside)).toBe(true);
|
|
158
|
+
initRepo(dir);
|
|
159
|
+
clearMemberLedgerCache();
|
|
160
|
+
const d = doc();
|
|
161
|
+
await writeLedgerFiles([{ path: `${CHANGE_SETS_DIR}/${changeSetFile(d.digest)}`, content: "{", expectPriorSha: null }], "broken", { cwd: dir });
|
|
162
|
+
const shown = await changeSetShowDocument({ cwd: dir, digest: d.digest });
|
|
163
|
+
expect(shown).toMatchObject({ changeSet: null, reason: { code: "ledger-malformed" } });
|
|
164
|
+
expect(validator(showSchema)(shown)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("a gate in workspace status links to its stored change set", () => {
|
|
170
|
+
test("a gate whose planDigest names a stored change set carries changeSet; a component gate does not", async () => {
|
|
171
|
+
await withTestDir(async (dir) => {
|
|
172
|
+
initRepo(dir);
|
|
173
|
+
clearMemberLedgerCache();
|
|
174
|
+
const d = doc();
|
|
175
|
+
await storeGateChangeSet({ doc: d, cwd: dir, readGraph: async () => ({ ids: ["jobs"] }) });
|
|
176
|
+
const now = "2026-10-16T00:00:00.000Z";
|
|
177
|
+
const later = "2026-10-17T00:00:00.000Z";
|
|
178
|
+
await appendPendingGate({ op: "pr-7", gate: "pr-apply", planDigest: d.digest, timestamp: now, expiresAt: later }, { cwd: dir });
|
|
179
|
+
const lifecyclePlan = "jcs1-sha256:" + "0".repeat(64);
|
|
180
|
+
await appendPendingGate({ op: "api", gate: "prod-approval", planDigest: lifecyclePlan, environment: "prod", timestamp: now, expiresAt: later }, { cwd: dir });
|
|
181
|
+
const commit = git(dir, "rev-parse", "chant/lifecycle");
|
|
182
|
+
const { gates } = await readMemberGates("_gates", "flat", commit, ["prod"], dir, now);
|
|
183
|
+
const byOp = Object.fromEntries(gates.map((g) => [g.component, g]));
|
|
184
|
+
expect(byOp["pr-7"]).toMatchObject({ state: "pending", planDigest: d.digest, changeSet: d.digest });
|
|
185
|
+
expect(byOp.api.planDigest).toBe(lifecyclePlan);
|
|
186
|
+
expect(byOp.api).not.toHaveProperty("changeSet");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("a release gate's stored change set (#4045)", () => {
|
|
192
|
+
test("names the release and environment it was planned for, and a release gate in status names its release", async () => {
|
|
193
|
+
await withTestDir(async (dir) => {
|
|
194
|
+
initRepo(dir);
|
|
195
|
+
clearMemberLedgerCache();
|
|
196
|
+
const releaseDigest = "jcs1-sha256:" + "a".repeat(64);
|
|
197
|
+
const d = composeChangeSet([lifecyclePlanPart({ member: "queues", plan })], { planned: { releaseDigest, environment: "prod" } });
|
|
198
|
+
await storeGateChangeSet({ doc: d, cwd: dir, readGraph: async () => ({ ids: ["jobs"] }) });
|
|
199
|
+
const read = await readStoredChangeSet(d.digest, { cwd: dir });
|
|
200
|
+
if (read.status !== "found") throw new Error(read.status);
|
|
201
|
+
expect(read.document).toMatchObject({ releaseDigest, environment: "prod" });
|
|
202
|
+
expect(validator(changeSetSchema)(read.document)).toBe(true);
|
|
203
|
+
const shown = await changeSetShowDocument({ cwd: dir, digest: d.digest });
|
|
204
|
+
expect(shown).toMatchObject({ changeSet: { releaseDigest, environment: "prod" } });
|
|
205
|
+
expect(validator(showSchema)(shown)).toBe(true);
|
|
206
|
+
|
|
207
|
+
const now = "2026-10-16T00:00:00.000Z";
|
|
208
|
+
await appendPendingGate(
|
|
209
|
+
{ op: "release", gate: "prod-release", planDigest: d.digest, environment: "prod", releaseDigest, timestamp: now, expiresAt: "2026-10-17T00:00:00.000Z" },
|
|
210
|
+
{ cwd: dir },
|
|
211
|
+
);
|
|
212
|
+
const commit = git(dir, "rev-parse", "chant/lifecycle");
|
|
213
|
+
const { gates } = await readMemberGates("_gates", "flat", commit, ["prod"], dir, now);
|
|
214
|
+
expect(gates).toEqual([expect.objectContaining({ component: "release", releaseDigest, changeSet: d.digest, state: "pending" })]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stored change sets (#4022): each change-set document a gate binds, kept on
|
|
3
|
+
* `chant/lifecycle` under its digest so the plan an approval bound can be read
|
|
4
|
+
* once the run that planned it is gone.
|
|
5
|
+
*
|
|
6
|
+
* A pull-request gate, a release gate and an `empty` resolution (ws-104
|
|
7
|
+
* question 2 and 6) bind the change-set document's `digest`
|
|
8
|
+
* (`../change-set.ts`) in `planDigest`. Only that value reaches the gate
|
|
9
|
+
* ledger, so this store keeps the document beside it:
|
|
10
|
+
*
|
|
11
|
+
* _change-sets/<digest>.json the document, written once and never rewritten
|
|
12
|
+
*
|
|
13
|
+
* `:` is written as `_` in the file name, as `_plans/`, `_builds/` and
|
|
14
|
+
* `_releases/` do. A workspace member's copy sits under
|
|
15
|
+
* `_members/<member>/_change-sets/` through the same `ledgerDir` prefix the
|
|
16
|
+
* gate ledger uses, so a gate and its change set are read from one member.
|
|
17
|
+
*
|
|
18
|
+
* The stored document is the one the gate bound, with each entry's graph
|
|
19
|
+
* `node` (`../change-set-graph.ts`) and the `graph` it came from. Neither is
|
|
20
|
+
* hashed, so the stored document still verifies against its digest. A
|
|
21
|
+
* component gate in a plain `run --components` binds a lifecycle-plan digest
|
|
22
|
+
* and stores nothing here.
|
|
23
|
+
*
|
|
24
|
+
* How long a stored change set is kept is #4036's retention policy.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { sortedJsonReplacer } from "../utils";
|
|
28
|
+
import { CHANGE_SET_CONTRACT, CHANGE_SET_SCHEMA_ID, verifyChangeSetDigest, type ChangeSetDocument } from "../change-set";
|
|
29
|
+
import { ledgerDir, readBlobFromPath, readPathSha, RefCASConflictError, writeLedgerFiles, type LedgerLocation } from "./git";
|
|
30
|
+
|
|
31
|
+
/** The directory on `chant/lifecycle` that holds stored change sets. */
|
|
32
|
+
export const CHANGE_SETS_DIR = "_change-sets";
|
|
33
|
+
|
|
34
|
+
/** The file a change set with `digest` is stored in, inside {@link CHANGE_SETS_DIR}. */
|
|
35
|
+
export function changeSetFile(digest: string): string {
|
|
36
|
+
return `${digest.replace(/:/g, "_")}.json`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Thrown when a document can't be stored: it is not a change-set document, or its content does not hash to its digest. */
|
|
40
|
+
export class ChangeSetStoreError extends Error {
|
|
41
|
+
constructor(message: string) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "ChangeSetStoreError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Store a change-set document under its digest. A document already stored
|
|
49
|
+
* under the digest is never rewritten. Refuses a document whose content does
|
|
50
|
+
* not hash to its own `digest`. Does not push: call `pushLifecycleReport`
|
|
51
|
+
* (./git.ts) after, or let the gate's own pending fact push it.
|
|
52
|
+
*/
|
|
53
|
+
export async function persistChangeSet(
|
|
54
|
+
doc: ChangeSetDocument,
|
|
55
|
+
opts?: { cwd?: string },
|
|
56
|
+
): Promise<{ commit: string | null; written: boolean }> {
|
|
57
|
+
if (doc?.$schema !== CHANGE_SET_SCHEMA_ID || doc.contract !== CHANGE_SET_CONTRACT) {
|
|
58
|
+
throw new ChangeSetStoreError(`not a change-set document: its $schema is not ${CHANGE_SET_SCHEMA_ID} with contract ${CHANGE_SET_CONTRACT}`);
|
|
59
|
+
}
|
|
60
|
+
if (!verifyChangeSetDigest(doc)) {
|
|
61
|
+
throw new ChangeSetStoreError(`change set ${doc.digest} does not hash to its own digest; refusing to store it`);
|
|
62
|
+
}
|
|
63
|
+
const file = changeSetFile(doc.digest);
|
|
64
|
+
if ((await readPathSha(CHANGE_SETS_DIR, file, opts)) !== null) return { commit: null, written: false };
|
|
65
|
+
const dir = await ledgerDir(CHANGE_SETS_DIR, opts);
|
|
66
|
+
try {
|
|
67
|
+
const commit = await writeLedgerFiles(
|
|
68
|
+
[{ path: `${dir}/${file}`, content: `${JSON.stringify(doc, sortedJsonReplacer)}\n`, expectPriorSha: null }],
|
|
69
|
+
`Change set ${doc.digest}`,
|
|
70
|
+
opts,
|
|
71
|
+
);
|
|
72
|
+
return { commit, written: true };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Another writer stored the same digest first; what is there is the same document.
|
|
75
|
+
if (err instanceof RefCASConflictError) return { commit: null, written: false };
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What reading one stored change set found. */
|
|
81
|
+
export type StoredChangeSetRead =
|
|
82
|
+
| { status: "found"; document: ChangeSetDocument }
|
|
83
|
+
| { status: "missing" }
|
|
84
|
+
/** A file is stored under the digest, and it is not a change-set document whose content hashes to that digest. */
|
|
85
|
+
| { status: "malformed"; message: string };
|
|
86
|
+
|
|
87
|
+
/** Parse a stored file, checking it is the document `digest` names. */
|
|
88
|
+
export function parseStoredChangeSet(text: string, digest: string): StoredChangeSetRead {
|
|
89
|
+
let doc: ChangeSetDocument;
|
|
90
|
+
try {
|
|
91
|
+
doc = JSON.parse(text) as ChangeSetDocument;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return { status: "malformed", message: `the file stored under ${digest} is not JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
94
|
+
}
|
|
95
|
+
if (!doc || typeof doc !== "object" || doc.$schema !== CHANGE_SET_SCHEMA_ID || doc.contract !== CHANGE_SET_CONTRACT || !Array.isArray(doc.entries) || !Array.isArray(doc.members)) {
|
|
96
|
+
return { status: "malformed", message: `the file stored under ${digest} is not a change-set document` };
|
|
97
|
+
}
|
|
98
|
+
if (doc.digest !== digest || !verifyChangeSetDigest(doc)) {
|
|
99
|
+
return { status: "malformed", message: `the change set stored under ${digest} does not hash to that digest` };
|
|
100
|
+
}
|
|
101
|
+
return { status: "found", document: doc };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read the change set stored under `digest`. Reads the local `chant/lifecycle`
|
|
106
|
+
* branch and never fetches. Throws only when git itself fails.
|
|
107
|
+
*/
|
|
108
|
+
export async function readStoredChangeSet(digest: string, opts?: LedgerLocation): Promise<StoredChangeSetRead> {
|
|
109
|
+
const text = await readBlobFromPath(CHANGE_SETS_DIR, changeSetFile(digest), opts);
|
|
110
|
+
if (text === null) return { status: "missing" };
|
|
111
|
+
return parseStoredChangeSet(text, digest);
|
|
112
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit a run, a gate or a deploy is for (#4020), as one answer for every
|
|
3
|
+
* record that carries it. The checkout's `HEAD` is what the process builds
|
|
4
|
+
* from, so it comes first; a CI job whose checkout has no git (a bare
|
|
5
|
+
* artifact job) falls back to the commit the forge says it started for.
|
|
6
|
+
* Undefined when neither is known, so a record leaves the field out rather
|
|
7
|
+
* than guessing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ciCommitFromEnv } from "../op/gate-run-locator";
|
|
11
|
+
import { getHeadCommit } from "./git";
|
|
12
|
+
|
|
13
|
+
export async function resolveCommitSha(
|
|
14
|
+
opts: { cwd?: string; env?: Record<string, string | undefined> } = {},
|
|
15
|
+
): Promise<string | undefined> {
|
|
16
|
+
try {
|
|
17
|
+
const head = await getHeadCommit(opts.cwd ? { cwd: opts.cwd } : undefined);
|
|
18
|
+
if (/^[0-9a-f]{40,64}$/.test(head)) return head;
|
|
19
|
+
} catch {
|
|
20
|
+
// Not a checkout, or no git on the path: the environment may still say.
|
|
21
|
+
}
|
|
22
|
+
return ciCommitFromEnv(opts.env);
|
|
23
|
+
}
|