@intentius/chant 0.124.26 → 0.124.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/dist/__golden__/wave-loop/op-held-on.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/dist/__golden__/wave-loop/op-per-run.json +7 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
- package/dist/__golden__/wave-loop/op-sealed.json +3 -0
- package/dist/__golden__/wave-loop/op-spend.json +3 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/dist/change-set-graph.d.ts +56 -0
- package/dist/change-set-graph.d.ts.map +1 -0
- package/dist/change-set-graph.js +128 -0
- package/dist/change-set.d.ts +52 -3
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +35 -4
- package/dist/chant-version.d.ts +9 -0
- package/dist/chant-version.d.ts.map +1 -0
- package/dist/chant-version.js +16 -0
- package/dist/cli/commands/build.d.ts +8 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -8
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.js +30 -2
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +13 -2
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.js +17 -8
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +19 -4
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +28 -0
- package/dist/cli/handlers/change-set.d.ts +10 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.js +51 -1
- package/dist/cli/handlers/fan-out.d.ts +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +14 -3
- package/dist/cli/handlers/pr.d.ts +8 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +148 -24
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +256 -21
- package/dist/cli/handlers/release.d.ts +34 -0
- package/dist/cli/handlers/release.d.ts.map +1 -0
- package/dist/cli/handlers/release.js +203 -0
- package/dist/cli/handlers/run.d.ts +28 -2
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +170 -13
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +88 -13
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +49 -3
- package/dist/cli/registry.d.ts +29 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +29 -0
- package/dist/cli/run-events.d.ts.map +1 -0
- package/dist/cli/run-events.js +47 -0
- package/dist/cli/version.d.ts +3 -5
- package/dist/cli/version.d.ts.map +1 -1
- package/dist/cli/version.js +3 -13
- package/dist/codegen/docs-in-site.d.ts +71 -0
- package/dist/codegen/docs-in-site.d.ts.map +1 -0
- package/dist/codegen/docs-in-site.js +177 -0
- package/dist/codegen/docs-types.d.ts +14 -1
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +18 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/docs.js +40 -7
- package/dist/codegen/rehype-base-url.mjs +15 -9
- package/dist/components/auto-release.d.ts +4 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +2 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +10 -9
- package/dist/components/driver.d.ts +42 -9
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +81 -54
- package/dist/components/fan-out-run.d.ts +7 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +35 -26
- package/dist/components/pr-run.d.ts +11 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +20 -1
- package/dist/components/promote-order.d.ts +38 -0
- package/dist/components/promote-order.d.ts.map +1 -0
- package/dist/components/promote-order.js +62 -0
- package/dist/components/promote.d.ts +15 -2
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +17 -0
- package/dist/components/release-compile.d.ts +133 -0
- package/dist/components/release-compile.d.ts.map +1 -0
- package/dist/components/release-compile.js +376 -0
- package/dist/components/release-gate.d.ts +125 -0
- package/dist/components/release-gate.d.ts.map +1 -0
- package/dist/components/release-gate.js +282 -0
- package/dist/components/release-promote.d.ts +96 -0
- package/dist/components/release-promote.d.ts.map +1 -0
- package/dist/components/release-promote.js +204 -0
- package/dist/components/run-progress.d.ts +7 -0
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +3 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lifecycle/change-set-store.d.ts +67 -0
- package/dist/lifecycle/change-set-store.d.ts.map +1 -0
- package/dist/lifecycle/change-set-store.js +96 -0
- package/dist/lifecycle/commit-sha.d.ts +13 -0
- package/dist/lifecycle/commit-sha.d.ts.map +1 -0
- package/dist/lifecycle/commit-sha.js +21 -0
- package/dist/lifecycle/gate-change-set.d.ts +69 -0
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
- package/dist/lifecycle/gate-change-set.js +106 -0
- package/dist/lifecycle/gate-ledger.d.ts +49 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +18 -0
- package/dist/lifecycle/release-cut.d.ts +107 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -0
- package/dist/lifecycle/release-cut.js +273 -0
- package/dist/lifecycle/release-ledger.d.ts +54 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +53 -20
- package/dist/lifecycle/release-store.d.ts +190 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -0
- package/dist/lifecycle/release-store.js +318 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +2 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.js +4 -2
- package/dist/op/activities/source-release.d.ts.map +1 -1
- package/dist/op/activities/source-release.js +7 -3
- package/dist/op/activities/source-rollback.d.ts.map +1 -1
- package/dist/op/activities/source-rollback.js +7 -3
- package/dist/op/activity-registry.d.ts +6 -3
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.js +7 -5
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +14 -7
- package/dist/op/gate-name.d.ts +11 -2
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-name.js +12 -3
- package/dist/op/gate-run-locator.d.ts +22 -0
- package/dist/op/gate-run-locator.d.ts.map +1 -1
- package/dist/op/gate-run-locator.js +31 -0
- package/dist/op/gate.d.ts +27 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +57 -3
- package/dist/op/local-executor.d.ts +36 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +158 -52
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -0
- package/dist/op/run-live.d.ts +2 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/run-live.js +1 -1
- package/dist/op/runtime.d.ts +46 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +12 -2
- package/dist/point-decision.d.ts +2 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +9 -2
- package/dist/run-event-store.d.ts +77 -0
- package/dist/run-event-store.d.ts.map +1 -0
- package/dist/run-event-store.js +204 -0
- package/dist/run-event-stream.d.ts +132 -0
- package/dist/run-event-stream.d.ts.map +1 -0
- package/dist/run-event-stream.js +238 -0
- package/dist/run-event.d.ts +267 -0
- package/dist/run-event.d.ts.map +1 -0
- package/dist/run-event.js +207 -0
- package/dist/wave-runtime.mjs +225 -242
- package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
- package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/run-event-check.js +59 -0
- package/dist/workspace/change-set-show.d.ts +56 -0
- package/dist/workspace/change-set-show.d.ts.map +1 -0
- package/dist/workspace/change-set-show.js +54 -0
- package/dist/workspace/change-set-show.schema.json +102 -0
- package/dist/workspace/change-set.schema.json +28 -1
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes-cli.js +10 -1
- package/dist/workspace/changes.d.ts +37 -0
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/changes.js +49 -2
- package/dist/workspace/changes.schema.json +135 -0
- package/dist/workspace/ci-green-cli.d.ts +19 -4
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +75 -12
- package/dist/workspace/ci-green-forge.d.ts +20 -3
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +6 -4
- package/dist/workspace/ci-green-workflow.d.ts +11 -0
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +6 -0
- package/dist/workspace/ci-green.d.ts +273 -13
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +449 -38
- package/dist/workspace/ci-last-green.schema.json +32 -2
- package/dist/workspace/ci-list.schema.json +525 -0
- package/dist/workspace/commit-links.d.ts +85 -0
- package/dist/workspace/commit-links.d.ts.map +1 -0
- package/dist/workspace/commit-links.js +163 -0
- package/dist/workspace/declaration.d.ts +19 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +1 -1
- package/dist/workspace/declaration.schema.json +12 -0
- package/dist/workspace/identity-gate-rule.d.ts +37 -0
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +72 -7
- package/dist/workspace/identity.d.ts +12 -0
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/identity.js +13 -1
- package/dist/workspace/intent.d.ts +6 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +51 -1
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +2 -0
- package/dist/workspace/ls.schema.json +3 -1
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +6 -0
- package/dist/workspace/promotion-order.d.ts +41 -0
- package/dist/workspace/promotion-order.d.ts.map +1 -0
- package/dist/workspace/promotion-order.js +95 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +4 -1
- package/dist/workspace/releases.d.ts +72 -0
- package/dist/workspace/releases.d.ts.map +1 -0
- package/dist/workspace/releases.js +94 -0
- package/dist/workspace/releases.schema.json +391 -0
- package/dist/workspace/run-event.schema.json +288 -0
- package/dist/workspace/status-gates.d.ts +26 -1
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +31 -2
- package/dist/workspace/status-stewards.d.ts +4 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +2 -0
- package/dist/workspace/status.d.ts +39 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +48 -1
- package/dist/workspace/status.schema.json +70 -1
- package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
- package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
- package/dist/workspace/trust/ci-tag-statement.js +123 -0
- package/dist/workspace/trust/op-run-statement.d.ts +91 -0
- package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/op-run-statement.js +143 -0
- package/dist/workspace/trust/rotation.d.ts +18 -2
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.js +65 -16
- package/dist/workspace/trust/seal.d.ts +11 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +9 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/ssh-commit.js +10 -10
- package/package.json +1 -1
- package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/src/__golden__/wave-loop/fan-out-only.json +6 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/src/__golden__/wave-loop/op-cost-member.json +3 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/src/__golden__/wave-loop/op-held-on.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/src/__golden__/wave-loop/op-per-run.json +7 -0
- package/src/__golden__/wave-loop/op-pr-review.json +7 -0
- package/src/__golden__/wave-loop/op-sealed.json +3 -0
- package/src/__golden__/wave-loop/op-spend.json +3 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/src/change-set-graph.test.ts +72 -0
- package/src/change-set-graph.ts +133 -0
- package/src/change-set.ts +76 -6
- package/src/chant-version.ts +16 -0
- package/src/cli/commands/build.ts +19 -8
- package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
- package/src/cli/commands/check-lexicon-docs.ts +39 -2
- package/src/cli/commands/check-lexicon.ts +14 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
- package/src/cli/commands/init-lexicon.test.ts +25 -0
- package/src/cli/commands/init-lexicon.ts +18 -8
- package/src/cli/compiled-cli.e2e.test.ts +31 -6
- package/src/cli/handlers/approve-release.test.ts +80 -0
- package/src/cli/handlers/approve.ts +18 -2
- package/src/cli/handlers/build.test.ts +52 -0
- package/src/cli/handlers/build.ts +29 -0
- package/src/cli/handlers/change-set.ts +49 -1
- package/src/cli/handlers/fan-out.test.ts +48 -0
- package/src/cli/handlers/fan-out.ts +15 -3
- package/src/cli/handlers/operator.test.ts +37 -0
- package/src/cli/handlers/pr.test.ts +194 -0
- package/src/cli/handlers/pr.ts +168 -34
- package/src/cli/handlers/promote.test.ts +389 -5
- package/src/cli/handlers/promote.ts +279 -20
- package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
- package/src/cli/handlers/release.e2e.test.ts +85 -0
- package/src/cli/handlers/release.ts +206 -0
- package/src/cli/handlers/run-auto-release.test.ts +71 -0
- package/src/cli/handlers/run-events-cmd.test.ts +92 -0
- package/src/cli/handlers/run.test.ts +30 -0
- package/src/cli/handlers/run.ts +176 -15
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +88 -13
- package/src/cli/mcp/never-over-mcp.test.ts +2 -0
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +37 -3
- package/src/cli/registry.ts +29 -3
- package/src/cli/run-events.ts +54 -0
- package/src/cli/version.ts +3 -13
- package/src/codegen/docs-in-site.test.ts +166 -0
- package/src/codegen/docs-in-site.ts +182 -0
- package/src/codegen/docs-site/AgentPrompt.astro +26 -1
- package/src/codegen/docs-types.ts +14 -1
- package/src/codegen/docs.ts +60 -7
- package/src/codegen/rehype-base-url.d.mts +3 -1
- package/src/codegen/rehype-base-url.mjs +15 -9
- package/src/codegen/rehype-base-url.test.ts +34 -0
- package/src/components/auto-release.test.ts +21 -0
- package/src/components/auto-release.ts +6 -1
- package/src/components/cli-support.test.ts +82 -0
- package/src/components/cli-support.ts +16 -9
- package/src/components/driver.ts +129 -56
- package/src/components/fan-out-run.ts +56 -35
- package/src/components/pr-run.ts +30 -1
- package/src/components/promote-order.test.ts +90 -0
- package/src/components/promote-order.ts +75 -0
- package/src/components/promote.test.ts +10 -1
- package/src/components/promote.ts +26 -1
- package/src/components/release-compile.test.ts +147 -0
- package/src/components/release-compile.ts +445 -0
- package/src/components/release-gate.test.ts +215 -0
- package/src/components/release-gate.ts +341 -0
- package/src/components/release-promote.test.ts +159 -0
- package/src/components/release-promote.ts +232 -0
- package/src/components/run-progress.ts +7 -0
- package/src/components/wave-plan.ts +1 -1
- package/src/config.ts +7 -1
- package/src/index.ts +2 -0
- package/src/lifecycle/change-set-store.test.ts +217 -0
- package/src/lifecycle/change-set-store.ts +112 -0
- package/src/lifecycle/commit-sha.ts +23 -0
- package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
- package/src/lifecycle/gate-change-set.ts +133 -0
- package/src/lifecycle/gate-ledger.test.ts +33 -0
- package/src/lifecycle/gate-ledger.ts +66 -1
- package/src/lifecycle/release-cut-signed.test.ts +180 -0
- package/src/lifecycle/release-cut.test.ts +411 -0
- package/src/lifecycle/release-cut.ts +367 -0
- package/src/lifecycle/release-ledger.test.ts +14 -0
- package/src/lifecycle/release-ledger.ts +85 -17
- package/src/lifecycle/release-store.ts +423 -0
- package/src/lifecycle/run-ledger.test.ts +10 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/op/activities/change-coverage.ts +4 -1
- package/src/op/activities/source-release.ts +8 -3
- package/src/op/activities/source-rollback.ts +8 -4
- package/src/op/activity-registry.ts +7 -5
- package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
- package/src/op/discover-root.test.ts +39 -0
- package/src/op/discover.ts +15 -8
- package/src/op/gate-name.ts +13 -3
- package/src/op/gate-release.test.ts +126 -0
- package/src/op/gate-run-locator.test.ts +110 -0
- package/src/op/gate-run-locator.ts +35 -0
- package/src/op/gate.ts +88 -6
- package/src/op/local-executor.ts +207 -52
- package/src/op/op-waves-run.ts +4 -0
- package/src/op/run-events.test.ts +166 -0
- package/src/op/run-live.ts +1 -1
- package/src/op/runtime.ts +39 -0
- package/src/op/runtimes/local.test.ts +16 -0
- package/src/op/runtimes/local.ts +13 -2
- package/src/point-decision.ts +12 -2
- package/src/run-event-store.test.ts +144 -0
- package/src/run-event-store.ts +213 -0
- package/src/run-event-stream.test.ts +175 -0
- package/src/run-event-stream.ts +306 -0
- package/src/run-event.test.ts +277 -0
- package/src/run-event.ts +451 -0
- package/src/wave-loop-golden.test.ts +9 -0
- package/src/workspace/__fixtures__/run-event-check.ts +58 -0
- package/src/workspace/ancestry-index.test.ts +93 -0
- package/src/workspace/change-set-show.schema.json +102 -0
- package/src/workspace/change-set-show.ts +72 -0
- package/src/workspace/change-set.schema.json +28 -1
- package/src/workspace/changes-cli.ts +10 -1
- package/src/workspace/changes.schema.json +135 -0
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/changes.ts +84 -2
- package/src/workspace/ci-green-cli.ts +75 -13
- package/src/workspace/ci-green-forge.ts +24 -4
- package/src/workspace/ci-green-runs.test.ts +242 -0
- package/src/workspace/ci-green-signed.test.ts +302 -0
- package/src/workspace/ci-green-workflow.test.ts +16 -0
- package/src/workspace/ci-green-workflow.ts +12 -0
- package/src/workspace/ci-green.test.ts +131 -2
- package/src/workspace/ci-green.ts +624 -42
- package/src/workspace/ci-last-green.schema.json +32 -2
- package/src/workspace/ci-list.schema.json +525 -0
- package/src/workspace/commit-links.test.ts +279 -0
- package/src/workspace/commit-links.ts +183 -0
- package/src/workspace/declaration.schema.json +12 -0
- package/src/workspace/declaration.ts +22 -2
- package/src/workspace/identity-gate-rule.ts +91 -7
- package/src/workspace/identity.ts +14 -1
- package/src/workspace/intent.ts +48 -1
- package/src/workspace/ls.schema.json +3 -1
- package/src/workspace/ls.ts +6 -0
- package/src/workspace/member-pipeline.test.ts +17 -0
- package/src/workspace/member-pipeline.ts +9 -0
- package/src/workspace/promotion-order.ts +97 -0
- package/src/workspace/read-contract.e2e.test.ts +4 -1
- package/src/workspace/reason-codes.test.ts +2 -1
- package/src/workspace/reason-codes.ts +4 -1
- package/src/workspace/releases.schema.json +391 -0
- package/src/workspace/releases.test.ts +108 -0
- package/src/workspace/releases.ts +122 -0
- package/src/workspace/run-event.schema.json +288 -0
- package/src/workspace/status-contract.test.ts +52 -1
- package/src/workspace/status-gates.ts +56 -1
- package/src/workspace/status-stewards.ts +6 -0
- package/src/workspace/status.schema.json +70 -1
- package/src/workspace/status.ts +80 -1
- package/src/workspace/trust/ci-tag-statement.ts +151 -0
- package/src/workspace/trust/op-run-statement.ts +148 -0
- package/src/workspace/trust/rotation.ts +61 -14
- package/src/workspace/trust/seal.ts +10 -0
- package/src/workspace/trust/ssh-commit.ts +10 -9
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release (#4026, ws-104): one green commit of one member, compiled for
|
|
3
|
+
* every environment the member declares, stored on `chant/lifecycle`.
|
|
4
|
+
*
|
|
5
|
+
* A release is a different object from a deploy record. A deploy record is
|
|
6
|
+
* one line of `<env>/releases.jsonl` (./release-ledger.ts) saying a component
|
|
7
|
+
* was deployed. A release is what is cut once from a green commit and then
|
|
8
|
+
* moves through the environments: the commit, the artifacts built once from
|
|
9
|
+
* it, and one compile target per environment, each with the template digests
|
|
10
|
+
* and the build parameters that environment was compiled with.
|
|
11
|
+
*
|
|
12
|
+
* Storage, per ws-104 question 3:
|
|
13
|
+
*
|
|
14
|
+
* _releases/<releaseDigest>.json the release, written once and never rewritten
|
|
15
|
+
* _releases/cuts.jsonl one line per cut, with the release's label
|
|
16
|
+
* _releases/templates/<digest>.txt each template's bytes, by content digest
|
|
17
|
+
*
|
|
18
|
+
* `:` is written as `_` in file names, as `_plans/` and `_builds/` do. A
|
|
19
|
+
* workspace member's copy sits under `_members/<member>/` through the same
|
|
20
|
+
* `ledgerDir` prefix every other store uses.
|
|
21
|
+
*
|
|
22
|
+
* The template bytes are kept so that a promote under a release deploys the
|
|
23
|
+
* templates the cut recorded, not whatever the promoting checkout holds
|
|
24
|
+
* (ws-104 question 5). They are named by content digest, so a template that
|
|
25
|
+
* two environments or two releases share is stored once.
|
|
26
|
+
*
|
|
27
|
+
* `releaseDigest` is `computePlanDigest("release", subject)` over the member,
|
|
28
|
+
* the commit, the artifact digests and each target's env, templates and the
|
|
29
|
+
* name and value of each param. A param's `source`, the label, the actor, the
|
|
30
|
+
* time and any signature stay outside it, so two cuts of one commit give the
|
|
31
|
+
* same digest.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { sortedJsonReplacer } from "../utils";
|
|
35
|
+
import { contentDigest } from "../content-digest";
|
|
36
|
+
import { computePlanDigest } from "./plan-digest";
|
|
37
|
+
import {
|
|
38
|
+
ledgerDir,
|
|
39
|
+
listFilesInDir,
|
|
40
|
+
readBlobBySha,
|
|
41
|
+
readBlobFromPath,
|
|
42
|
+
readPathSha,
|
|
43
|
+
RefCASConflictError,
|
|
44
|
+
writeLedgerFiles,
|
|
45
|
+
type LedgerFileWrite,
|
|
46
|
+
type LedgerLocation,
|
|
47
|
+
} from "./git";
|
|
48
|
+
import type { RunOrigin } from "./release-ledger";
|
|
49
|
+
|
|
50
|
+
/** The directory on `chant/lifecycle` that holds releases, cuts and template bytes. */
|
|
51
|
+
export const RELEASES_DIR = "_releases";
|
|
52
|
+
/** The cut log inside {@link RELEASES_DIR}. */
|
|
53
|
+
export const CUTS_FILE = "cuts.jsonl";
|
|
54
|
+
/** The directory inside {@link RELEASES_DIR} that holds template bytes by content digest. */
|
|
55
|
+
export const TEMPLATES_SUBDIR = "templates";
|
|
56
|
+
|
|
57
|
+
/** The `kind` a release digest is computed under, so it can never equal a plan digest of another kind. */
|
|
58
|
+
export const RELEASE_DIGEST_KIND = "release";
|
|
59
|
+
|
|
60
|
+
/** An artifact built once from the release's commit: an `image` or `asset` archive entry. */
|
|
61
|
+
export interface ReleaseArtifact {
|
|
62
|
+
component: string;
|
|
63
|
+
kind: "image" | "asset";
|
|
64
|
+
/** The archive path the build step wrote. */
|
|
65
|
+
path: string;
|
|
66
|
+
digest: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One template of a target: a file a component's deploy reads, compiled for the target's environment. */
|
|
70
|
+
export interface ReleaseTemplate {
|
|
71
|
+
/** The component whose deploy reads it, or null for a project with no components. */
|
|
72
|
+
component: string | null;
|
|
73
|
+
/** The path the deploy step names, relative to the member's directory. */
|
|
74
|
+
path: string;
|
|
75
|
+
/** `sha256:<hex>` of the file's bytes. */
|
|
76
|
+
digest: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One resolved build parameter of a target. */
|
|
80
|
+
export interface ReleaseParam {
|
|
81
|
+
name: string;
|
|
82
|
+
value: string | number | boolean;
|
|
83
|
+
/** Where the value came from: `cli`, `params-file`, `env` or `default`. Outside the digest. */
|
|
84
|
+
source: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** What one environment compiles to. */
|
|
88
|
+
export interface ReleaseTarget {
|
|
89
|
+
env: string;
|
|
90
|
+
templates: ReleaseTemplate[];
|
|
91
|
+
params: ReleaseParam[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The release file, `_releases/<releaseDigest>.json`. */
|
|
95
|
+
export interface Release {
|
|
96
|
+
version: 1;
|
|
97
|
+
releaseDigest: string;
|
|
98
|
+
/** The workspace member, its path of names joined with `/` when nested, or null outside a workspace. */
|
|
99
|
+
member: string | null;
|
|
100
|
+
/** The full sha of the green commit. */
|
|
101
|
+
commit: string;
|
|
102
|
+
artifactDigests: ReleaseArtifact[];
|
|
103
|
+
targets: ReleaseTarget[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One line of `_releases/cuts.jsonl`. */
|
|
107
|
+
export interface ReleaseCut {
|
|
108
|
+
version: 1;
|
|
109
|
+
releaseDigest: string;
|
|
110
|
+
/** The human name of the release, such as `v1.4.0`, or null for a cut with none. */
|
|
111
|
+
label: string | null;
|
|
112
|
+
actor: string;
|
|
113
|
+
timestamp: string;
|
|
114
|
+
/** The CI run the cut ran in, or null outside CI. */
|
|
115
|
+
runOrigin: RunOrigin | null;
|
|
116
|
+
/**
|
|
117
|
+
* The cut's signature (#4031): an armored ssh signature by the actor's key
|
|
118
|
+
* in the `chant-release` namespace over the release digest, the label, the
|
|
119
|
+
* actor and the time, one per line (`releaseSealPayload`). Null for an
|
|
120
|
+
* unsigned cut, and on every cut written before 0.124.28.
|
|
121
|
+
*/
|
|
122
|
+
signature: string | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The parts of a release a digest is computed from. */
|
|
126
|
+
export type ReleaseSubject = Pick<Release, "member" | "commit" | "artifactDigests" | "targets">;
|
|
127
|
+
|
|
128
|
+
const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
129
|
+
|
|
130
|
+
/** The same release with every list in the order ws-104 sets: artifacts by component then path, targets by env, templates by component then path, params by name. */
|
|
131
|
+
export function normalizeRelease<T extends ReleaseSubject>(release: T): T {
|
|
132
|
+
return {
|
|
133
|
+
...release,
|
|
134
|
+
artifactDigests: [...release.artifactDigests]
|
|
135
|
+
.map(({ component, kind, path, digest }) => ({ component, kind, path, digest }))
|
|
136
|
+
.sort((a, b) => byString(a.component, b.component) || byString(a.path, b.path)),
|
|
137
|
+
targets: [...release.targets]
|
|
138
|
+
.map((t) => ({
|
|
139
|
+
env: t.env,
|
|
140
|
+
templates: [...t.templates]
|
|
141
|
+
.map(({ component, path, digest }) => ({ component, path, digest }))
|
|
142
|
+
.sort((a, b) => byString(a.component ?? "", b.component ?? "") || byString(a.path, b.path)),
|
|
143
|
+
params: [...t.params].map(({ name, value, source }) => ({ name, value, source })).sort((a, b) => byString(a.name, b.name)),
|
|
144
|
+
}))
|
|
145
|
+
.sort((a, b) => byString(a.env, b.env)),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The hash input of ws-104 question 3: a param's `source` is left out. */
|
|
150
|
+
export function releaseHashInput(release: ReleaseSubject): unknown {
|
|
151
|
+
const n = normalizeRelease(release);
|
|
152
|
+
return {
|
|
153
|
+
member: n.member,
|
|
154
|
+
commit: n.commit,
|
|
155
|
+
artifactDigests: n.artifactDigests,
|
|
156
|
+
targets: n.targets.map((t) => ({ env: t.env, templates: t.templates, params: t.params.map(({ name, value }) => ({ name, value })) })),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** `computePlanDigest("release", input)` over {@link releaseHashInput}. */
|
|
161
|
+
export function computeReleaseDigest(release: ReleaseSubject): string {
|
|
162
|
+
return computePlanDigest(RELEASE_DIGEST_KIND, releaseHashInput(release));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A release with its digest computed and its lists in order. */
|
|
166
|
+
export function makeRelease(subject: ReleaseSubject): Release {
|
|
167
|
+
const normalized = normalizeRelease(subject);
|
|
168
|
+
return { version: 1, releaseDigest: computeReleaseDigest(normalized), ...normalized };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Whether a release's own `releaseDigest` is the digest of what it holds. */
|
|
172
|
+
export function verifyReleaseDigest(release: Release): boolean {
|
|
173
|
+
return release.releaseDigest === computeReleaseDigest(release);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const stem = (digest: string): string => digest.replace(/:/g, "_");
|
|
177
|
+
const releaseFile = (digest: string): string => `${stem(digest)}.json`;
|
|
178
|
+
const templateFile = (digest: string): string => `${stem(digest)}.txt`;
|
|
179
|
+
|
|
180
|
+
/** A label names a release in commands such as `chant release cut <label>`, so it is one plain word. */
|
|
181
|
+
const LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
182
|
+
|
|
183
|
+
/** Why a label can't be used, or undefined when it can. */
|
|
184
|
+
export function invalidLabel(label: string): string | undefined {
|
|
185
|
+
if (LABEL_PATTERN.test(label)) return undefined;
|
|
186
|
+
return `"${label}" is not a release label: use letters, digits and . _ + -, starting with a letter or digit, at most 128 characters`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Thrown when a cut would give a label two digests, or a digest two labels. */
|
|
190
|
+
export class ReleaseLabelError extends Error {
|
|
191
|
+
constructor(
|
|
192
|
+
readonly code: "release-label-taken" | "release-already-labelled" | "release-label-invalid",
|
|
193
|
+
message: string,
|
|
194
|
+
) {
|
|
195
|
+
super(message);
|
|
196
|
+
this.name = "ReleaseLabelError";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A release's template bytes, as the cut read them. */
|
|
201
|
+
export interface TemplateContent {
|
|
202
|
+
digest: string;
|
|
203
|
+
content: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Store a release and its template bytes in one commit. A release already
|
|
208
|
+
* stored under its digest is never rewritten, and a template already stored
|
|
209
|
+
* is not written again. Does not push.
|
|
210
|
+
*/
|
|
211
|
+
export async function persistRelease(
|
|
212
|
+
release: Release,
|
|
213
|
+
templates: readonly TemplateContent[],
|
|
214
|
+
opts?: { cwd?: string },
|
|
215
|
+
): Promise<{ commit: string | null; written: boolean }> {
|
|
216
|
+
if (!verifyReleaseDigest(release)) {
|
|
217
|
+
throw new Error(`release ${release.releaseDigest} does not hash to its own digest; refusing to store it`);
|
|
218
|
+
}
|
|
219
|
+
for (const t of templates) {
|
|
220
|
+
if (contentDigest(t.content) !== t.digest) throw new Error(`template bytes do not hash to ${t.digest}; refusing to store them`);
|
|
221
|
+
}
|
|
222
|
+
const dir = await ledgerDir(RELEASES_DIR, opts);
|
|
223
|
+
const files: LedgerFileWrite[] = [];
|
|
224
|
+
const releasePath = `${dir}/${releaseFile(release.releaseDigest)}`;
|
|
225
|
+
const exists = (await readPathSha(RELEASES_DIR, releaseFile(release.releaseDigest), opts)) !== null;
|
|
226
|
+
if (!exists) files.push({ path: releasePath, content: `${JSON.stringify(release, sortedJsonReplacer)}\n`, expectPriorSha: null });
|
|
227
|
+
const seen = new Set<string>();
|
|
228
|
+
for (const t of templates) {
|
|
229
|
+
if (seen.has(t.digest)) continue;
|
|
230
|
+
seen.add(t.digest);
|
|
231
|
+
if ((await readPathSha(`${RELEASES_DIR}/${TEMPLATES_SUBDIR}`, templateFile(t.digest), opts)) !== null) continue;
|
|
232
|
+
files.push({ path: `${dir}/${TEMPLATES_SUBDIR}/${templateFile(t.digest)}`, content: t.content, expectPriorSha: null });
|
|
233
|
+
}
|
|
234
|
+
if (files.length === 0) return { commit: null, written: false };
|
|
235
|
+
try {
|
|
236
|
+
const commit = await writeLedgerFiles(files, `Release ${release.releaseDigest}`, opts);
|
|
237
|
+
return { commit, written: !exists };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// Another writer stored the same content-addressed files first.
|
|
240
|
+
if (err instanceof RefCASConflictError) return { commit: null, written: false };
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Parse a stored release file, or null when it is not a release. */
|
|
246
|
+
export function parseRelease(text: string): Release | null {
|
|
247
|
+
let parsed: unknown;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(text);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
254
|
+
const r = parsed as Partial<Release>;
|
|
255
|
+
const ok =
|
|
256
|
+
r.version === 1 &&
|
|
257
|
+
typeof r.releaseDigest === "string" &&
|
|
258
|
+
(r.member === null || typeof r.member === "string") &&
|
|
259
|
+
typeof r.commit === "string" &&
|
|
260
|
+
Array.isArray(r.artifactDigests) &&
|
|
261
|
+
r.artifactDigests.every((a) => a && typeof a.component === "string" && (a.kind === "image" || a.kind === "asset") && typeof a.path === "string" && typeof a.digest === "string") &&
|
|
262
|
+
Array.isArray(r.targets) &&
|
|
263
|
+
r.targets.every(
|
|
264
|
+
(t) =>
|
|
265
|
+
t &&
|
|
266
|
+
typeof t.env === "string" &&
|
|
267
|
+
Array.isArray(t.templates) &&
|
|
268
|
+
t.templates.every((x) => x && (x.component === null || typeof x.component === "string") && typeof x.path === "string" && typeof x.digest === "string") &&
|
|
269
|
+
Array.isArray(t.params) &&
|
|
270
|
+
t.params.every((p) => p && typeof p.name === "string" && ["string", "number", "boolean"].includes(typeof p.value) && typeof p.source === "string"),
|
|
271
|
+
);
|
|
272
|
+
return ok ? (r as Release) : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Read one release by digest. Null when none is stored, or when the file is not a release whose content hashes to its digest. */
|
|
276
|
+
export async function readRelease(digest: string, opts?: LedgerLocation): Promise<Release | null> {
|
|
277
|
+
const text = await readBlobFromPath(RELEASES_DIR, releaseFile(digest), opts);
|
|
278
|
+
if (text === null) return null;
|
|
279
|
+
const release = parseRelease(text);
|
|
280
|
+
if (!release || release.releaseDigest !== digest || !verifyReleaseDigest(release)) return null;
|
|
281
|
+
return release;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Every stored release, and how many release files could not be read as one. */
|
|
285
|
+
export async function readAllReleases(opts?: LedgerLocation): Promise<{ releases: Release[]; malformed: number }> {
|
|
286
|
+
const releases: Release[] = [];
|
|
287
|
+
let malformed = 0;
|
|
288
|
+
for (const name of await listFilesInDir(RELEASES_DIR, opts)) {
|
|
289
|
+
if (!name.endsWith(".json")) continue;
|
|
290
|
+
const digest = name.slice(0, -".json".length).replace(/_/g, ":");
|
|
291
|
+
const release = await readRelease(digest, opts);
|
|
292
|
+
if (release) releases.push(release);
|
|
293
|
+
else malformed++;
|
|
294
|
+
}
|
|
295
|
+
return { releases, malformed };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Parse the lines of `cuts.jsonl`. A line that is not a cut is counted and skipped. */
|
|
299
|
+
export function parseCuts(text: string | null): { cuts: ReleaseCut[]; malformed: number } {
|
|
300
|
+
const cuts: ReleaseCut[] = [];
|
|
301
|
+
let malformed = 0;
|
|
302
|
+
for (const line of (text ?? "").split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
303
|
+
try {
|
|
304
|
+
const c = JSON.parse(line) as Partial<ReleaseCut>;
|
|
305
|
+
const ok =
|
|
306
|
+
c &&
|
|
307
|
+
c.version === 1 &&
|
|
308
|
+
typeof c.releaseDigest === "string" &&
|
|
309
|
+
(c.label === null || typeof c.label === "string") &&
|
|
310
|
+
typeof c.actor === "string" &&
|
|
311
|
+
typeof c.timestamp === "string" &&
|
|
312
|
+
(c.runOrigin === null || (typeof c.runOrigin === "object" && typeof c.runOrigin?.forge === "string")) &&
|
|
313
|
+
(c.signature === null || c.signature === undefined || typeof c.signature === "string");
|
|
314
|
+
if (!ok) {
|
|
315
|
+
malformed++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
cuts.push({ ...(c as ReleaseCut), signature: c.signature ?? null });
|
|
319
|
+
} catch {
|
|
320
|
+
malformed++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { cuts, malformed };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Every cut, oldest first. */
|
|
327
|
+
export async function readCuts(opts?: LedgerLocation): Promise<{ cuts: ReleaseCut[]; malformed: number }> {
|
|
328
|
+
return parseCuts(await readBlobFromPath(RELEASES_DIR, CUTS_FILE, opts));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The label each digest carries, from its labelled cuts. */
|
|
332
|
+
export function labelsByDigest(cuts: readonly ReleaseCut[]): Map<string, string> {
|
|
333
|
+
const out = new Map<string, string>();
|
|
334
|
+
for (const c of cuts) if (c.label !== null && !out.has(c.releaseDigest)) out.set(c.releaseDigest, c.label);
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** What {@link appendCut} did. */
|
|
339
|
+
export type CutOutcome =
|
|
340
|
+
| { appended: true; cut: ReleaseCut }
|
|
341
|
+
/** The digest was already cut; nothing was written. `label` is the label it carries, or null. */
|
|
342
|
+
| { appended: false; label: string | null };
|
|
343
|
+
|
|
344
|
+
const CUT_RETRY_ATTEMPTS = 5;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Append a cut for `releaseDigest`. A label names one digest and a digest
|
|
348
|
+
* takes at most one label (ws-104 question 3): cutting a digest again is a
|
|
349
|
+
* no-op that returns its label, giving an unlabelled digest a label appends a
|
|
350
|
+
* cut, and cutting a digest under a label other than its own, or under a
|
|
351
|
+
* label another digest has, throws {@link ReleaseLabelError}. Does not push.
|
|
352
|
+
*/
|
|
353
|
+
export async function appendCut(
|
|
354
|
+
input: Omit<ReleaseCut, "version" | "signature"> & { signature?: string | null },
|
|
355
|
+
opts?: { cwd?: string },
|
|
356
|
+
): Promise<CutOutcome> {
|
|
357
|
+
if (input.label !== null) {
|
|
358
|
+
const why = invalidLabel(input.label);
|
|
359
|
+
if (why) throw new ReleaseLabelError("release-label-invalid", why);
|
|
360
|
+
}
|
|
361
|
+
let lastErr: unknown;
|
|
362
|
+
for (let attempt = 1; attempt <= CUT_RETRY_ATTEMPTS; attempt++) {
|
|
363
|
+
const priorSha = await readPathSha(RELEASES_DIR, CUTS_FILE, opts);
|
|
364
|
+
const existing = priorSha ? await readBlobBySha(priorSha, opts) : null;
|
|
365
|
+
const { cuts } = parseCuts(existing);
|
|
366
|
+
const labels = labelsByDigest(cuts);
|
|
367
|
+
const own = labels.get(input.releaseDigest);
|
|
368
|
+
const alreadyCut = cuts.some((c) => c.releaseDigest === input.releaseDigest);
|
|
369
|
+
if (own !== undefined) {
|
|
370
|
+
if (input.label === null || input.label === own) return { appended: false, label: own };
|
|
371
|
+
throw new ReleaseLabelError(
|
|
372
|
+
"release-already-labelled",
|
|
373
|
+
`release ${input.releaseDigest} is already labelled "${own}", and a release takes one label`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (input.label === null && alreadyCut) return { appended: false, label: null };
|
|
377
|
+
if (input.label !== null) {
|
|
378
|
+
for (const [digest, label] of labels) {
|
|
379
|
+
if (label === input.label) {
|
|
380
|
+
throw new ReleaseLabelError("release-label-taken", `the label "${input.label}" already names release ${digest}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const cut: ReleaseCut = {
|
|
385
|
+
version: 1,
|
|
386
|
+
releaseDigest: input.releaseDigest,
|
|
387
|
+
label: input.label,
|
|
388
|
+
actor: input.actor,
|
|
389
|
+
timestamp: input.timestamp,
|
|
390
|
+
runOrigin: input.runOrigin,
|
|
391
|
+
signature: input.signature ?? null,
|
|
392
|
+
};
|
|
393
|
+
const content = `${existing ? `${existing.replace(/\n$/, "")}\n` : ""}${JSON.stringify(cut, sortedJsonReplacer)}\n`;
|
|
394
|
+
try {
|
|
395
|
+
const dir = await ledgerDir(RELEASES_DIR, opts);
|
|
396
|
+
await writeLedgerFiles([{ path: `${dir}/${CUTS_FILE}`, content, expectPriorSha: priorSha }], "Release cut", opts);
|
|
397
|
+
return { appended: true, cut };
|
|
398
|
+
} catch (err) {
|
|
399
|
+
if (!(err instanceof RefCASConflictError)) throw err;
|
|
400
|
+
lastErr = err;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw lastErr;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** A template's bytes by digest, or null when none is stored. */
|
|
407
|
+
export async function readReleaseTemplate(digest: string, opts?: LedgerLocation): Promise<string | null> {
|
|
408
|
+
return readBlobFromPath(`${RELEASES_DIR}/${TEMPLATES_SUBDIR}`, templateFile(digest), opts);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Find a release by its digest or its label. Null when neither names one.
|
|
413
|
+
* A string that names a digest is read as a digest first.
|
|
414
|
+
*/
|
|
415
|
+
export async function resolveRelease(ref: string, opts?: LedgerLocation): Promise<{ release: Release; label: string | null } | null> {
|
|
416
|
+
const { cuts } = await readCuts(opts);
|
|
417
|
+
const labels = labelsByDigest(cuts);
|
|
418
|
+
let digest: string | undefined = ref.includes(":") ? ref : undefined;
|
|
419
|
+
if (!digest) for (const [d, label] of labels) if (label === ref) digest = d;
|
|
420
|
+
if (!digest) return null;
|
|
421
|
+
const release = await readRelease(digest, opts);
|
|
422
|
+
return release ? { release, label: labels.get(digest) ?? null } : null;
|
|
423
|
+
}
|
|
@@ -92,6 +92,16 @@ describe("buildRunRecord", () => {
|
|
|
92
92
|
expect(input.phases[1].steps.map((s) => s.status)).toEqual(["ok", "skipped"]);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
test("carries the commit and the CI run origin when given (#4020), and neither when not", () => {
|
|
96
|
+
const times = { started: "2026-01-01T00:00:00.000Z", ended: "2026-01-01T00:00:01.000Z", status: "ok" as const };
|
|
97
|
+
const withBoth = buildRunRecord({ name: "ship" }, [], { ...times, gitSha: "a".repeat(40), runOrigin: { forge: "github", repo: "acme/app" } });
|
|
98
|
+
expect(withBoth.gitSha).toBe("a".repeat(40));
|
|
99
|
+
expect(withBoth.runOrigin).toEqual({ forge: "github", repo: "acme/app" });
|
|
100
|
+
const without = buildRunRecord({ name: "ship" }, [], times);
|
|
101
|
+
expect(without).not.toHaveProperty("gitSha");
|
|
102
|
+
expect(without).not.toHaveProperty("runOrigin");
|
|
103
|
+
});
|
|
104
|
+
|
|
95
105
|
test("a phase with a failed step is fail; a phase whose steps all skipped is skipped", () => {
|
|
96
106
|
const input = buildRunRecord(
|
|
97
107
|
{ name: "deploy" },
|
|
@@ -89,6 +89,10 @@ export function buildRunRecord(
|
|
|
89
89
|
steward?: string;
|
|
90
90
|
/** The gate the run resumed at, for a run that resumed (#3947). */
|
|
91
91
|
resumed?: OpRunRecord["resumed"];
|
|
92
|
+
/** The commit the run built from (#4020). */
|
|
93
|
+
gitSha?: string;
|
|
94
|
+
/** The CI run the Op ran in (#4020), when it ran in CI. */
|
|
95
|
+
runOrigin?: OpRunRecord["runOrigin"];
|
|
92
96
|
/** Each phase's wall-clock time, by name, as the executor measured it. */
|
|
93
97
|
phaseDurations?: Record<string, number>;
|
|
94
98
|
},
|
|
@@ -138,6 +142,8 @@ export function buildRunRecord(
|
|
|
138
142
|
...(times.point ? { point: times.point } : {}),
|
|
139
143
|
...(times.steward ? { steward: times.steward } : {}),
|
|
140
144
|
...(times.resumed ? { resumed: times.resumed } : {}),
|
|
145
|
+
...(times.gitSha ? { gitSha: times.gitSha } : {}),
|
|
146
|
+
...(times.runOrigin ? { runOrigin: times.runOrigin } : {}),
|
|
141
147
|
};
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -55,6 +55,9 @@ export async function changeCoverage(args: ChangeCoverageArgs = {}): Promise<Cha
|
|
|
55
55
|
findings: doc.findings.map(({ code, path, message, severity, records, triage }) => ({ code, path, message, severity, records, triage })),
|
|
56
56
|
summary: doc.summary,
|
|
57
57
|
};
|
|
58
|
-
if (!doc.ok)
|
|
58
|
+
if (!doc.ok) {
|
|
59
|
+
const all = [...doc.findings.map((f) => `${f.code} ${f.path}`), ...(doc.links?.findings ?? []).map((f) => `${f.code} ${f.commit.slice(0, 8)}`)];
|
|
60
|
+
throw new Error(`changeCoverage: ${all.length} change ${all.length === 1 ? "finding" : "findings"} at severity fail: ${all.join(", ")}`);
|
|
61
|
+
}
|
|
59
62
|
return result;
|
|
60
63
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { join, resolve } from "node:path";
|
|
23
|
+
import type { ReleaseAdmission } from "../../lifecycle/release-ledger";
|
|
23
24
|
import { sortedJsonReplacer } from "../../utils";
|
|
24
25
|
import { archiveSourceTree, sha256Digest, type SourceArchive } from "../source-archive";
|
|
25
26
|
|
|
@@ -126,16 +127,19 @@ export async function releaseRecord(args: ReleaseRecordArgs): Promise<ReleaseRec
|
|
|
126
127
|
const env = plan.env as string;
|
|
127
128
|
const gitSha = plan.gitSha as string;
|
|
128
129
|
|
|
129
|
-
const { readReleaseLedger, latestPerComponent, appendReleaseRecord, resolveRunId } = await import("../../lifecycle/release-ledger");
|
|
130
|
+
const { readReleaseLedger, latestPerComponent, appendReleaseRecord, resolveRunId, admissionOf } = await import("../../lifecycle/release-ledger");
|
|
130
131
|
const { persistReleasePlan } = await import("../../lifecycle/plan-ledger");
|
|
131
132
|
const { pushLifecycle } = await import("../../lifecycle/git");
|
|
132
133
|
|
|
133
134
|
let approver: string | null = null;
|
|
135
|
+
let admission: ReleaseAdmission | undefined;
|
|
134
136
|
if (args.approval) {
|
|
135
137
|
const { readGateLedger } = await import("../../lifecycle/gate-ledger");
|
|
136
138
|
const { resolutions } = await readGateLedger(args.approval.op, opts);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
// #4020: the approver and the gate resolution that admitted the release.
|
|
140
|
+
const admitted = admissionOf(resolutions, { ...args.approval, planDigest: plan.digest });
|
|
141
|
+
approver = admitted?.approver ?? null;
|
|
142
|
+
admission = admitted?.admission;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
const latest = latestPerComponent((await readReleaseLedger(env, opts)).records).get(component);
|
|
@@ -160,6 +164,7 @@ export async function releaseRecord(args: ReleaseRecordArgs): Promise<ReleaseRec
|
|
|
160
164
|
timestamp: new Date().toISOString(),
|
|
161
165
|
actor,
|
|
162
166
|
...(approver ? { approver } : {}),
|
|
167
|
+
...(admission ? { admission } : {}),
|
|
163
168
|
},
|
|
164
169
|
opts,
|
|
165
170
|
);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { execFileSync } from "node:child_process";
|
|
25
25
|
import { realpathSync } from "node:fs";
|
|
26
26
|
import { join, resolve } from "node:path";
|
|
27
|
-
import type { ReleaseRecord, ReleaseRef } from "../../lifecycle/release-ledger";
|
|
27
|
+
import type { ReleaseAdmission, ReleaseRecord, ReleaseRef } from "../../lifecycle/release-ledger";
|
|
28
28
|
import { archiveSourceTree } from "../source-archive";
|
|
29
29
|
import { readReleasePlan, releasePlan, releasePlanDigest } from "./source-release";
|
|
30
30
|
|
|
@@ -200,15 +200,18 @@ export async function releaseRollbackRecord(args: ReleaseRollbackRecordArgs): Pr
|
|
|
200
200
|
const env = plan.env as string;
|
|
201
201
|
const gitSha = plan.gitSha as string;
|
|
202
202
|
|
|
203
|
-
const { readReleaseLedger, latestPerComponent, appendReleaseRecord, resolveRunId } = await import("../../lifecycle/release-ledger");
|
|
203
|
+
const { readReleaseLedger, latestPerComponent, appendReleaseRecord, resolveRunId, admissionOf } = await import("../../lifecycle/release-ledger");
|
|
204
204
|
const { pushLifecycle } = await import("../../lifecycle/git");
|
|
205
205
|
|
|
206
206
|
let approver: string | null = null;
|
|
207
|
+
let admission: ReleaseAdmission | undefined;
|
|
207
208
|
if (args.approval) {
|
|
208
209
|
const { readGateLedger } = await import("../../lifecycle/gate-ledger");
|
|
209
210
|
const { resolutions } = await readGateLedger(args.approval.op, opts);
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
// #4020: the approver and the gate resolution that admitted the release.
|
|
212
|
+
const admitted = admissionOf(resolutions, { ...args.approval, planDigest: plan.digest });
|
|
213
|
+
approver = admitted?.approver ?? null;
|
|
214
|
+
admission = admitted?.admission;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
const latest = latestPerComponent((await readReleaseLedger(env, opts)).records.filter((r) => r.component === component)).get(component);
|
|
@@ -230,6 +233,7 @@ export async function releaseRollbackRecord(args: ReleaseRollbackRecordArgs): Pr
|
|
|
230
233
|
timestamp: new Date().toISOString(),
|
|
231
234
|
actor,
|
|
232
235
|
...(approver ? { approver } : {}),
|
|
236
|
+
...(admission ? { admission } : {}),
|
|
233
237
|
restores: rollback.restores,
|
|
234
238
|
},
|
|
235
239
|
opts,
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The base activities are core's own (`./activities`): plain async functions
|
|
6
6
|
* taking a single args object, with no workflow engine anywhere in the call
|
|
7
|
-
* path. They
|
|
8
|
-
*
|
|
7
|
+
* path. They ship with core, so `loadActivities([])` works in a project that
|
|
8
|
+
* has installed nothing but chant. They are imported when activities are
|
|
9
|
+
* loaded, not when this module is: their implementations reach lint, codegen
|
|
10
|
+
* and `typescript`, and every `*.op.ts` imports `@intentius/chant/op`, which
|
|
11
|
+
* re-exports this module (#3982). Product-specific appliers stay
|
|
9
12
|
* in their owning lexicon (#809) and are pulled in dynamically per the
|
|
10
13
|
* project's configured lexicon list.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import * as baseActivities from "./activities";
|
|
14
16
|
import { ACTIVITY_PROFILES, type ActivityProfile } from "./activity-profiles";
|
|
15
17
|
import { importLexiconModule, importLexiconPackage } from "../lexicon-module";
|
|
16
18
|
|
|
@@ -63,13 +65,13 @@ function collectActivities(mod: Record<string, unknown>, into: Map<string, Activ
|
|
|
63
65
|
* lexicon's activity of the same name; otherwise a later lexicon's export
|
|
64
66
|
* replaces an earlier one's.
|
|
65
67
|
*
|
|
66
|
-
* Never throws: the base library
|
|
68
|
+
* Never throws: the base library ships with core, so there is no "no
|
|
67
69
|
* activities registered" state to report.
|
|
68
70
|
*/
|
|
69
71
|
export async function loadActivities(lexicons: string[] = []): Promise<Map<string, ActivityFn>> {
|
|
70
72
|
const activities = new Map<string, ActivityFn>();
|
|
71
73
|
|
|
72
|
-
collectActivities(
|
|
74
|
+
collectActivities((await import("./activities")) as unknown as Record<string, unknown>, activities);
|
|
73
75
|
|
|
74
76
|
for (const name of lexicons) {
|
|
75
77
|
try {
|