@intentius/chant 0.124.27 → 0.124.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/dist/__golden__/wave-loop/op-held-on.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/dist/__golden__/wave-loop/op-per-run.json +7 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
- package/dist/__golden__/wave-loop/op-sealed.json +3 -0
- package/dist/__golden__/wave-loop/op-spend.json +3 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/dist/change-set-graph.d.ts +56 -0
- package/dist/change-set-graph.d.ts.map +1 -0
- package/dist/change-set-graph.js +128 -0
- package/dist/change-set.d.ts +52 -3
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +35 -4
- package/dist/chant-version.d.ts +9 -0
- package/dist/chant-version.d.ts.map +1 -0
- package/dist/chant-version.js +16 -0
- package/dist/cli/commands/build.d.ts +8 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -8
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.js +30 -2
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +13 -2
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.js +17 -8
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +19 -4
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +28 -0
- package/dist/cli/handlers/change-set.d.ts +10 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.js +51 -1
- package/dist/cli/handlers/fan-out.d.ts +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +14 -3
- package/dist/cli/handlers/pr.d.ts +8 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +148 -24
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +256 -21
- package/dist/cli/handlers/release.d.ts +34 -0
- package/dist/cli/handlers/release.d.ts.map +1 -0
- package/dist/cli/handlers/release.js +203 -0
- package/dist/cli/handlers/run.d.ts +28 -2
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +170 -13
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +88 -13
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +49 -3
- package/dist/cli/registry.d.ts +29 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +29 -0
- package/dist/cli/run-events.d.ts.map +1 -0
- package/dist/cli/run-events.js +47 -0
- package/dist/cli/version.d.ts +3 -5
- package/dist/cli/version.d.ts.map +1 -1
- package/dist/cli/version.js +3 -13
- package/dist/codegen/docs-in-site.d.ts +71 -0
- package/dist/codegen/docs-in-site.d.ts.map +1 -0
- package/dist/codegen/docs-in-site.js +177 -0
- package/dist/codegen/docs-types.d.ts +14 -1
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +18 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/docs.js +40 -7
- package/dist/codegen/rehype-base-url.mjs +15 -9
- package/dist/components/auto-release.d.ts +4 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +2 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +10 -9
- package/dist/components/driver.d.ts +42 -9
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +81 -54
- package/dist/components/fan-out-run.d.ts +7 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +35 -26
- package/dist/components/pr-run.d.ts +11 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +20 -1
- package/dist/components/promote-order.d.ts +38 -0
- package/dist/components/promote-order.d.ts.map +1 -0
- package/dist/components/promote-order.js +62 -0
- package/dist/components/promote.d.ts +15 -2
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +17 -0
- package/dist/components/release-compile.d.ts +133 -0
- package/dist/components/release-compile.d.ts.map +1 -0
- package/dist/components/release-compile.js +376 -0
- package/dist/components/release-gate.d.ts +125 -0
- package/dist/components/release-gate.d.ts.map +1 -0
- package/dist/components/release-gate.js +282 -0
- package/dist/components/release-promote.d.ts +96 -0
- package/dist/components/release-promote.d.ts.map +1 -0
- package/dist/components/release-promote.js +204 -0
- package/dist/components/run-progress.d.ts +7 -0
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +3 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lifecycle/change-set-store.d.ts +67 -0
- package/dist/lifecycle/change-set-store.d.ts.map +1 -0
- package/dist/lifecycle/change-set-store.js +96 -0
- package/dist/lifecycle/commit-sha.d.ts +13 -0
- package/dist/lifecycle/commit-sha.d.ts.map +1 -0
- package/dist/lifecycle/commit-sha.js +21 -0
- package/dist/lifecycle/gate-change-set.d.ts +69 -0
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
- package/dist/lifecycle/gate-change-set.js +106 -0
- package/dist/lifecycle/gate-ledger.d.ts +49 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +18 -0
- package/dist/lifecycle/release-cut.d.ts +107 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -0
- package/dist/lifecycle/release-cut.js +273 -0
- package/dist/lifecycle/release-ledger.d.ts +54 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +53 -20
- package/dist/lifecycle/release-store.d.ts +190 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -0
- package/dist/lifecycle/release-store.js +318 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +2 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.js +4 -2
- package/dist/op/activities/source-release.d.ts.map +1 -1
- package/dist/op/activities/source-release.js +7 -3
- package/dist/op/activities/source-rollback.d.ts.map +1 -1
- package/dist/op/activities/source-rollback.js +7 -3
- package/dist/op/gate-name.d.ts +11 -2
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-name.js +12 -3
- package/dist/op/gate-run-locator.d.ts +22 -0
- package/dist/op/gate-run-locator.d.ts.map +1 -1
- package/dist/op/gate-run-locator.js +31 -0
- package/dist/op/gate.d.ts +27 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +57 -3
- package/dist/op/local-executor.d.ts +36 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +158 -52
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -0
- package/dist/op/run-live.d.ts +2 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/run-live.js +1 -1
- package/dist/op/runtime.d.ts +46 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +8 -0
- package/dist/point-decision.d.ts +2 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +9 -2
- package/dist/run-event-store.d.ts +77 -0
- package/dist/run-event-store.d.ts.map +1 -0
- package/dist/run-event-store.js +204 -0
- package/dist/run-event-stream.d.ts +132 -0
- package/dist/run-event-stream.d.ts.map +1 -0
- package/dist/run-event-stream.js +238 -0
- package/dist/run-event.d.ts +267 -0
- package/dist/run-event.d.ts.map +1 -0
- package/dist/run-event.js +207 -0
- package/dist/wave-runtime.mjs +204 -199
- package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
- package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/run-event-check.js +59 -0
- package/dist/workspace/change-set-show.d.ts +56 -0
- package/dist/workspace/change-set-show.d.ts.map +1 -0
- package/dist/workspace/change-set-show.js +54 -0
- package/dist/workspace/change-set-show.schema.json +102 -0
- package/dist/workspace/change-set.schema.json +28 -1
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes-cli.js +10 -1
- package/dist/workspace/changes.d.ts +37 -0
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/changes.js +49 -2
- package/dist/workspace/changes.schema.json +135 -0
- package/dist/workspace/ci-green-cli.d.ts +19 -4
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +75 -12
- package/dist/workspace/ci-green-forge.d.ts +20 -3
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +6 -4
- package/dist/workspace/ci-green-workflow.d.ts +11 -0
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +6 -0
- package/dist/workspace/ci-green.d.ts +273 -13
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +449 -38
- package/dist/workspace/ci-last-green.schema.json +32 -2
- package/dist/workspace/ci-list.schema.json +525 -0
- package/dist/workspace/commit-links.d.ts +85 -0
- package/dist/workspace/commit-links.d.ts.map +1 -0
- package/dist/workspace/commit-links.js +163 -0
- package/dist/workspace/declaration.d.ts +19 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +1 -1
- package/dist/workspace/declaration.schema.json +12 -0
- package/dist/workspace/identity-gate-rule.d.ts +37 -0
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +72 -7
- package/dist/workspace/identity.d.ts +12 -0
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/identity.js +13 -1
- package/dist/workspace/intent.d.ts +6 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +51 -1
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +2 -0
- package/dist/workspace/ls.schema.json +3 -1
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +6 -0
- package/dist/workspace/promotion-order.d.ts +41 -0
- package/dist/workspace/promotion-order.d.ts.map +1 -0
- package/dist/workspace/promotion-order.js +95 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +4 -1
- package/dist/workspace/releases.d.ts +72 -0
- package/dist/workspace/releases.d.ts.map +1 -0
- package/dist/workspace/releases.js +94 -0
- package/dist/workspace/releases.schema.json +391 -0
- package/dist/workspace/run-event.schema.json +288 -0
- package/dist/workspace/status-gates.d.ts +26 -1
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +31 -2
- package/dist/workspace/status-stewards.d.ts +4 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +2 -0
- package/dist/workspace/status.d.ts +39 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +48 -1
- package/dist/workspace/status.schema.json +70 -1
- package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
- package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
- package/dist/workspace/trust/ci-tag-statement.js +123 -0
- package/dist/workspace/trust/op-run-statement.d.ts +91 -0
- package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/op-run-statement.js +143 -0
- package/dist/workspace/trust/rotation.d.ts +18 -2
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.js +65 -16
- package/dist/workspace/trust/seal.d.ts +11 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +9 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/ssh-commit.js +10 -10
- package/package.json +1 -1
- package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/src/__golden__/wave-loop/fan-out-only.json +6 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/src/__golden__/wave-loop/op-cost-member.json +3 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/src/__golden__/wave-loop/op-held-on.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/src/__golden__/wave-loop/op-per-run.json +7 -0
- package/src/__golden__/wave-loop/op-pr-review.json +7 -0
- package/src/__golden__/wave-loop/op-sealed.json +3 -0
- package/src/__golden__/wave-loop/op-spend.json +3 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/src/change-set-graph.test.ts +72 -0
- package/src/change-set-graph.ts +133 -0
- package/src/change-set.ts +76 -6
- package/src/chant-version.ts +16 -0
- package/src/cli/commands/build.ts +19 -8
- package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
- package/src/cli/commands/check-lexicon-docs.ts +39 -2
- package/src/cli/commands/check-lexicon.ts +14 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
- package/src/cli/commands/init-lexicon.test.ts +25 -0
- package/src/cli/commands/init-lexicon.ts +18 -8
- package/src/cli/handlers/approve-release.test.ts +80 -0
- package/src/cli/handlers/approve.ts +18 -2
- package/src/cli/handlers/build.test.ts +52 -0
- package/src/cli/handlers/build.ts +29 -0
- package/src/cli/handlers/change-set.ts +49 -1
- package/src/cli/handlers/fan-out.test.ts +48 -0
- package/src/cli/handlers/fan-out.ts +15 -3
- package/src/cli/handlers/operator.test.ts +37 -0
- package/src/cli/handlers/pr.test.ts +194 -0
- package/src/cli/handlers/pr.ts +168 -34
- package/src/cli/handlers/promote.test.ts +389 -5
- package/src/cli/handlers/promote.ts +279 -20
- package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
- package/src/cli/handlers/release.e2e.test.ts +85 -0
- package/src/cli/handlers/release.ts +206 -0
- package/src/cli/handlers/run-auto-release.test.ts +71 -0
- package/src/cli/handlers/run-events-cmd.test.ts +92 -0
- package/src/cli/handlers/run.test.ts +30 -0
- package/src/cli/handlers/run.ts +176 -15
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +88 -13
- package/src/cli/mcp/never-over-mcp.test.ts +2 -0
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +37 -3
- package/src/cli/registry.ts +29 -3
- package/src/cli/run-events.ts +54 -0
- package/src/cli/version.ts +3 -13
- package/src/codegen/docs-in-site.test.ts +166 -0
- package/src/codegen/docs-in-site.ts +182 -0
- package/src/codegen/docs-site/AgentPrompt.astro +26 -1
- package/src/codegen/docs-types.ts +14 -1
- package/src/codegen/docs.ts +60 -7
- package/src/codegen/rehype-base-url.d.mts +3 -1
- package/src/codegen/rehype-base-url.mjs +15 -9
- package/src/codegen/rehype-base-url.test.ts +34 -0
- package/src/components/auto-release.test.ts +21 -0
- package/src/components/auto-release.ts +6 -1
- package/src/components/cli-support.test.ts +82 -0
- package/src/components/cli-support.ts +16 -9
- package/src/components/driver.ts +129 -56
- package/src/components/fan-out-run.ts +56 -35
- package/src/components/pr-run.ts +30 -1
- package/src/components/promote-order.test.ts +90 -0
- package/src/components/promote-order.ts +75 -0
- package/src/components/promote.test.ts +10 -1
- package/src/components/promote.ts +26 -1
- package/src/components/release-compile.test.ts +147 -0
- package/src/components/release-compile.ts +445 -0
- package/src/components/release-gate.test.ts +215 -0
- package/src/components/release-gate.ts +341 -0
- package/src/components/release-promote.test.ts +159 -0
- package/src/components/release-promote.ts +232 -0
- package/src/components/run-progress.ts +7 -0
- package/src/components/wave-plan.ts +1 -1
- package/src/config.ts +7 -1
- package/src/index.ts +2 -0
- package/src/lifecycle/change-set-store.test.ts +217 -0
- package/src/lifecycle/change-set-store.ts +112 -0
- package/src/lifecycle/commit-sha.ts +23 -0
- package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
- package/src/lifecycle/gate-change-set.ts +133 -0
- package/src/lifecycle/gate-ledger.test.ts +33 -0
- package/src/lifecycle/gate-ledger.ts +66 -1
- package/src/lifecycle/release-cut-signed.test.ts +180 -0
- package/src/lifecycle/release-cut.test.ts +411 -0
- package/src/lifecycle/release-cut.ts +367 -0
- package/src/lifecycle/release-ledger.test.ts +14 -0
- package/src/lifecycle/release-ledger.ts +85 -17
- package/src/lifecycle/release-store.ts +423 -0
- package/src/lifecycle/run-ledger.test.ts +10 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/op/activities/change-coverage.ts +4 -1
- package/src/op/activities/source-release.ts +8 -3
- package/src/op/activities/source-rollback.ts +8 -4
- package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
- package/src/op/gate-name.ts +13 -3
- package/src/op/gate-release.test.ts +126 -0
- package/src/op/gate-run-locator.test.ts +110 -0
- package/src/op/gate-run-locator.ts +35 -0
- package/src/op/gate.ts +88 -6
- package/src/op/local-executor.ts +207 -52
- package/src/op/op-waves-run.ts +4 -0
- package/src/op/run-events.test.ts +166 -0
- package/src/op/run-live.ts +1 -1
- package/src/op/runtime.ts +39 -0
- package/src/op/runtimes/local.test.ts +16 -0
- package/src/op/runtimes/local.ts +9 -0
- package/src/point-decision.ts +12 -2
- package/src/run-event-store.test.ts +144 -0
- package/src/run-event-store.ts +213 -0
- package/src/run-event-stream.test.ts +175 -0
- package/src/run-event-stream.ts +306 -0
- package/src/run-event.test.ts +277 -0
- package/src/run-event.ts +451 -0
- package/src/wave-loop-golden.test.ts +9 -0
- package/src/workspace/__fixtures__/run-event-check.ts +58 -0
- package/src/workspace/ancestry-index.test.ts +93 -0
- package/src/workspace/change-set-show.schema.json +102 -0
- package/src/workspace/change-set-show.ts +72 -0
- package/src/workspace/change-set.schema.json +28 -1
- package/src/workspace/changes-cli.ts +10 -1
- package/src/workspace/changes.schema.json +135 -0
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/changes.ts +84 -2
- package/src/workspace/ci-green-cli.ts +75 -13
- package/src/workspace/ci-green-forge.ts +24 -4
- package/src/workspace/ci-green-runs.test.ts +242 -0
- package/src/workspace/ci-green-signed.test.ts +302 -0
- package/src/workspace/ci-green-workflow.test.ts +16 -0
- package/src/workspace/ci-green-workflow.ts +12 -0
- package/src/workspace/ci-green.test.ts +131 -2
- package/src/workspace/ci-green.ts +624 -42
- package/src/workspace/ci-last-green.schema.json +32 -2
- package/src/workspace/ci-list.schema.json +525 -0
- package/src/workspace/commit-links.test.ts +279 -0
- package/src/workspace/commit-links.ts +183 -0
- package/src/workspace/declaration.schema.json +12 -0
- package/src/workspace/declaration.ts +22 -2
- package/src/workspace/identity-gate-rule.ts +91 -7
- package/src/workspace/identity.ts +14 -1
- package/src/workspace/intent.ts +48 -1
- package/src/workspace/ls.schema.json +3 -1
- package/src/workspace/ls.ts +6 -0
- package/src/workspace/member-pipeline.test.ts +17 -0
- package/src/workspace/member-pipeline.ts +9 -0
- package/src/workspace/promotion-order.ts +97 -0
- package/src/workspace/read-contract.e2e.test.ts +4 -1
- package/src/workspace/reason-codes.test.ts +2 -1
- package/src/workspace/reason-codes.ts +4 -1
- package/src/workspace/releases.schema.json +391 -0
- package/src/workspace/releases.test.ts +108 -0
- package/src/workspace/releases.ts +122 -0
- package/src/workspace/run-event.schema.json +288 -0
- package/src/workspace/status-contract.test.ts +52 -1
- package/src/workspace/status-gates.ts +56 -1
- package/src/workspace/status-stewards.ts +6 -0
- package/src/workspace/status.schema.json +70 -1
- package/src/workspace/status.ts +80 -1
- package/src/workspace/trust/ci-tag-statement.ts +151 -0
- package/src/workspace/trust/op-run-statement.ts +148 -0
- package/src/workspace/trust/rotation.ts +61 -14
- package/src/workspace/trust/seal.ts +10 -0
- package/src/workspace/trust/ssh-commit.ts +10 -9
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
package/src/op/gate-name.ts
CHANGED
|
@@ -44,6 +44,16 @@ export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
|
|
|
44
44
|
*/
|
|
45
45
|
export const OVERRIDE_GATE_OP = "policy-override";
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The op name every release gate is recorded under (#4045): the gates
|
|
49
|
+
* `chant components promote --release` reaches, `_gates/release.jsonl`, each
|
|
50
|
+
* named as its composition names it and bound to a release, an environment
|
|
51
|
+
* and the release's change set. Like a fan-out, it has no `*.op.ts`, and like
|
|
52
|
+
* the pull-request gate it approves a change to an environment, so it is
|
|
53
|
+
* never resolved over MCP or ACP.
|
|
54
|
+
*/
|
|
55
|
+
export const RELEASE_GATE_OP = "release";
|
|
56
|
+
|
|
47
57
|
/**
|
|
48
58
|
* The pull-request loop records its gate under `pr-<number>`, or
|
|
49
59
|
* `pr-<number>-<member>` in a workspace member (`prOp` in `pr-loop.ts`), and
|
|
@@ -54,12 +64,12 @@ const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
|
|
|
54
64
|
/**
|
|
55
65
|
* Whether gates recorded under this op name are never resolved over MCP or ACP
|
|
56
66
|
* (chant#3485, chant#3513). These are the gate-only names MCP cannot discover:
|
|
57
|
-
* `fan-out`, `workspace-upgrade`, `policy-override` (#3760)
|
|
58
|
-
* pull-request gate `pr-<number>`. With
|
|
67
|
+
* `fan-out`, `workspace-upgrade`, `policy-override` (#3760), `release`
|
|
68
|
+
* (#4045) and the pull-request gate `pr-<number>`. With
|
|
59
69
|
* no Op config to read `neverOverMcp` from, the pending record carries it.
|
|
60
70
|
*/
|
|
61
71
|
export function isNeverOverMcpGateOp(op: string): boolean {
|
|
62
|
-
return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || PR_GATE_OP.test(op);
|
|
72
|
+
return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || op === RELEASE_GATE_OP || PR_GATE_OP.test(op);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A release gate (#4045, ws-104 question 2) binds the release, the
|
|
3
|
+
* environment and the change-set document digest; an approval counts only
|
|
4
|
+
* when all three match. An `empty` pass (question 6) counts only once the
|
|
5
|
+
* stored document reads back empty. Decided against an in-memory ledger.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { evaluateGate, memoryGateLedgerPort } from "./gate";
|
|
10
|
+
import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
|
|
11
|
+
|
|
12
|
+
const sha = (c: string) => `jcs1-sha256:${c.repeat(64)}`;
|
|
13
|
+
const RELEASE_A = sha("a");
|
|
14
|
+
const RELEASE_B = sha("b");
|
|
15
|
+
const DOC = sha("d");
|
|
16
|
+
const NOW = "2026-10-16T12:00:00.000Z";
|
|
17
|
+
const LATER = "2026-10-16T13:00:00.000Z";
|
|
18
|
+
|
|
19
|
+
const gateInput = (over: Partial<Parameters<typeof evaluateGate>[1]> = {}) => ({
|
|
20
|
+
op: "release",
|
|
21
|
+
gate: "prod-release",
|
|
22
|
+
environment: "prod",
|
|
23
|
+
planDigest: DOC,
|
|
24
|
+
releaseDigest: RELEASE_A,
|
|
25
|
+
now: NOW,
|
|
26
|
+
...over,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const approval = (over: Partial<GateResolutionRecord> = {}): GateResolutionRecord => ({
|
|
30
|
+
version: 1,
|
|
31
|
+
op: "release",
|
|
32
|
+
gate: "prod-release",
|
|
33
|
+
resolvedBy: "alice",
|
|
34
|
+
timestamp: "2026-10-16T12:30:00.000Z",
|
|
35
|
+
planDigest: DOC,
|
|
36
|
+
environment: "prod",
|
|
37
|
+
releaseDigest: RELEASE_A,
|
|
38
|
+
...over,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("a release gate binds the release, the environment and the change set", () => {
|
|
42
|
+
test("the pending fact names the release it waits on", async () => {
|
|
43
|
+
const port = memoryGateLedgerPort();
|
|
44
|
+
const check = await evaluateGate(port, gateInput());
|
|
45
|
+
expect(check.satisfied).toBe(false);
|
|
46
|
+
expect(port.appended[0]).toMatchObject({ op: "release", gate: "prod-release", environment: "prod", planDigest: DOC, releaseDigest: RELEASE_A });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("an approval for the release, environment and change set passes, and names the release it admitted", async () => {
|
|
50
|
+
const port = memoryGateLedgerPort({ resolutions: [approval()] });
|
|
51
|
+
const check = await evaluateGate(port, gateInput({ now: LATER }));
|
|
52
|
+
expect(check.satisfied).toBe(true);
|
|
53
|
+
if (check.satisfied) expect(check.resolution.releaseDigest).toBe(RELEASE_A);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("an approval for one release can't be used by another, even for the same change-set digest", async () => {
|
|
57
|
+
const port = memoryGateLedgerPort({ resolutions: [approval()] });
|
|
58
|
+
const check = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, now: LATER }));
|
|
59
|
+
expect(check.satisfied).toBe(false);
|
|
60
|
+
if (!check.satisfied) {
|
|
61
|
+
expect(check.mismatch).toMatchObject({ resolvedBy: "alice", approved: DOC, planned: DOC, environment: "prod" });
|
|
62
|
+
expect(check.pending.releaseDigest).toBe(RELEASE_B);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("an approval for one environment can't be used for another", async () => {
|
|
67
|
+
const port = memoryGateLedgerPort({ resolutions: [approval({ environment: "staging" })] });
|
|
68
|
+
const check = await evaluateGate(port, gateInput({ now: LATER }));
|
|
69
|
+
expect(check.satisfied).toBe(false);
|
|
70
|
+
if (!check.satisfied) expect(check.pending.environment).toBe("prod");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("an approval for another change set of the same release doesn't pass", async () => {
|
|
74
|
+
const port = memoryGateLedgerPort({ resolutions: [approval({ planDigest: sha("e") })] });
|
|
75
|
+
const check = await evaluateGate(port, gateInput({ now: LATER }));
|
|
76
|
+
expect(check.satisfied).toBe(false);
|
|
77
|
+
if (!check.satisfied) expect(check.mismatch?.approved).toBe(sha("e"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a gate that binds no release counts no approval recorded for one", async () => {
|
|
81
|
+
const port = memoryGateLedgerPort({ resolutions: [approval()] });
|
|
82
|
+
const check = await evaluateGate(port, gateInput({ releaseDigest: undefined, now: LATER }));
|
|
83
|
+
expect(check.satisfied).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a standing fact for another release is recorded again for this one", async () => {
|
|
87
|
+
const port = memoryGateLedgerPort();
|
|
88
|
+
await evaluateGate(port, gateInput());
|
|
89
|
+
const again = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, now: "2026-10-16T12:01:00.000Z" }));
|
|
90
|
+
expect(again.satisfied).toBe(false);
|
|
91
|
+
if (!again.satisfied) expect(again.recorded).toBe(true);
|
|
92
|
+
expect(port.appended.map((p) => p.releaseDigest)).toEqual([RELEASE_A, RELEASE_B]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("an empty pass", () => {
|
|
97
|
+
const pass = approval({ resolvedBy: "bob", empty: true, timestamp: "2026-10-16T11:59:00.000Z" });
|
|
98
|
+
|
|
99
|
+
test("passes the gate once the stored change set reads back empty", async () => {
|
|
100
|
+
const asked: string[] = [];
|
|
101
|
+
const port = memoryGateLedgerPort({ resolutions: [pass] });
|
|
102
|
+
const check = await evaluateGate(port, gateInput({ verifyEmpty: async (d) => (asked.push(d), true) }));
|
|
103
|
+
expect(check).toMatchObject({ satisfied: true, via: "empty", resolution: { resolvedBy: "bob", empty: true, releaseDigest: RELEASE_A } });
|
|
104
|
+
expect(asked).toEqual([DOC]);
|
|
105
|
+
expect(port.appended).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("doesn't pass when the stored change set is not empty", async () => {
|
|
109
|
+
const port = memoryGateLedgerPort({ resolutions: [pass] });
|
|
110
|
+
const check = await evaluateGate(port, gateInput({ verifyEmpty: async () => false }));
|
|
111
|
+
expect(check.satisfied).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("is never an approval: without a verifier, or on a gate without a plan, it counts for nothing", async () => {
|
|
115
|
+
const port = memoryGateLedgerPort({ resolutions: [pass] });
|
|
116
|
+
expect((await evaluateGate(port, gateInput())).satisfied).toBe(false);
|
|
117
|
+
const plain = memoryGateLedgerPort({ resolutions: [{ ...pass, planDigest: undefined, releaseDigest: undefined }] });
|
|
118
|
+
expect((await evaluateGate(plain, { op: "release", gate: "prod-release", now: NOW })).satisfied).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("for another release it doesn't pass", async () => {
|
|
122
|
+
const port = memoryGateLedgerPort({ resolutions: [pass] });
|
|
123
|
+
const check = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, verifyEmpty: async () => true }));
|
|
124
|
+
expect(check.satisfied).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { ciCommitFromEnv, ciRunIdFromEnv, resolveGateRunLocator, runOriginOfLocator } from "./gate-run-locator";
|
|
3
|
+
import { resolveCiRun, resolveRunId } from "../lifecycle/release-ledger";
|
|
4
|
+
|
|
5
|
+
const GITHUB = {
|
|
6
|
+
GITHUB_ACTIONS: "true",
|
|
7
|
+
GITHUB_REPOSITORY: "acme/app",
|
|
8
|
+
GITHUB_RUN_ID: "9001",
|
|
9
|
+
GITHUB_RUN_ATTEMPT: "2",
|
|
10
|
+
GITHUB_SERVER_URL: "https://github.example",
|
|
11
|
+
GITHUB_API_URL: "https://github.example/api/v3",
|
|
12
|
+
GITHUB_SHA: "a".repeat(40),
|
|
13
|
+
};
|
|
14
|
+
const GITLAB = {
|
|
15
|
+
GITLAB_CI: "true",
|
|
16
|
+
CI_API_V4_URL: "https://gitlab.example/api/v4",
|
|
17
|
+
CI_PROJECT_ID: "77",
|
|
18
|
+
CI_PROJECT_PATH: "acme/app",
|
|
19
|
+
CI_PIPELINE_ID: "5150",
|
|
20
|
+
CI_PIPELINE_URL: "https://gitlab.example/acme/app/-/pipelines/5150",
|
|
21
|
+
CI_JOB_ID: "31",
|
|
22
|
+
CI_JOB_NAME: "deploy",
|
|
23
|
+
CI_JOB_URL: "https://gitlab.example/acme/app/-/jobs/31",
|
|
24
|
+
CI_COMMIT_SHA: "b".repeat(40),
|
|
25
|
+
};
|
|
26
|
+
const FORGEJO = {
|
|
27
|
+
...GITHUB,
|
|
28
|
+
FORGEJO_ACTIONS: "true",
|
|
29
|
+
GITHUB_WORKFLOW_REF: "acme/app/.forgejo/workflows/deploy.yml@refs/heads/main",
|
|
30
|
+
GITHUB_REF_NAME: "main",
|
|
31
|
+
GITHUB_SERVER_URL: "https://forgejo.example",
|
|
32
|
+
GITHUB_API_URL: "https://forgejo.example/api/v1",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("runOriginOfLocator (#4020)", () => {
|
|
36
|
+
test("a GitHub locator maps to forge github, with its repo and run page, and the run as the run id", () => {
|
|
37
|
+
const locator = resolveGateRunLocator(GITHUB)!;
|
|
38
|
+
expect(runOriginOfLocator(locator)).toEqual({
|
|
39
|
+
runId: "9001",
|
|
40
|
+
runOrigin: { forge: "github", repo: "acme/app", url: "https://github.example/acme/app/actions/runs/9001" },
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a Forgejo locator maps to forge github, since the url names the host", () => {
|
|
45
|
+
const locator = resolveGateRunLocator(FORGEJO)!;
|
|
46
|
+
expect(locator.forge).toBe("forgejo");
|
|
47
|
+
expect(runOriginOfLocator(locator)).toEqual({
|
|
48
|
+
runId: "9001",
|
|
49
|
+
runOrigin: { forge: "github", repo: "acme/app", url: "https://forgejo.example/acme/app/actions/runs/9001" },
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a GitLab locator stays gitlab, with the project id as repo and the pipeline id as the run id", () => {
|
|
54
|
+
const locator = resolveGateRunLocator(GITLAB)!;
|
|
55
|
+
expect(runOriginOfLocator(locator)).toEqual({
|
|
56
|
+
runId: "5150",
|
|
57
|
+
runOrigin: { forge: "gitlab", repo: "77", url: "https://gitlab.example/acme/app/-/jobs/31" },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the API base, attempt, job and workflow fields do not reach the origin", () => {
|
|
62
|
+
const { runOrigin } = runOriginOfLocator(resolveGateRunLocator(GITHUB)!);
|
|
63
|
+
expect(Object.keys(runOrigin).sort()).toEqual(["forge", "repo", "url"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a locator without a url maps to an origin without one", () => {
|
|
67
|
+
const { api, repo, run } = resolveGateRunLocator(GITHUB)!;
|
|
68
|
+
expect(runOriginOfLocator({ forge: "github", api, repo, run })).toEqual({ runId: "9001", runOrigin: { forge: "github", repo: "acme/app" } });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("a GateRunLocator and the RunOrigin written for the same CI job name the same run (#4020)", () => {
|
|
73
|
+
test.each([
|
|
74
|
+
["GitHub", GITHUB, "github"],
|
|
75
|
+
["Forgejo", FORGEJO, "github"],
|
|
76
|
+
["GitLab", GITLAB, "gitlab"],
|
|
77
|
+
] as const)("%s", (_name, env, forge) => {
|
|
78
|
+
const locator = resolveGateRunLocator(env)!;
|
|
79
|
+
const fromLocator = runOriginOfLocator(locator);
|
|
80
|
+
const written = resolveCiRun(env);
|
|
81
|
+
// A deploy record's run id is the one resolveRunId gives, and the gate's is the CI run id.
|
|
82
|
+
expect(resolveRunId(undefined, env).runId).toBe(fromLocator.runId);
|
|
83
|
+
expect(ciRunIdFromEnv(env)).toBe(fromLocator.runId);
|
|
84
|
+
expect(written?.runId).toBe(fromLocator.runId);
|
|
85
|
+
expect(written?.runOrigin.forge).toBe(forge);
|
|
86
|
+
expect(fromLocator.runOrigin.forge).toBe(written?.runOrigin.forge);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("on GitHub and Forgejo the two origins are the same object, repo and url included", () => {
|
|
90
|
+
for (const env of [GITHUB, FORGEJO]) {
|
|
91
|
+
expect(runOriginOfLocator(resolveGateRunLocator(env)!).runOrigin).toEqual(resolveCiRun(env)!.runOrigin);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("on GitLab the locator keeps the project id and the job page where the origin keeps the path and the pipeline page", () => {
|
|
96
|
+
expect(runOriginOfLocator(resolveGateRunLocator(GITLAB)!).runOrigin).toEqual({ forge: "gitlab", repo: "77", url: GITLAB.CI_JOB_URL });
|
|
97
|
+
expect(resolveCiRun(GITLAB)!.runOrigin).toEqual({ forge: "gitlab", repo: "acme/app", url: GITLAB.CI_PIPELINE_URL });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("the CI environment helpers", () => {
|
|
102
|
+
test("the commit is GITHUB_SHA or CI_COMMIT_SHA, and none outside CI", () => {
|
|
103
|
+
expect(ciCommitFromEnv(GITHUB)).toBe("a".repeat(40));
|
|
104
|
+
expect(ciCommitFromEnv(GITLAB)).toBe("b".repeat(40));
|
|
105
|
+
expect(ciCommitFromEnv({})).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
test("the run id is none outside CI", () => {
|
|
108
|
+
expect(ciRunIdFromEnv({})).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ForgeKind } from "../pr-forge";
|
|
10
|
+
import type { RunOrigin } from "../lifecycle/release-ledger";
|
|
10
11
|
|
|
11
12
|
/** Where the job that recorded a pending fact runs, and what its forge needs to start it again. */
|
|
12
13
|
export interface GateRunLocator {
|
|
@@ -103,3 +104,37 @@ export function isGateRunLocator(value: unknown): value is GateRunLocator {
|
|
|
103
104
|
if (v.forge === "forgejo") return typeof v.workflow === "string" && typeof v.ref === "string";
|
|
104
105
|
return false;
|
|
105
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The CI run a {@link GateRunLocator} names, in the shape a run and a deploy
|
|
110
|
+
* record carry (#4020). `runId` is the locator's `run`. Forge `github` and
|
|
111
|
+
* `forgejo` both become `github`, as `RunOrigin.forge` documents (the `url`
|
|
112
|
+
* names the host), and `gitlab` stays `gitlab`. `repo` and `url` carry over.
|
|
113
|
+
* The API base, attempt, job and workflow fields stay on the locator, which
|
|
114
|
+
* exists to start the job again.
|
|
115
|
+
*/
|
|
116
|
+
export function runOriginOfLocator(locator: GateRunLocator): { runId: string; runOrigin: RunOrigin } {
|
|
117
|
+
return {
|
|
118
|
+
runId: locator.run,
|
|
119
|
+
runOrigin: {
|
|
120
|
+
forge: locator.forge === "gitlab" ? "gitlab" : "github",
|
|
121
|
+
repo: locator.repo,
|
|
122
|
+
...(locator.url ? { url: locator.url } : {}),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The id of the CI run this process is in, or undefined outside CI: the
|
|
129
|
+
* GitHub (and Forgejo) run id, else the GitLab pipeline id. The same
|
|
130
|
+
* precedence `resolveRunId` (../lifecycle/release-ledger.ts) uses for a
|
|
131
|
+
* deploy record, so a gate and a deploy name the run alike.
|
|
132
|
+
*/
|
|
133
|
+
export function ciRunIdFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
|
|
134
|
+
return env.GITHUB_RUN_ID || env.CI_PIPELINE_ID || undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The commit the CI job was started for: `GITHUB_SHA` (GitHub and Forgejo) or `CI_COMMIT_SHA` (GitLab). Undefined outside CI. */
|
|
138
|
+
export function ciCommitFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
|
|
139
|
+
return env.GITHUB_SHA || env.CI_COMMIT_SHA || undefined;
|
|
140
|
+
}
|
package/src/op/gate.ts
CHANGED
|
@@ -53,7 +53,8 @@ import { sortedJsonReplacer } from "../utils";
|
|
|
53
53
|
import { gateContextDigest, type GateApprover, type ResolvedGateApproval } from "./gate-approval";
|
|
54
54
|
import { pushLifecycleReport, readBlobBySha, readBlobFromPath, readPathSha, requireLifecycleLedger, writeBlobToPath, RefCASConflictError } from "../lifecycle/git";
|
|
55
55
|
import { parseDuration } from "./duration";
|
|
56
|
-
import { resolveGateRunLocator } from "./gate-run-locator";
|
|
56
|
+
import { ciRunIdFromEnv, resolveGateRunLocator } from "./gate-run-locator";
|
|
57
|
+
import { resolveCommitSha } from "../lifecycle/commit-sha";
|
|
57
58
|
import { refuseGateWithoutRecording } from "../lifecycle/no-record";
|
|
58
59
|
import { gateCheckpointDigest, gateCheckpointText, readGateCheckpointText, writeGateCheckpoint, type GateCheckpoint } from "../lifecycle/gate-checkpoint";
|
|
59
60
|
import type { ResolvedGateAlias } from "./gate-alias";
|
|
@@ -258,8 +259,10 @@ export interface GateCheckInput {
|
|
|
258
259
|
description?: string;
|
|
259
260
|
/** The gate's authored `timeout`, which becomes the pending fact's expiry. Default {@link DEFAULT_GATE_EXPIRY}. */
|
|
260
261
|
timeout?: string;
|
|
261
|
-
/** Identifies the run that reached the gate, when the caller has one. */
|
|
262
|
+
/** Identifies the run that reached the gate, when the caller has one. Absent, a run in CI records the CI run id (#4020). */
|
|
262
263
|
runId?: string;
|
|
264
|
+
/** The commit the run is at (#4020). Absent, the checkout's `HEAD`, else the commit the CI job started for. */
|
|
265
|
+
gitSha?: string;
|
|
263
266
|
/**
|
|
264
267
|
* The plan this run reached the gate with (#2300) — `computePlanDigest`'s
|
|
265
268
|
* output (`../lifecycle/plan-digest.ts`), from the Plan phase that ran a
|
|
@@ -321,6 +324,25 @@ export interface GateCheckInput {
|
|
|
321
324
|
* carried earlier counts only while its alias is still declared.
|
|
322
325
|
*/
|
|
323
326
|
aliases?: readonly ResolvedGateAlias[];
|
|
327
|
+
/**
|
|
328
|
+
* The release this gate binds (#4045, ws-104 question 2): set on a release
|
|
329
|
+
* gate, the gate `chant components promote --release` passes for an
|
|
330
|
+
* environment, with `planDigest` the change-set document's digest and
|
|
331
|
+
* `environment` the environment. Set, a resolution counts only when it was
|
|
332
|
+
* recorded for this release as well as this plan and environment, and the
|
|
333
|
+
* pending fact it records carries it. Absent, a resolution recorded for a
|
|
334
|
+
* release never counts.
|
|
335
|
+
*/
|
|
336
|
+
releaseDigest?: string;
|
|
337
|
+
/**
|
|
338
|
+
* Whether the change set stored under a digest is empty (#4045, ws-104
|
|
339
|
+
* question 6): read back from `chant/lifecycle` and recomputed, never
|
|
340
|
+
* taken from the resolution. Given, an `empty: true` resolution for this
|
|
341
|
+
* gate, environment, release and plan passes the gate when this says its
|
|
342
|
+
* document is empty; quorum and approval rules don't apply to it. Absent,
|
|
343
|
+
* no `empty` resolution counts.
|
|
344
|
+
*/
|
|
345
|
+
verifyEmpty?: (planDigest: string) => Promise<boolean>;
|
|
324
346
|
}
|
|
325
347
|
|
|
326
348
|
/**
|
|
@@ -386,14 +408,18 @@ export type GateCheck =
|
|
|
386
408
|
* Set on a gate that declares `approval` (#2508). `"quorum"` when enough
|
|
387
409
|
* human approvals were recorded, `"policy"` when an `enforce`-mode permit
|
|
388
410
|
* passed it on its own. `"alias"` (#3949) when an approval recorded
|
|
389
|
-
* under one of the gate's aliases counted, on any gate.
|
|
411
|
+
* under one of the gate's aliases counted, on any gate. `"empty"`
|
|
412
|
+
* (#4045) when a release gate passed over an empty change set, on the
|
|
413
|
+
* `empty: true` resolution the promote wrote, after the stored
|
|
414
|
+
* document was read back and found empty.
|
|
390
415
|
*/
|
|
391
|
-
via?: "quorum" | "policy" | "alias";
|
|
416
|
+
via?: "quorum" | "policy" | "alias" | "empty";
|
|
392
417
|
/** Every approval that counted toward the quorum. Set with `via`. */
|
|
393
418
|
approvals?: GateResolutionRecord[];
|
|
394
419
|
/** The approvals this run carried from an alias and wrote under the gate's name (#3949). Set when `via` is `"alias"`. */
|
|
395
420
|
carried?: GateResolutionRecord[];
|
|
396
421
|
}
|
|
422
|
+
|
|
397
423
|
| {
|
|
398
424
|
satisfied: false;
|
|
399
425
|
pending: PendingGateRecord;
|
|
@@ -593,6 +619,7 @@ export function carriedApproval(r: GateResolutionRecord, op: string, gate: strin
|
|
|
593
619
|
...(r.url ? { url: r.url } : {}),
|
|
594
620
|
...(r.planDigest !== undefined ? { planDigest: r.planDigest } : {}),
|
|
595
621
|
...(r.environment !== undefined ? { environment: r.environment } : {}),
|
|
622
|
+
...(r.releaseDigest !== undefined ? { releaseDigest: r.releaseDigest } : {}),
|
|
596
623
|
...(r.origin ? { origin: r.origin } : {}),
|
|
597
624
|
...(r.sameOriginOverride ? { sameOriginOverride: true } : {}),
|
|
598
625
|
approver: approverOf(r),
|
|
@@ -650,6 +677,16 @@ async function aliasApprovals(
|
|
|
650
677
|
return out.sort((a, b) => at(a.timestamp) - at(b.timestamp));
|
|
651
678
|
}
|
|
652
679
|
|
|
680
|
+
/**
|
|
681
|
+
* The commit and run a gate record names (#4020): the input's own, else the
|
|
682
|
+
* checkout's commit and, in CI, the CI run id. Fields nobody knows are left out.
|
|
683
|
+
*/
|
|
684
|
+
async function runStamp(input: { runId?: string; gitSha?: string }): Promise<{ runId?: string; gitSha?: string }> {
|
|
685
|
+
const runId = input.runId ?? ciRunIdFromEnv();
|
|
686
|
+
const gitSha = input.gitSha ?? (await resolveCommitSha());
|
|
687
|
+
return { ...(runId ? { runId } : {}), ...(gitSha ? { gitSha } : {}) };
|
|
688
|
+
}
|
|
689
|
+
|
|
653
690
|
/** Whether two resolved approval blocks are the same, so a standing pending fact still describes this gate. */
|
|
654
691
|
function sameApproval(a: ResolvedGateApproval | undefined, b: ResolvedGateApproval | undefined): boolean {
|
|
655
692
|
return JSON.stringify(a ?? null, sortedJsonReplacer) === JSON.stringify(b ?? null, sortedJsonReplacer);
|
|
@@ -692,20 +729,48 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
692
729
|
// a mismatch with a message instead of silently not counting.
|
|
693
730
|
const env = input.environment;
|
|
694
731
|
const pending = env === undefined ? ledger.pending : ledger.pending.filter((p) => p.environment === env);
|
|
732
|
+
// #4045: an approval is for one release. On this gate, a resolution for
|
|
733
|
+
// another release (or for one, on a gate that binds none) never counts; the
|
|
734
|
+
// newest one since the standing fact comes back as a mismatch instead.
|
|
735
|
+
const forOtherRelease = (r: GateResolutionRecord) => r.gate === input.gate && r.releaseDigest !== input.releaseDigest;
|
|
695
736
|
// #3163: a gate the workspace's identity.gates names counts only signed approvals from its class.
|
|
696
737
|
const rule = (await port.approvalRule?.(input.gate)) ?? null;
|
|
697
738
|
const standing = latestPendingGate(pending, input.gate);
|
|
698
739
|
const since = standing?.timestamp ?? EPOCH;
|
|
740
|
+
|
|
741
|
+
// #4045: a pass over an empty change set. The promote wrote it, and it
|
|
742
|
+
// counts only once the document stored under its plan reads back empty.
|
|
743
|
+
const envResolutions = env === undefined ? ledger.resolutions : ledger.resolutions.filter((r) => r.environment === env);
|
|
744
|
+
if (input.verifyEmpty && input.planDigest !== undefined) {
|
|
745
|
+
const passes = envResolutions
|
|
746
|
+
.filter(
|
|
747
|
+
(r) =>
|
|
748
|
+
r.empty === true && r.gate === input.gate && !forOtherRelease(r) && samePlanDigest(r.planDigest, input.planDigest) &&
|
|
749
|
+
new Date(r.timestamp).getTime() >= new Date(since).getTime() &&
|
|
750
|
+
(input.spent === undefined || !isApprovalSpent(input.spent, r)),
|
|
751
|
+
)
|
|
752
|
+
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
753
|
+
const pass = passes[passes.length - 1];
|
|
754
|
+
if (pass && (await input.verifyEmpty(input.planDigest))) return { satisfied: true, resolution: pass, via: "empty" };
|
|
755
|
+
}
|
|
699
756
|
// #3949: aliases admit only approvals of this run's plan, and carrying one writes a line.
|
|
700
757
|
const aliases = input.planDigest !== undefined && port.appendResolution ? (input.aliases ?? []) : [];
|
|
701
758
|
const aliased = (from: { op: string; gate: string }) => aliases.some((a) => a.op === from.op && a.gate === from.gate);
|
|
702
759
|
// #3841: an approval of this plan the rule refuses is named, so a wait says why.
|
|
703
760
|
const refused: GateRefusedApproval[] = [];
|
|
761
|
+
const otherRelease: GateResolutionRecord[] = [];
|
|
704
762
|
const resolutions = (
|
|
705
763
|
env === undefined
|
|
706
764
|
? ledger.resolutions
|
|
707
765
|
: ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env)
|
|
708
766
|
)
|
|
767
|
+
// #4045: an `empty` pass is never an approval; it counts only above.
|
|
768
|
+
.filter((r) => r.empty !== true)
|
|
769
|
+
.filter((r) => {
|
|
770
|
+
if (!forOtherRelease(r)) return true;
|
|
771
|
+
otherRelease.push(r);
|
|
772
|
+
return false;
|
|
773
|
+
})
|
|
709
774
|
// #3949: a carried approval counts only while the gate still names its alias.
|
|
710
775
|
.filter((r) => r.gate !== input.gate || r.carriedFrom === undefined || aliased(r.carriedFrom))
|
|
711
776
|
.filter((r) => input.spent === undefined || r.gate !== input.gate || !isApprovalSpent(input.spent, r))
|
|
@@ -722,7 +787,10 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
722
787
|
// #3949: write the approvals that counted from an alias under this gate.
|
|
723
788
|
const carry = async (records: readonly GateResolutionRecord[]): Promise<GateResolutionRecord[]> => {
|
|
724
789
|
const written: GateResolutionRecord[] = [];
|
|
725
|
-
|
|
790
|
+
if (records.length === 0) return written;
|
|
791
|
+
// #4020: a carried approval is for this run's gate, so it names this run's commit and run.
|
|
792
|
+
const at = await runStamp(input);
|
|
793
|
+
for (const r of records) written.push((await port.appendResolution!({ ...carriedApproval(r, input.op, input.gate, now), ...at })).record);
|
|
726
794
|
return written;
|
|
727
795
|
};
|
|
728
796
|
|
|
@@ -775,6 +843,16 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
775
843
|
mismatched = found.mismatched;
|
|
776
844
|
}
|
|
777
845
|
|
|
846
|
+
// #4045: an approval of this gate for another release stands as a mismatch
|
|
847
|
+
// when nothing closer does, so the refusal says an approval exists.
|
|
848
|
+
if (!mismatched && input.planDigest !== undefined) {
|
|
849
|
+
const sinceMs = new Date(since).getTime();
|
|
850
|
+
mismatched = otherRelease
|
|
851
|
+
.filter((r) => new Date(r.timestamp).getTime() >= sinceMs)
|
|
852
|
+
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
|
853
|
+
.at(-1);
|
|
854
|
+
}
|
|
855
|
+
|
|
778
856
|
// `input.planDigest` is defined whenever `mismatched` is — `latestResolutionForPlan`
|
|
779
857
|
// returns a mismatch only on the plan-bound path.
|
|
780
858
|
const mismatch: GateDigestMismatch | undefined = mismatched
|
|
@@ -800,6 +878,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
800
878
|
// that already carries a checkpoint stands: an approval copies that one.
|
|
801
879
|
if (
|
|
802
880
|
standing && !isPendingGateExpired(standing, now) && samePlanDigest(standing.planDigest, input.planDigest) &&
|
|
881
|
+
standing.releaseDigest === input.releaseDigest &&
|
|
803
882
|
sameApproval(standing.approval, input.approval) &&
|
|
804
883
|
(input.checkpoint === undefined || port.writeCheckpoint === undefined || standing.checkpointDigest !== undefined)
|
|
805
884
|
) {
|
|
@@ -817,6 +896,8 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
817
896
|
const url = resolveApprovalUrl();
|
|
818
897
|
// #3683: in CI, where this job runs, so an approval can start it again.
|
|
819
898
|
const resume = resolveGateRunLocator();
|
|
899
|
+
// #4020: the commit and run the gate waits on, so a reader can join it to both.
|
|
900
|
+
const stamp = await runStamp(input);
|
|
820
901
|
const { record, pushed, pushWarning } = await port.appendPending({
|
|
821
902
|
...(input.fields as Partial<PendingGateInput> | undefined),
|
|
822
903
|
op: input.op,
|
|
@@ -826,12 +907,13 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
826
907
|
new Date(now).getTime() + parseDuration(input.timeout ?? DEFAULT_GATE_EXPIRY),
|
|
827
908
|
).toISOString(),
|
|
828
909
|
...(input.description ? { description: input.description } : {}),
|
|
829
|
-
...
|
|
910
|
+
...stamp,
|
|
830
911
|
...(url ? { url } : {}),
|
|
831
912
|
...(resume ? { resume } : {}),
|
|
832
913
|
...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
|
|
833
914
|
...(checkpointDigest !== undefined ? { checkpointDigest } : {}),
|
|
834
915
|
...(env !== undefined ? { environment: env } : {}),
|
|
916
|
+
...(input.releaseDigest !== undefined ? { releaseDigest: input.releaseDigest } : {}),
|
|
835
917
|
...(input.approval ? { approval: input.approval } : {}),
|
|
836
918
|
...(input.neverOverMcp ? { neverOverMcp: true as const } : {}),
|
|
837
919
|
});
|