@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
|
@@ -18,6 +18,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
18
18
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
|
+
import { pathToFileURL } from "node:url";
|
|
21
22
|
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
23
|
|
|
23
24
|
const coreRoot = resolve(import.meta.dirname, "..", "..");
|
|
@@ -37,12 +38,13 @@ const TRACER = `
|
|
|
37
38
|
import { registerHooks } from "node:module";
|
|
38
39
|
import { appendFileSync } from "node:fs";
|
|
39
40
|
const out = process.env.CHANT_TRACE_OUT;
|
|
40
|
-
const seen = { chantSrc: 0, chantDist: 0, tsx: 0 };
|
|
41
|
+
const seen = { chantSrc: 0, chantDist: 0, tsx: 0, typescript: 0, dist: [] };
|
|
41
42
|
registerHooks({
|
|
42
43
|
load(url, context, next) {
|
|
43
44
|
if (url.includes("/@intentius/chant/src/")) seen.chantSrc++;
|
|
44
|
-
else if (url.includes("/@intentius/chant/dist/")) seen.chantDist++;
|
|
45
|
+
else if (url.includes("/@intentius/chant/dist/")) { seen.chantDist++; seen.dist.push(url.slice(url.indexOf("/@intentius/chant/dist/") + 23)); }
|
|
45
46
|
else if (url.includes("/node_modules/tsx/")) seen.tsx++;
|
|
47
|
+
else if (url.includes("/node_modules/typescript/")) seen.typescript++;
|
|
46
48
|
return next(url, context);
|
|
47
49
|
},
|
|
48
50
|
});
|
|
@@ -59,14 +61,15 @@ interface Run {
|
|
|
59
61
|
status: number | null;
|
|
60
62
|
stdout: string;
|
|
61
63
|
stderr: string;
|
|
62
|
-
loaded
|
|
64
|
+
/** Counts of what loaded, plus each compiled module's path under `dist/`. */
|
|
65
|
+
loaded: { chantSrc: number; chantDist: number; tsx: number; typescript: number; dist: string[] };
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
function run(cwd: string, args: string[]): Run {
|
|
68
|
+
function run(cwd: string, args: string[], extraEnv: NodeJS.ProcessEnv = {}, command = chant): Run {
|
|
66
69
|
const traceOut = join(scratch, `trace-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
67
|
-
const env: NodeJS.ProcessEnv = { ...process.env, NODE_OPTIONS: `--import ${tracer}`, CHANT_TRACE_OUT: traceOut };
|
|
70
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ...extraEnv, NODE_OPTIONS: `--import ${tracer}`, CHANT_TRACE_OUT: traceOut };
|
|
68
71
|
delete env.CHANT_ENV;
|
|
69
|
-
const result = spawnSync(
|
|
72
|
+
const result = spawnSync(command, args, { cwd, env, encoding: "utf-8" });
|
|
70
73
|
const lines = readFileSync(traceOut, "utf-8").trim().split("\n");
|
|
71
74
|
return { status: result.status, stdout: result.stdout, stderr: result.stderr, loaded: JSON.parse(lines[0]!) as Run["loaded"] };
|
|
72
75
|
}
|
|
@@ -192,6 +195,28 @@ describe("the installed chant CLI (#3878)", () => {
|
|
|
192
195
|
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
193
196
|
});
|
|
194
197
|
|
|
198
|
+
test("workspace status on a steward-index miss loads no tsx for a member without Op files (#3976)", () => {
|
|
199
|
+
// A cache dir of its own, so there is no index to read and the member's stewards are discovered.
|
|
200
|
+
const r = run(join(workspace, "app"), ["workspace", "status", "local", "--json"], { CHANT_CACHE_DIR: mkdtempSync(join(scratch, "cache-")) });
|
|
201
|
+
expect(r.status, r.stderr).toBe(0);
|
|
202
|
+
const status = JSON.parse(r.stdout) as { members: Array<{ name: string; stewards: unknown[] }> };
|
|
203
|
+
expect(status.members.map((m) => [m.name, m.stewards])).toEqual([["app", []]]);
|
|
204
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("importing the op entry loads the Op authoring surface, not lint, the LSP, codegen or typescript (#3982)", () => {
|
|
208
|
+
// Every *.op.ts imports @intentius/chant/op, so this is what a steward-index miss in `workspace status` pays per
|
|
209
|
+
// member with Op files. The base activities, which reach lint and typescript, load only when an Op runs.
|
|
210
|
+
const entry = join(project, "node_modules", "@intentius", "chant", "dist", "op", "index.js");
|
|
211
|
+
const script = `const op = await import(${JSON.stringify(pathToFileURL(entry).href)});\n` +
|
|
212
|
+
`if (typeof op.Op !== "function" || typeof op.factoryOp !== "function" || typeof op.declareSteward !== "function") process.exit(2);\n`;
|
|
213
|
+
const r = run(project, ["--input-type=module", "-e", script], {}, process.execPath);
|
|
214
|
+
expect(r.status, r.stderr).toBe(0);
|
|
215
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0, typescript: 0 });
|
|
216
|
+
const heavy = r.loaded.dist.filter((path) => /^(lint|lsp|codegen|mcp|audit|fold|cli)\//.test(path) || path === "index.js" || path === "op/activities/index.js");
|
|
217
|
+
expect(heavy).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
195
220
|
test("build loads the TypeScript lexicon and project through tsx, against the one compiled core", () => {
|
|
196
221
|
const r = run(project, ["build", "src", "-o", "out/render.json"]);
|
|
197
222
|
expect(r.status, r.stderr).toBe(0);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant approve` on a release gate (#4045): the approval copies the release
|
|
3
|
+
* the pending fact names, with its plan and environment, so it counts for
|
|
4
|
+
* that release only. Against a temp repo; nothing is pushed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
8
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { appendPendingGate, readGateLedger } from "../../lifecycle/gate-ledger";
|
|
13
|
+
import { resetGateOrigin } from "../../lifecycle/gate-origin";
|
|
14
|
+
|
|
15
|
+
vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
|
|
16
|
+
|
|
17
|
+
const { recordGateApproval } = await import("./approve");
|
|
18
|
+
|
|
19
|
+
function git(args: string[], cwd: string): void {
|
|
20
|
+
spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const RELEASE = `jcs1-sha256:${"a".repeat(64)}`;
|
|
24
|
+
const DOC = `jcs1-sha256:${"d".repeat(64)}`;
|
|
25
|
+
|
|
26
|
+
async function repoWithReleaseGate(dir: string): Promise<void> {
|
|
27
|
+
git(["init", "-q", "-b", "main"], dir);
|
|
28
|
+
git(["config", "user.email", "t@chant.dev"], dir);
|
|
29
|
+
git(["config", "user.name", "T"], dir);
|
|
30
|
+
writeFileSync(join(dir, "README.md"), "x\n");
|
|
31
|
+
git(["add", "."], dir);
|
|
32
|
+
git(["commit", "-q", "-m", "init"], dir);
|
|
33
|
+
await appendPendingGate(
|
|
34
|
+
{
|
|
35
|
+
op: "release",
|
|
36
|
+
gate: "prod-release",
|
|
37
|
+
timestamp: "2026-10-16T00:00:00.000Z",
|
|
38
|
+
expiresAt: "2999-01-01T00:00:00.000Z",
|
|
39
|
+
planDigest: DOC,
|
|
40
|
+
environment: "prod",
|
|
41
|
+
releaseDigest: RELEASE,
|
|
42
|
+
},
|
|
43
|
+
{ cwd: dir },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("chant approve on a release gate (#4045)", () => {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
process.chdir(cwd);
|
|
51
|
+
resetGateOrigin();
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the approval names the release, the environment and the change set the pending fact names", async () => {
|
|
56
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
57
|
+
await withTestDir(async (dir) => {
|
|
58
|
+
await repoWithReleaseGate(dir);
|
|
59
|
+
process.chdir(dir);
|
|
60
|
+
const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
|
|
61
|
+
expect(outcome.ok).toBe(true);
|
|
62
|
+
const { resolutions } = await readGateLedger("release", { cwd: dir });
|
|
63
|
+
expect(resolutions).toEqual([expect.objectContaining({ resolvedBy: "alice", planDigest: DOC, environment: "prod", releaseDigest: RELEASE })]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("an approval of another plan names no release, so no release gate counts it", async () => {
|
|
68
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
69
|
+
await withTestDir(async (dir) => {
|
|
70
|
+
await repoWithReleaseGate(dir);
|
|
71
|
+
process.chdir(dir);
|
|
72
|
+
const other = `jcs1-sha256:${"e".repeat(64)}`;
|
|
73
|
+
const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod", plan: other });
|
|
74
|
+
expect(outcome.ok).toBe(true);
|
|
75
|
+
const { resolutions } = await readGateLedger("release", { cwd: dir });
|
|
76
|
+
expect(resolutions[0].planDigest).toBe(other);
|
|
77
|
+
expect(resolutions[0].releaseDigest).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type GateApprover, type GatePolicyDecision, type GatePolicyEvaluator, type ResolvedGateApproval,
|
|
22
22
|
} from "../../op/gate-approval";
|
|
23
23
|
import { approverOf, humanQuorumSince, tallyGateApprovals } from "../../op/gate";
|
|
24
|
-
import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name";
|
|
24
|
+
import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, RELEASE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name";
|
|
25
25
|
import { PR_GATE_OP } from "../../pr-loop";
|
|
26
26
|
import { pushLifecycleReport, requireLifecycleLedger } from "../../lifecycle/git";
|
|
27
27
|
import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note";
|
|
@@ -112,6 +112,8 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
|
|
|
112
112
|
timestamp: now,
|
|
113
113
|
expiresAt: now,
|
|
114
114
|
...(standing.description ? { description: standing.description } : {}),
|
|
115
|
+
...(standing.runId ? { runId: standing.runId } : {}),
|
|
116
|
+
...(standing.gitSha ? { gitSha: standing.gitSha } : {}),
|
|
115
117
|
});
|
|
116
118
|
const push = await reportedPush(
|
|
117
119
|
`The expiry is recorded locally on ${LIFECYCLE_BRANCH}. Until it reaches the remote, ` +
|
|
@@ -127,6 +129,8 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
|
|
|
127
129
|
? `The next \`chant components pr-apply\` decides this gate from scratch and records a fresh pending fact.`
|
|
128
130
|
: opName === WORKSPACE_UPGRADE_GATE_OP
|
|
129
131
|
? `The next \`chant workspace upgrade ${gate}\` decides this gate from scratch and records a fresh pending fact.`
|
|
132
|
+
: opName === RELEASE_GATE_OP
|
|
133
|
+
? `The next \`chant components promote --release\` decides this gate from scratch and records a fresh pending fact.`
|
|
130
134
|
: `The next \`chant run ${opName}\` decides this gate from scratch and records a fresh pending fact.`,
|
|
131
135
|
));
|
|
132
136
|
return 0;
|
|
@@ -177,6 +181,8 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
|
|
|
177
181
|
? `The pull request's apply (\`chant components pr-apply\`) walks through gate "${gate}" when it plans this same digest.`
|
|
178
182
|
: opName === WORKSPACE_UPGRADE_GATE_OP
|
|
179
183
|
? `Repeat \`chant workspace upgrade ${gate}\` with the same target, and it applies the patch this approval names.`
|
|
184
|
+
: opName === RELEASE_GATE_OP
|
|
185
|
+
? `Repeat the same \`chant components promote --release\`, and it walks through gate "${gate}" when it plans this same change set for this release.`
|
|
180
186
|
: `Run \`chant run ${opName}\` and it walks through gate "${gate}".`),
|
|
181
187
|
));
|
|
182
188
|
return 0;
|
|
@@ -283,7 +289,7 @@ export async function recordGateApproval(
|
|
|
283
289
|
// so there is no `*.op.ts` to find and nothing is wrong when none is there.
|
|
284
290
|
// #3949: a gate that lists this one as an alias, after an Op was restructured.
|
|
285
291
|
const successor = aliasingGate([...ops.values()].map((o) => o.config), opName, gate);
|
|
286
|
-
if (!successor && !ops.has(opName) && opName !== FAN_OUT_GATE_OP && opName !== WORKSPACE_UPGRADE_GATE_OP && opName !== OVERRIDE_GATE_OP) {
|
|
292
|
+
if (!successor && !ops.has(opName) && opName !== FAN_OUT_GATE_OP && opName !== WORKSPACE_UPGRADE_GATE_OP && opName !== OVERRIDE_GATE_OP && opName !== RELEASE_GATE_OP) {
|
|
287
293
|
console.error(formatWarning({
|
|
288
294
|
message: `Op "${opName}" was not found among discovered *.op.ts declarations — recording the resolution anyway`,
|
|
289
295
|
}));
|
|
@@ -389,6 +395,11 @@ export async function recordGateApproval(
|
|
|
389
395
|
standing?.checkpointDigest !== undefined && samePlanDigest(planDigest, standing.planDigest)
|
|
390
396
|
? standing.checkpointDigest
|
|
391
397
|
: undefined;
|
|
398
|
+
// #4045: a release gate's fact names the release its change set was
|
|
399
|
+
// planned for. An approval of that fact's plan is for that release too; an
|
|
400
|
+
// approval of another plan names none, and no release gate counts it.
|
|
401
|
+
const releaseDigest =
|
|
402
|
+
standing?.releaseDigest !== undefined && samePlanDigest(planDigest, standing.planDigest) ? standing.releaseDigest : undefined;
|
|
392
403
|
|
|
393
404
|
// chant#2384 — the gate's whole value is that its two halves have different
|
|
394
405
|
// authors. At a shell they do: a person runs, reads the plan, and approves.
|
|
@@ -587,6 +598,10 @@ export async function recordGateApproval(
|
|
|
587
598
|
...(planDigest !== undefined ? { planDigest } : {}),
|
|
588
599
|
...(checkpointDigest !== undefined ? { checkpointDigest } : {}),
|
|
589
600
|
...(environment !== undefined ? { environment } : {}),
|
|
601
|
+
...(releaseDigest !== undefined ? { releaseDigest } : {}),
|
|
602
|
+
// #4020: the commit and run the gate waited on, off the fact this approves.
|
|
603
|
+
...(standing?.gitSha ? { gitSha: standing.gitSha } : {}),
|
|
604
|
+
...(standing?.runId ? { runId: standing.runId } : {}),
|
|
590
605
|
origin,
|
|
591
606
|
...(refusal && opts.allowSameOrigin ? { sameOriginOverride: true } : {}),
|
|
592
607
|
});
|
|
@@ -607,6 +622,7 @@ export async function recordGateApproval(
|
|
|
607
622
|
console.error(formatInfo(
|
|
608
623
|
`This approves the plan ${record.planDigest}` +
|
|
609
624
|
(record.environment !== undefined ? ` in environment "${record.environment}"` : "") +
|
|
625
|
+
(record.releaseDigest !== undefined ? ` for release ${record.releaseDigest}` : "") +
|
|
610
626
|
", and only that plan. A run whose fresh plan differs refuses rather than applying it.",
|
|
611
627
|
));
|
|
612
628
|
}
|
|
@@ -70,6 +70,37 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
70
70
|
vi.restoreAllMocks();
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
test("--promote-to that skips a step of the declared promotion order is refused, and generates nothing (#4030)", async () => {
|
|
74
|
+
loadChantConfigUpwardMock.mockResolvedValue({ config: { environments: ["dev", "staging", "prod"] } });
|
|
75
|
+
const stderr = makeStderrSpy();
|
|
76
|
+
const exit = await runBuild({ args: makeArgs({ env: "dev", promoteTo: "prod" }), plugins: [], serializers: [] });
|
|
77
|
+
expect(exit).toBe(1);
|
|
78
|
+
expect(generateComponentsPipelineMock).not.toHaveBeenCalled();
|
|
79
|
+
const out = stderr.join("\n");
|
|
80
|
+
expect(out).toMatch(/\[promote-skips-environment\]/);
|
|
81
|
+
expect(out).toMatch(/--allow-skip/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("--promote-to the next step of the order generates as before (#4030)", async () => {
|
|
85
|
+
loadChantConfigUpwardMock.mockResolvedValue({ config: { environments: ["dev", "staging", "prod"] } });
|
|
86
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "x", stages: [], jobs: [] });
|
|
87
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
88
|
+
const exit = await runBuild({ args: makeArgs({ env: "staging", promoteTo: "prod" }), plugins: [], serializers: [] });
|
|
89
|
+
expect(exit).toBe(0);
|
|
90
|
+
expect(generateComponentsPipelineMock.mock.calls[0][2]).toEqual({ env: "staging", promoteTo: "prod" });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("--allow-skip lets the skip through and the promote job passes it on (#4030)", async () => {
|
|
94
|
+
loadChantConfigUpwardMock.mockResolvedValue({ config: { environments: ["dev", "staging", "prod"] } });
|
|
95
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "x", stages: [], jobs: [] });
|
|
96
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
97
|
+
const exit = await runBuild({ args: makeArgs({ env: "dev", promoteTo: "prod", allowSkip: true }), plugins: [], serializers: [] });
|
|
98
|
+
expect(exit).toBe(0);
|
|
99
|
+
expect(generateComponentsPipelineMock.mock.calls[0][2]).toMatchObject({
|
|
100
|
+
promoteCommand: ["chant", "components", "promote", "--from", "dev", "--to", "prod", "--allow-skip"],
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
73
104
|
test("--promote-to is forwarded to the generator as promoteTo (#2575)", async () => {
|
|
74
105
|
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
75
106
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
@@ -81,6 +112,27 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
81
112
|
vi.restoreAllMocks();
|
|
82
113
|
});
|
|
83
114
|
|
|
115
|
+
test("--image and each --before-script are forwarded to the generator, in order (#4002)", async () => {
|
|
116
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
117
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
118
|
+
|
|
119
|
+
const exit = await runBuild({
|
|
120
|
+
args: makeArgs({ generate: "forgejo", prLoop: true, image: "node:22-bookworm", beforeScript: ["npm ci", "npm run setup"] }),
|
|
121
|
+
plugins: [],
|
|
122
|
+
serializers: [],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(exit).toBe(0);
|
|
126
|
+
expect(generateComponentsPipelineMock).toHaveBeenCalledWith(
|
|
127
|
+
".",
|
|
128
|
+
"forgejo",
|
|
129
|
+
{ env: undefined, prLoop: {}, image: "node:22-bookworm", beforeScript: ["npm ci", "npm run setup"] },
|
|
130
|
+
undefined,
|
|
131
|
+
[],
|
|
132
|
+
);
|
|
133
|
+
vi.restoreAllMocks();
|
|
134
|
+
});
|
|
135
|
+
|
|
84
136
|
test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
|
|
85
137
|
loadChantConfigUpwardMock.mockResolvedValue({
|
|
86
138
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
@@ -7,6 +7,7 @@ import { generateComponentsPipeline } from "../../components/cli-support";
|
|
|
7
7
|
import { loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
8
8
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
9
9
|
import { findWorkspaceRoot } from "../../project-root";
|
|
10
|
+
import { resolvePromotionSource } from "../../components/promote-order";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* `chant build --components --generate <lexicon>` — generate mode (#563,
|
|
@@ -45,6 +46,24 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
45
46
|
return 1;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// #4030: the generated promote job runs `promote --from <env> --to <promote-to>`,
|
|
50
|
+
// which the member's declared promotion order refuses when it skips a step.
|
|
51
|
+
// Refuse here, with the same code, rather than write a job that fails.
|
|
52
|
+
if (args.promoteTo) {
|
|
53
|
+
// Loaded here, not at the top: a build without --promote-to loads no workspace module (#2526).
|
|
54
|
+
const { promotionOrder } = await import("../../workspace/promotion-order");
|
|
55
|
+
const resolved = resolvePromotionSource({
|
|
56
|
+
order: promotionOrder(config.environments),
|
|
57
|
+
to: args.promoteTo,
|
|
58
|
+
from: args.env ?? "production",
|
|
59
|
+
...(args.allowSkip ? { allowSkip: true } : {}),
|
|
60
|
+
});
|
|
61
|
+
if ("error" in resolved) {
|
|
62
|
+
console.error(formatError({ message: `${resolved.error.message} [${resolved.error.code}]`, hint: resolved.error.fix }));
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
// A project inside a workspace member gets that member's pipeline (#2542):
|
|
49
68
|
// path-filtered, run in the member's directory, written to a file named
|
|
50
69
|
// after the member and recorded as the member's generated file. The
|
|
@@ -62,11 +81,14 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
62
81
|
lexicon,
|
|
63
82
|
env: args.env,
|
|
64
83
|
promoteTo: args.promoteTo,
|
|
84
|
+
...(args.allowSkip ? { allowSkip: true } : {}),
|
|
65
85
|
output: args.output,
|
|
66
86
|
params: args.param,
|
|
67
87
|
paramsFile: args.paramsFile,
|
|
68
88
|
...(args.waveGate ? { waveGate: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } } : {}),
|
|
69
89
|
...(args.prLoop ? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } } : {}),
|
|
90
|
+
...(args.image ? { image: args.image } : {}),
|
|
91
|
+
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
70
92
|
});
|
|
71
93
|
} catch (err) {
|
|
72
94
|
console.error(formatError({ message: memberPipeline.describeError(err) }));
|
|
@@ -84,6 +106,10 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
84
106
|
{
|
|
85
107
|
env: args.env,
|
|
86
108
|
...(args.promoteTo ? { promoteTo: args.promoteTo } : {}),
|
|
109
|
+
// #4030: the job passes --allow-skip through, so it is not refused again at deploy time.
|
|
110
|
+
...(args.promoteTo && args.allowSkip
|
|
111
|
+
? { promoteCommand: ["chant", "components", "promote", "--from", args.env ?? "production", "--to", args.promoteTo, "--allow-skip"] }
|
|
112
|
+
: {}),
|
|
87
113
|
...(plan ? { member: plan.member } : {}),
|
|
88
114
|
// #3049: one job per wave of a gated-wave fan-out instead of one per component.
|
|
89
115
|
...(args.waveGate
|
|
@@ -93,6 +119,9 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
93
119
|
...(args.prLoop
|
|
94
120
|
? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } }
|
|
95
121
|
: {}),
|
|
122
|
+
// #4002: the job image and the lines that install what it lacks, chant itself included.
|
|
123
|
+
...(args.image ? { image: args.image } : {}),
|
|
124
|
+
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
96
125
|
},
|
|
97
126
|
args.sandbox,
|
|
98
127
|
paramsResolution.provenance,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import { CHANGE_SET_CONTRACT, CHANGE_SET_SCHEMA_ID, type ChangeSetDocument } from "../../change-set";
|
|
3
4
|
import { GITHUB_COMMENT_LIMIT, groupChangeSet, renderPlanSummaryMarkdown, renderPlanSummaryText } from "../../plan-summary";
|
|
4
|
-
import { formatError } from "../format";
|
|
5
|
+
import { formatError, formatWarning } from "../format";
|
|
5
6
|
import type { CommandContext } from "../registry";
|
|
6
7
|
|
|
7
8
|
const USAGE = "chant change-set summary <change-set.json> [--format text|json|markdown] [--limit <chars>]";
|
|
9
|
+
const SHOW_USAGE = "chant change-set show <digest> [--member <name>] [--json]";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* `chant change-set summary <file>` (#3188): the grouped plan summary of a
|
|
@@ -42,3 +44,49 @@ export async function runChangeSetSummary(ctx: CommandContext): Promise<number>
|
|
|
42
44
|
else process.stdout.write(renderPlanSummaryText(summary));
|
|
43
45
|
return 0;
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `chant change-set show <digest>` (#4022): the change-set document a gate
|
|
50
|
+
* bound, stored on `chant/lifecycle` under its digest, with each entry's
|
|
51
|
+
* graph node. `--json` prints the read contract's `change-set-show`
|
|
52
|
+
* document (`../../workspace/change-set-show.ts`); without it, the grouped
|
|
53
|
+
* summary and each entry with its node. `--member` reads a workspace
|
|
54
|
+
* member's ledger from anywhere in the checkout. Exit 1 when nothing is
|
|
55
|
+
* stored under the digest.
|
|
56
|
+
*/
|
|
57
|
+
export async function runChangeSetShow(ctx: CommandContext): Promise<number> {
|
|
58
|
+
const { args } = ctx;
|
|
59
|
+
const digest = args.extraPositional;
|
|
60
|
+
const { changeSetShowDocument, CHANGE_SET_DIGEST_PATTERN } = await import("../../workspace/change-set-show");
|
|
61
|
+
if (!digest || !CHANGE_SET_DIGEST_PATTERN.test(digest)) {
|
|
62
|
+
console.error(formatError({ message: digest ? `${digest} is not a change-set digest` : "change-set show needs the digest a gate binds", hint: SHOW_USAGE }));
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
if ((args.members?.length ?? 0) > 1) {
|
|
66
|
+
console.error(formatError({ message: `change-set show reads one member, not ${args.members!.join(", ")}`, hint: SHOW_USAGE }));
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const member = args.members?.[0]?.split("/").filter(Boolean);
|
|
70
|
+
const doc = await changeSetShowDocument({ cwd: resolve("."), digest, ...(member ? { member } : {}) });
|
|
71
|
+
if (args.json) {
|
|
72
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
73
|
+
return "error" in doc || doc.changeSet === null ? 1 : 0;
|
|
74
|
+
}
|
|
75
|
+
if ("error" in doc) {
|
|
76
|
+
console.error(formatError({ message: doc.error.message, hint: SHOW_USAGE }));
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
if (doc.reason) console.error(formatWarning({ message: doc.reason.message }));
|
|
80
|
+
if (doc.changeSet === null) {
|
|
81
|
+
if (!doc.reason) console.error(formatError({ message: `no change set is stored under ${digest}${doc.member ? ` for member ${doc.member}` : ""}`, hint: "A pull-request or release gate stores the change set it binds; a component gate binds a lifecycle plan and stores none." }));
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(renderPlanSummaryText(groupChangeSet(doc.changeSet)));
|
|
85
|
+
const graph = doc.changeSet.graph;
|
|
86
|
+
console.log(graph ? `\nNodes from the graph of ${graph.member ?? "this project"}${graph.at ? ` at ${graph.at.slice(0, 12)}` : ""}:` : "\nNo graph was read, so no entry has a node.");
|
|
87
|
+
for (const e of doc.changeSet.entries) {
|
|
88
|
+
if (e.action === "no-op" || e.action === "read") continue;
|
|
89
|
+
console.log(` ${e.action.padEnd(8)} ${e.member} ${e.address}${e.deposed ? ` (deposed ${e.deposed})` : ""} ${e.node ?? "(no node)"}`);
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
@@ -52,6 +52,7 @@ const sealedGates = new Set<string>();
|
|
|
52
52
|
const { CapabilityRegistry } = await import("../../components/capability");
|
|
53
53
|
const { memoryGateLedgerPort } = await import("../../op/gate");
|
|
54
54
|
const { runComponentsFanOut } = await import("./fan-out");
|
|
55
|
+
const { checkRunEventLines } = await import("../../workspace/__fixtures__/run-event-check");
|
|
55
56
|
|
|
56
57
|
let gateLedger = memoryGateLedgerPort();
|
|
57
58
|
|
|
@@ -622,3 +623,50 @@ describe("gated waves (#3049): one gate per wave, bound to the wave's set digest
|
|
|
622
623
|
expect(stderr()).toContain("--wave runs one wave");
|
|
623
624
|
});
|
|
624
625
|
});
|
|
626
|
+
|
|
627
|
+
describe("--events-json (#4041)", () => {
|
|
628
|
+
const lines = () => stdoutLines.filter((l) => l.startsWith('{"$schema"'));
|
|
629
|
+
|
|
630
|
+
test("a fan-out over the set: the gate is a gate phase settled gated, and nothing after it starts", async () => {
|
|
631
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", eventsJson: true }))).toBe(3);
|
|
632
|
+
const events = checkRunEventLines(lines());
|
|
633
|
+
expect(events[0]).toMatchObject({ type: "run-start", kind: "component", waves: [["net"], ["cluster-a", "cluster-b"], ["app-one", "app-three", "app-two"]] });
|
|
634
|
+
expect(events.map((e) => e.type)).toEqual(["run-start", "phase-start", "step-start", "gate-waiting", "step-settle", "phase-settle", "run-settle"]);
|
|
635
|
+
expect(events.find((e) => e.type === "gate-waiting")).toMatchObject({ component: null, phase: "gate", step: "gate:release", op: "fan-out", gate: "release" });
|
|
636
|
+
expect(events.at(-1)).toMatchObject({ status: "gated" });
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("approved, the gate resolves and every wave and component follows, each step started before it settles", async () => {
|
|
640
|
+
await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release" }));
|
|
641
|
+
const pending = gateLedger.appended[0];
|
|
642
|
+
gateLedger = memoryGateLedgerPort({
|
|
643
|
+
pending: [pending],
|
|
644
|
+
resolutions: [{
|
|
645
|
+
version: 1, kind: "resolution", op: "fan-out", gate: "release", resolvedBy: "operator",
|
|
646
|
+
timestamp: new Date(Date.parse(pending.timestamp) + 1000).toISOString(), planDigest: pending.planDigest,
|
|
647
|
+
}],
|
|
648
|
+
});
|
|
649
|
+
stdoutLines = [];
|
|
650
|
+
failing = ["cluster-b"];
|
|
651
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", eventsJson: true }))).toBe(1);
|
|
652
|
+
const events = checkRunEventLines(lines());
|
|
653
|
+
expect(events.find((e) => e.type === "gate-resolved")).toMatchObject({ gate: "release", resolvedBy: "operator" });
|
|
654
|
+
expect(events.filter((e) => e.type === "wave-start").map((e) => (e as { wave: number }).wave)).toEqual([1, 2, 3]);
|
|
655
|
+
expect(events.find((e) => e.type === "component-settle" && e.component === "cluster-b")).toMatchObject({ status: "failed" });
|
|
656
|
+
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("--progress-json on a set the gate stops now prints run-start and run-done gated (a level-0 change)", async () => {
|
|
660
|
+
expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", progressJson: true }))).toBe(3);
|
|
661
|
+
const progress = stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l));
|
|
662
|
+
expect(progress).toEqual([
|
|
663
|
+
{ type: "run-start", waves: [["net"], ["cluster-a", "cluster-b"], ["app-one", "app-three", "app-two"]] },
|
|
664
|
+
{ type: "run-done", status: "gated" },
|
|
665
|
+
]);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("beside --progress-json it is refused", async () => {
|
|
669
|
+
expect(await runComponentsFanOut(ctx({ base: "main", eventsJson: true, progressJson: true }))).toBe(1);
|
|
670
|
+
expect(stderr()).toContain("--events-json and --progress-json both stream to stdout");
|
|
671
|
+
});
|
|
672
|
+
});
|
|
@@ -52,6 +52,7 @@ import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../..
|
|
|
52
52
|
import { describeChangedWave, withWaveRecord, type WaveRecord } from "../../gated-waves";
|
|
53
53
|
import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
|
|
54
54
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
55
|
+
import { openCliRunEvents, refusesBothStreams } from "../run-events";
|
|
55
56
|
import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
|
|
56
57
|
import { approveCommand, gateIsSealed } from "../../op/gate";
|
|
57
58
|
import { getHeadCommit } from "../../lifecycle/git";
|
|
@@ -60,7 +61,7 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
|
60
61
|
import { formatError, formatInfo, formatWarning } from "../format";
|
|
61
62
|
import { FAN_OUT_GATE_OP } from "../../op/gate-name";
|
|
62
63
|
import { resolveBuildRoot } from "./lifecycle";
|
|
63
|
-
import { GATED_EXIT_CODE, recordAutoReleasesForRun } from "./run";
|
|
64
|
+
import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run";
|
|
64
65
|
import type { CommandContext } from "../registry";
|
|
65
66
|
|
|
66
67
|
/**
|
|
@@ -172,10 +173,11 @@ export async function resolveChangedUnits(
|
|
|
172
173
|
* chant components fan-out --base <ref> | --from-affected <file>
|
|
173
174
|
* [--head <ref>] [--include-dependents] [--env <env>] [--gate <name>]
|
|
174
175
|
* [--dry-run] [--json] [--resume <file>] [--seed-outputs <file>]
|
|
175
|
-
* [--dump-outputs <file>] [--progress-json]
|
|
176
|
+
* [--dump-outputs <file>] [--progress-json | --events-json]
|
|
176
177
|
*/
|
|
177
178
|
export async function runComponentsFanOut(ctx: CommandContext): Promise<number> {
|
|
178
179
|
const { args } = ctx;
|
|
180
|
+
if (refusesBothStreams(args)) return 1;
|
|
179
181
|
const projectPath = resolve(".");
|
|
180
182
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
181
183
|
|
|
@@ -334,6 +336,11 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
334
336
|
writeFanOutAttempt(resumePath, attempt);
|
|
335
337
|
};
|
|
336
338
|
|
|
339
|
+
// The run id the release records carry, chosen before the run so that
|
|
340
|
+
// `--events-json` (#4041) names the run by it too.
|
|
341
|
+
const release = autoReleaseRun(`fan-out-${Date.now()}`);
|
|
342
|
+
const events = await openCliRunEvents(args, { kind: "component", name: `fan-out ${derived.plan.order.join(",")}`, runId: release.runId, cwd: projectPath });
|
|
343
|
+
|
|
337
344
|
let result;
|
|
338
345
|
try {
|
|
339
346
|
result = await runFanOut(derived.plan, derived.components, registry, {
|
|
@@ -364,6 +371,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
364
371
|
}
|
|
365
372
|
: {}),
|
|
366
373
|
...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
|
|
374
|
+
...(events ? { onRunEvent: events.emit } : {}),
|
|
367
375
|
...(resumePath
|
|
368
376
|
? {
|
|
369
377
|
onComponentSettled: (settled, outputs) => {
|
|
@@ -379,6 +387,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
379
387
|
: {}),
|
|
380
388
|
});
|
|
381
389
|
} catch (err) {
|
|
390
|
+
events?.settle("failed", err instanceof Error ? err.message : String(err));
|
|
382
391
|
// `--wave <n>` reached before an earlier wave applied: nothing ran.
|
|
383
392
|
if (err instanceof EarlierWaveNotAppliedError) {
|
|
384
393
|
console.error(formatError({ message: err.message }));
|
|
@@ -386,6 +395,8 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
386
395
|
}
|
|
387
396
|
throw err;
|
|
388
397
|
}
|
|
398
|
+
// The fan-out settles the stream; this only ends one it left open.
|
|
399
|
+
events?.settle(result.status === "fail" ? "failed" : result.status);
|
|
389
400
|
|
|
390
401
|
if (args.dumpOutputs) {
|
|
391
402
|
const dumpPath = resolve(args.dumpOutputs);
|
|
@@ -420,8 +431,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
|
|
|
420
431
|
await recordAutoReleasesForRun(
|
|
421
432
|
result.results,
|
|
422
433
|
args.env ?? "local",
|
|
423
|
-
|
|
434
|
+
release.runId,
|
|
424
435
|
resolveAutoReleaseDisabled(config, args.noReleaseRecord),
|
|
436
|
+
release.runOrigin,
|
|
425
437
|
);
|
|
426
438
|
}
|
|
427
439
|
|
|
@@ -850,6 +850,43 @@ describe("runApprove — the resolution names the plan it approves (#2300)", ()
|
|
|
850
850
|
errSpy.mockRestore();
|
|
851
851
|
});
|
|
852
852
|
|
|
853
|
+
// #4020: the resolution names the commit and run the gate waited on, off the fact it answers.
|
|
854
|
+
test("it copies the standing fact's commit and run onto the resolution", async () => {
|
|
855
|
+
readGateLedgerMock.mockResolvedValue({
|
|
856
|
+
resolutions: [],
|
|
857
|
+
pending: [{
|
|
858
|
+
version: 1, kind: "pending", op: "fountain-apply", gate: "rollout-gate",
|
|
859
|
+
timestamp: "2026-01-01T00:00:00.000Z", expiresAt: "2099-01-01T00:00:00.000Z",
|
|
860
|
+
planDigest: PLAN_A, gitSha: "f".repeat(40), runId: "9001",
|
|
861
|
+
}],
|
|
862
|
+
malformed: 0,
|
|
863
|
+
});
|
|
864
|
+
appendGateResolutionMock.mockResolvedValue({
|
|
865
|
+
commit: "sha",
|
|
866
|
+
record: { version: 1, op: "fountain-apply", gate: "rollout-gate", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z" },
|
|
867
|
+
});
|
|
868
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
869
|
+
|
|
870
|
+
expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "alex" }))).toBe(0);
|
|
871
|
+
expect(appendGateResolutionMock).toHaveBeenCalledWith(expect.objectContaining({ gitSha: "f".repeat(40), runId: "9001" }));
|
|
872
|
+
errSpy.mockRestore();
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test("a standing fact from before the commit was recorded gives a resolution without one", async () => {
|
|
876
|
+
seedPending("fountain-apply", "rollout-gate", PLAN_A);
|
|
877
|
+
appendGateResolutionMock.mockResolvedValue({
|
|
878
|
+
commit: "sha",
|
|
879
|
+
record: { version: 1, op: "fountain-apply", gate: "rollout-gate", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z" },
|
|
880
|
+
});
|
|
881
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
882
|
+
|
|
883
|
+
expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "alex" }))).toBe(0);
|
|
884
|
+
const written = appendGateResolutionMock.mock.calls.at(-1)![0] as Record<string, unknown>;
|
|
885
|
+
expect(written).not.toHaveProperty("gitSha");
|
|
886
|
+
expect(written).not.toHaveProperty("runId");
|
|
887
|
+
errSpy.mockRestore();
|
|
888
|
+
});
|
|
889
|
+
|
|
853
890
|
test("--plan names one explicitly, and wins over the standing fact's", async () => {
|
|
854
891
|
const other = `sha256:${"b".repeat(64)}`;
|
|
855
892
|
seedPending("fountain-apply", "rollout-gate", PLAN_A);
|