@intentius/chant 0.124.27 → 0.124.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/dist/__golden__/wave-loop/op-held-on.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/dist/__golden__/wave-loop/op-per-run.json +7 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
- package/dist/__golden__/wave-loop/op-sealed.json +3 -0
- package/dist/__golden__/wave-loop/op-spend.json +3 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/dist/change-set-graph.d.ts +56 -0
- package/dist/change-set-graph.d.ts.map +1 -0
- package/dist/change-set-graph.js +128 -0
- package/dist/change-set.d.ts +52 -3
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +35 -4
- package/dist/chant-version.d.ts +9 -0
- package/dist/chant-version.d.ts.map +1 -0
- package/dist/chant-version.js +16 -0
- package/dist/cli/commands/build.d.ts +8 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -8
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.js +30 -2
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +13 -2
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.js +17 -8
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +19 -4
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +28 -0
- package/dist/cli/handlers/change-set.d.ts +10 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.js +51 -1
- package/dist/cli/handlers/fan-out.d.ts +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +14 -3
- package/dist/cli/handlers/pr.d.ts +8 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +148 -24
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +256 -21
- package/dist/cli/handlers/release.d.ts +34 -0
- package/dist/cli/handlers/release.d.ts.map +1 -0
- package/dist/cli/handlers/release.js +203 -0
- package/dist/cli/handlers/run.d.ts +28 -2
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +170 -13
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +88 -13
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +49 -3
- package/dist/cli/registry.d.ts +29 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +29 -0
- package/dist/cli/run-events.d.ts.map +1 -0
- package/dist/cli/run-events.js +47 -0
- package/dist/cli/version.d.ts +3 -5
- package/dist/cli/version.d.ts.map +1 -1
- package/dist/cli/version.js +3 -13
- package/dist/codegen/docs-in-site.d.ts +71 -0
- package/dist/codegen/docs-in-site.d.ts.map +1 -0
- package/dist/codegen/docs-in-site.js +177 -0
- package/dist/codegen/docs-types.d.ts +14 -1
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +18 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/docs.js +40 -7
- package/dist/codegen/rehype-base-url.mjs +15 -9
- package/dist/components/auto-release.d.ts +4 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +2 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +10 -9
- package/dist/components/driver.d.ts +42 -9
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +81 -54
- package/dist/components/fan-out-run.d.ts +7 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +35 -26
- package/dist/components/pr-run.d.ts +11 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +20 -1
- package/dist/components/promote-order.d.ts +38 -0
- package/dist/components/promote-order.d.ts.map +1 -0
- package/dist/components/promote-order.js +62 -0
- package/dist/components/promote.d.ts +15 -2
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +17 -0
- package/dist/components/release-compile.d.ts +133 -0
- package/dist/components/release-compile.d.ts.map +1 -0
- package/dist/components/release-compile.js +376 -0
- package/dist/components/release-gate.d.ts +125 -0
- package/dist/components/release-gate.d.ts.map +1 -0
- package/dist/components/release-gate.js +282 -0
- package/dist/components/release-promote.d.ts +96 -0
- package/dist/components/release-promote.d.ts.map +1 -0
- package/dist/components/release-promote.js +204 -0
- package/dist/components/run-progress.d.ts +7 -0
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +3 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lifecycle/change-set-store.d.ts +67 -0
- package/dist/lifecycle/change-set-store.d.ts.map +1 -0
- package/dist/lifecycle/change-set-store.js +96 -0
- package/dist/lifecycle/commit-sha.d.ts +13 -0
- package/dist/lifecycle/commit-sha.d.ts.map +1 -0
- package/dist/lifecycle/commit-sha.js +21 -0
- package/dist/lifecycle/gate-change-set.d.ts +69 -0
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
- package/dist/lifecycle/gate-change-set.js +106 -0
- package/dist/lifecycle/gate-ledger.d.ts +49 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +18 -0
- package/dist/lifecycle/release-cut.d.ts +107 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -0
- package/dist/lifecycle/release-cut.js +273 -0
- package/dist/lifecycle/release-ledger.d.ts +54 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +53 -20
- package/dist/lifecycle/release-store.d.ts +190 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -0
- package/dist/lifecycle/release-store.js +318 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +2 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.js +4 -2
- package/dist/op/activities/source-release.d.ts.map +1 -1
- package/dist/op/activities/source-release.js +7 -3
- package/dist/op/activities/source-rollback.d.ts.map +1 -1
- package/dist/op/activities/source-rollback.js +7 -3
- package/dist/op/gate-name.d.ts +11 -2
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-name.js +12 -3
- package/dist/op/gate-run-locator.d.ts +22 -0
- package/dist/op/gate-run-locator.d.ts.map +1 -1
- package/dist/op/gate-run-locator.js +31 -0
- package/dist/op/gate.d.ts +27 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +57 -3
- package/dist/op/local-executor.d.ts +36 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +158 -52
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -0
- package/dist/op/run-live.d.ts +2 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/run-live.js +1 -1
- package/dist/op/runtime.d.ts +46 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +8 -0
- package/dist/point-decision.d.ts +2 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +9 -2
- package/dist/run-event-store.d.ts +77 -0
- package/dist/run-event-store.d.ts.map +1 -0
- package/dist/run-event-store.js +204 -0
- package/dist/run-event-stream.d.ts +132 -0
- package/dist/run-event-stream.d.ts.map +1 -0
- package/dist/run-event-stream.js +238 -0
- package/dist/run-event.d.ts +267 -0
- package/dist/run-event.d.ts.map +1 -0
- package/dist/run-event.js +207 -0
- package/dist/wave-runtime.mjs +204 -199
- package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
- package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/run-event-check.js +59 -0
- package/dist/workspace/change-set-show.d.ts +56 -0
- package/dist/workspace/change-set-show.d.ts.map +1 -0
- package/dist/workspace/change-set-show.js +54 -0
- package/dist/workspace/change-set-show.schema.json +102 -0
- package/dist/workspace/change-set.schema.json +28 -1
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes-cli.js +10 -1
- package/dist/workspace/changes.d.ts +37 -0
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/changes.js +49 -2
- package/dist/workspace/changes.schema.json +135 -0
- package/dist/workspace/ci-green-cli.d.ts +19 -4
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +75 -12
- package/dist/workspace/ci-green-forge.d.ts +20 -3
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +6 -4
- package/dist/workspace/ci-green-workflow.d.ts +11 -0
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +6 -0
- package/dist/workspace/ci-green.d.ts +273 -13
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +449 -38
- package/dist/workspace/ci-last-green.schema.json +32 -2
- package/dist/workspace/ci-list.schema.json +525 -0
- package/dist/workspace/commit-links.d.ts +85 -0
- package/dist/workspace/commit-links.d.ts.map +1 -0
- package/dist/workspace/commit-links.js +163 -0
- package/dist/workspace/declaration.d.ts +19 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +1 -1
- package/dist/workspace/declaration.schema.json +12 -0
- package/dist/workspace/identity-gate-rule.d.ts +37 -0
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +72 -7
- package/dist/workspace/identity.d.ts +12 -0
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/identity.js +13 -1
- package/dist/workspace/intent.d.ts +6 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +51 -1
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +2 -0
- package/dist/workspace/ls.schema.json +3 -1
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +6 -0
- package/dist/workspace/promotion-order.d.ts +41 -0
- package/dist/workspace/promotion-order.d.ts.map +1 -0
- package/dist/workspace/promotion-order.js +95 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +4 -1
- package/dist/workspace/releases.d.ts +72 -0
- package/dist/workspace/releases.d.ts.map +1 -0
- package/dist/workspace/releases.js +94 -0
- package/dist/workspace/releases.schema.json +391 -0
- package/dist/workspace/run-event.schema.json +288 -0
- package/dist/workspace/status-gates.d.ts +26 -1
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +31 -2
- package/dist/workspace/status-stewards.d.ts +4 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +2 -0
- package/dist/workspace/status.d.ts +39 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +48 -1
- package/dist/workspace/status.schema.json +70 -1
- package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
- package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
- package/dist/workspace/trust/ci-tag-statement.js +123 -0
- package/dist/workspace/trust/op-run-statement.d.ts +91 -0
- package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/op-run-statement.js +143 -0
- package/dist/workspace/trust/rotation.d.ts +18 -2
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.js +65 -16
- package/dist/workspace/trust/seal.d.ts +11 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +9 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/ssh-commit.js +10 -10
- package/package.json +1 -1
- package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
- package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
- package/src/__golden__/wave-loop/fan-out-only.json +6 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
- package/src/__golden__/wave-loop/op-cost-member.json +3 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
- package/src/__golden__/wave-loop/op-held-on.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
- package/src/__golden__/wave-loop/op-per-run.json +7 -0
- package/src/__golden__/wave-loop/op-pr-review.json +7 -0
- package/src/__golden__/wave-loop/op-sealed.json +3 -0
- package/src/__golden__/wave-loop/op-spend.json +3 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
- package/src/change-set-graph.test.ts +72 -0
- package/src/change-set-graph.ts +133 -0
- package/src/change-set.ts +76 -6
- package/src/chant-version.ts +16 -0
- package/src/cli/commands/build.ts +19 -8
- package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
- package/src/cli/commands/check-lexicon-docs.ts +39 -2
- package/src/cli/commands/check-lexicon.ts +14 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
- package/src/cli/commands/init-lexicon.test.ts +25 -0
- package/src/cli/commands/init-lexicon.ts +18 -8
- package/src/cli/handlers/approve-release.test.ts +80 -0
- package/src/cli/handlers/approve.ts +18 -2
- package/src/cli/handlers/build.test.ts +52 -0
- package/src/cli/handlers/build.ts +29 -0
- package/src/cli/handlers/change-set.ts +49 -1
- package/src/cli/handlers/fan-out.test.ts +48 -0
- package/src/cli/handlers/fan-out.ts +15 -3
- package/src/cli/handlers/operator.test.ts +37 -0
- package/src/cli/handlers/pr.test.ts +194 -0
- package/src/cli/handlers/pr.ts +168 -34
- package/src/cli/handlers/promote.test.ts +389 -5
- package/src/cli/handlers/promote.ts +279 -20
- package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
- package/src/cli/handlers/release.e2e.test.ts +85 -0
- package/src/cli/handlers/release.ts +206 -0
- package/src/cli/handlers/run-auto-release.test.ts +71 -0
- package/src/cli/handlers/run-events-cmd.test.ts +92 -0
- package/src/cli/handlers/run.test.ts +30 -0
- package/src/cli/handlers/run.ts +176 -15
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +88 -13
- package/src/cli/mcp/never-over-mcp.test.ts +2 -0
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +37 -3
- package/src/cli/registry.ts +29 -3
- package/src/cli/run-events.ts +54 -0
- package/src/cli/version.ts +3 -13
- package/src/codegen/docs-in-site.test.ts +166 -0
- package/src/codegen/docs-in-site.ts +182 -0
- package/src/codegen/docs-site/AgentPrompt.astro +26 -1
- package/src/codegen/docs-types.ts +14 -1
- package/src/codegen/docs.ts +60 -7
- package/src/codegen/rehype-base-url.d.mts +3 -1
- package/src/codegen/rehype-base-url.mjs +15 -9
- package/src/codegen/rehype-base-url.test.ts +34 -0
- package/src/components/auto-release.test.ts +21 -0
- package/src/components/auto-release.ts +6 -1
- package/src/components/cli-support.test.ts +82 -0
- package/src/components/cli-support.ts +16 -9
- package/src/components/driver.ts +129 -56
- package/src/components/fan-out-run.ts +56 -35
- package/src/components/pr-run.ts +30 -1
- package/src/components/promote-order.test.ts +90 -0
- package/src/components/promote-order.ts +75 -0
- package/src/components/promote.test.ts +10 -1
- package/src/components/promote.ts +26 -1
- package/src/components/release-compile.test.ts +147 -0
- package/src/components/release-compile.ts +445 -0
- package/src/components/release-gate.test.ts +215 -0
- package/src/components/release-gate.ts +341 -0
- package/src/components/release-promote.test.ts +159 -0
- package/src/components/release-promote.ts +232 -0
- package/src/components/run-progress.ts +7 -0
- package/src/components/wave-plan.ts +1 -1
- package/src/config.ts +7 -1
- package/src/index.ts +2 -0
- package/src/lifecycle/change-set-store.test.ts +217 -0
- package/src/lifecycle/change-set-store.ts +112 -0
- package/src/lifecycle/commit-sha.ts +23 -0
- package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
- package/src/lifecycle/gate-change-set.ts +133 -0
- package/src/lifecycle/gate-ledger.test.ts +33 -0
- package/src/lifecycle/gate-ledger.ts +66 -1
- package/src/lifecycle/release-cut-signed.test.ts +180 -0
- package/src/lifecycle/release-cut.test.ts +411 -0
- package/src/lifecycle/release-cut.ts +367 -0
- package/src/lifecycle/release-ledger.test.ts +14 -0
- package/src/lifecycle/release-ledger.ts +85 -17
- package/src/lifecycle/release-store.ts +423 -0
- package/src/lifecycle/run-ledger.test.ts +10 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/op/activities/change-coverage.ts +4 -1
- package/src/op/activities/source-release.ts +8 -3
- package/src/op/activities/source-rollback.ts +8 -4
- package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
- package/src/op/gate-name.ts +13 -3
- package/src/op/gate-release.test.ts +126 -0
- package/src/op/gate-run-locator.test.ts +110 -0
- package/src/op/gate-run-locator.ts +35 -0
- package/src/op/gate.ts +88 -6
- package/src/op/local-executor.ts +207 -52
- package/src/op/op-waves-run.ts +4 -0
- package/src/op/run-events.test.ts +166 -0
- package/src/op/run-live.ts +1 -1
- package/src/op/runtime.ts +39 -0
- package/src/op/runtimes/local.test.ts +16 -0
- package/src/op/runtimes/local.ts +9 -0
- package/src/point-decision.ts +12 -2
- package/src/run-event-store.test.ts +144 -0
- package/src/run-event-store.ts +213 -0
- package/src/run-event-stream.test.ts +175 -0
- package/src/run-event-stream.ts +306 -0
- package/src/run-event.test.ts +277 -0
- package/src/run-event.ts +451 -0
- package/src/wave-loop-golden.test.ts +9 -0
- package/src/workspace/__fixtures__/run-event-check.ts +58 -0
- package/src/workspace/ancestry-index.test.ts +93 -0
- package/src/workspace/change-set-show.schema.json +102 -0
- package/src/workspace/change-set-show.ts +72 -0
- package/src/workspace/change-set.schema.json +28 -1
- package/src/workspace/changes-cli.ts +10 -1
- package/src/workspace/changes.schema.json +135 -0
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/changes.ts +84 -2
- package/src/workspace/ci-green-cli.ts +75 -13
- package/src/workspace/ci-green-forge.ts +24 -4
- package/src/workspace/ci-green-runs.test.ts +242 -0
- package/src/workspace/ci-green-signed.test.ts +302 -0
- package/src/workspace/ci-green-workflow.test.ts +16 -0
- package/src/workspace/ci-green-workflow.ts +12 -0
- package/src/workspace/ci-green.test.ts +131 -2
- package/src/workspace/ci-green.ts +624 -42
- package/src/workspace/ci-last-green.schema.json +32 -2
- package/src/workspace/ci-list.schema.json +525 -0
- package/src/workspace/commit-links.test.ts +279 -0
- package/src/workspace/commit-links.ts +183 -0
- package/src/workspace/declaration.schema.json +12 -0
- package/src/workspace/declaration.ts +22 -2
- package/src/workspace/identity-gate-rule.ts +91 -7
- package/src/workspace/identity.ts +14 -1
- package/src/workspace/intent.ts +48 -1
- package/src/workspace/ls.schema.json +3 -1
- package/src/workspace/ls.ts +6 -0
- package/src/workspace/member-pipeline.test.ts +17 -0
- package/src/workspace/member-pipeline.ts +9 -0
- package/src/workspace/promotion-order.ts +97 -0
- package/src/workspace/read-contract.e2e.test.ts +4 -1
- package/src/workspace/reason-codes.test.ts +2 -1
- package/src/workspace/reason-codes.ts +4 -1
- package/src/workspace/releases.schema.json +391 -0
- package/src/workspace/releases.test.ts +108 -0
- package/src/workspace/releases.ts +122 -0
- package/src/workspace/run-event.schema.json +288 -0
- package/src/workspace/status-contract.test.ts +52 -1
- package/src/workspace/status-gates.ts +56 -1
- package/src/workspace/status-stewards.ts +6 -0
- package/src/workspace/status.schema.json +70 -1
- package/src/workspace/status.ts +80 -1
- package/src/workspace/trust/ci-tag-statement.ts +151 -0
- package/src/workspace/trust/op-run-statement.ts +148 -0
- package/src/workspace/trust/rotation.ts +61 -14
- package/src/workspace/trust/seal.ts +10 -0
- package/src/workspace/trust/ssh-commit.ts +10 -9
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
|
@@ -27,14 +27,46 @@
|
|
|
27
27
|
*
|
|
28
28
|
* The forge is asked only for check runs (`./ci-green-forge.ts`). The tags
|
|
29
29
|
* are fetched from and pushed to the remote with git.
|
|
30
|
+
*
|
|
31
|
+
* Signed tags (#4031): when `ci.green.signedSince` names a commit, a tag on
|
|
32
|
+
* any commit that is not a strict first-parent ancestor of it counts only
|
|
33
|
+
* when its annotation carries a statement signed by a runner key (`./trust/ci-tag-statement.ts`). Every
|
|
34
|
+
* reader here, and the release cut, ignores a tag without a valid one and
|
|
35
|
+
* reports it. A tick given a runner key signs the tags it makes. Without
|
|
36
|
+
* `signedSince` every tag counts, as before.
|
|
37
|
+
*
|
|
38
|
+
* Chant runs as the source (#4044): with `ci.green.source: "runs"` (or
|
|
39
|
+
* `--source runs`) the tick reads chant's own Op runs from the run ledger on
|
|
40
|
+
* `chant/lifecycle` in place of the forge's check runs, so a project whose
|
|
41
|
+
* pipelines run as chant Ops, such as one on GitLab or Forgejo, gets green
|
|
42
|
+
* and revoked tags with no forge check runs. A phase's patterns then match
|
|
43
|
+
* Op names, a run counts for the commit its record names as `gitSha`, and
|
|
44
|
+
* the latest run of each Op decides, by its end time. A run counts only when
|
|
45
|
+
* its record carries a statement signed by a runner key whose principal
|
|
46
|
+
* holds the `runner` role at base (`./trust/op-run-statement.ts`); the tick
|
|
47
|
+
* ignores and reports any other, so it neither greens nor revokes a commit.
|
|
48
|
+
* A tick from runs signs every tag it makes, and each run in the tag's
|
|
49
|
+
* record names the Op run's id and carries its statement.
|
|
30
50
|
*/
|
|
31
51
|
|
|
52
|
+
import { judgeCommits, mergeCommits, readWorkIndex, type CommitLink } from "./commit-links";
|
|
32
53
|
import { execFileSync } from "node:child_process";
|
|
33
|
-
import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type CiGreen, type CiPhase } from "./declaration";
|
|
34
|
-
import type { CheckRun, CiForge } from "./ci-green-forge";
|
|
54
|
+
import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type CiGreen, type CiPhase, type CiSource } from "./declaration";
|
|
55
|
+
import type { CheckRun, CiForge, OpRunRef } from "./ci-green-forge";
|
|
56
|
+
import type { OpRunRecord } from "../op/runtime";
|
|
57
|
+
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
58
|
+
import { fetchLifecycleStatus, listFilesInDir, listLedgerTrees } from "../lifecycle/git";
|
|
59
|
+
import { signOpRunRecord, verifyOpRunStatement } from "./trust/op-run-statement";
|
|
35
60
|
import type { ReasonCode } from "./reason-codes";
|
|
36
61
|
import { gitTop } from "./tree";
|
|
37
62
|
import { locateWorkspace } from "./which-chant";
|
|
63
|
+
import { gitRevisionSource } from "./record-source";
|
|
64
|
+
import { emptyPolicy, readTrustPolicy } from "./trust/policy";
|
|
65
|
+
import { loadRunnerKey } from "./trust/dsse";
|
|
66
|
+
import { signCiTagRecord, verifyCiTagStatement, CI_TAG_SIGNER_ROLE, type CiTagFailure, type CiTagRecord, type CiTagSigner } from "./trust/ci-tag-statement";
|
|
67
|
+
|
|
68
|
+
/** The environment variable a tick (#4031) and `chant run` (#4044) read a runner key from, as PEM, when `--key` is not given. */
|
|
69
|
+
export const RUNNER_KEY_ENV = "CHANT_RUNNER_KEY";
|
|
38
70
|
|
|
39
71
|
export const CI_GREEN_TAG_PREFIX = "ci/green/";
|
|
40
72
|
export const CI_REVOKED_TAG_PREFIX = "ci/revoked/";
|
|
@@ -83,6 +115,8 @@ export interface RunVerdict {
|
|
|
83
115
|
conclusion: string | null;
|
|
84
116
|
url: string | null;
|
|
85
117
|
verdict: Verdict;
|
|
118
|
+
/** The Op run this is, when the source is chant runs (#4044). */
|
|
119
|
+
opRun?: OpRunRef;
|
|
86
120
|
}
|
|
87
121
|
|
|
88
122
|
export interface PhaseVerdict {
|
|
@@ -110,7 +144,7 @@ export function evaluatePhase(phase: CiPhase, latest: readonly CheckRun[]): Phas
|
|
|
110
144
|
if (matched.length === 0) unmatched.push(pattern);
|
|
111
145
|
for (const r of matched) {
|
|
112
146
|
if (runs.some((x) => x.name === r.name)) continue;
|
|
113
|
-
runs.push({ name: r.name, id: r.id, status: r.status, conclusion: r.conclusion, url: r.url, verdict: runVerdict(phase, r) });
|
|
147
|
+
runs.push({ name: r.name, id: r.id, status: r.status, conclusion: r.conclusion, url: r.url, verdict: runVerdict(phase, r), ...(r.opRun ? { opRun: r.opRun } : {}) });
|
|
114
148
|
}
|
|
115
149
|
}
|
|
116
150
|
const verdict: Verdict = runs.some((r) => r.verdict === "fail")
|
|
@@ -152,6 +186,24 @@ export interface TickCommit {
|
|
|
152
186
|
verdict?: CommitVerdict;
|
|
153
187
|
/** The tag the tick made, if any. */
|
|
154
188
|
made: { tag: string; message: string } | null;
|
|
189
|
+
/** Whether the commit names its work item (#4029), or undefined when the workspace declares no work kind. */
|
|
190
|
+
work?: CommitLink;
|
|
191
|
+
/** The commit's ci tags that were ignored, each without a valid statement (#4031). Absent when none were. */
|
|
192
|
+
ignored?: IgnoredCiTag[];
|
|
193
|
+
/** The tag the tick would have made, when an ignored tag already holds its name (#4031). Nothing is made until a person deletes that tag. */
|
|
194
|
+
blocked?: string;
|
|
195
|
+
/** The Op runs on this commit that were ignored, each without a valid statement (#4044). Absent when none were. */
|
|
196
|
+
ignoredRuns?: IgnoredOpRun[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** An Op run on a commit that does not count (#4044), and why. */
|
|
200
|
+
export interface IgnoredOpRun {
|
|
201
|
+
run: string;
|
|
202
|
+
op: string;
|
|
203
|
+
env: string;
|
|
204
|
+
/** `unsigned`, `invalid`, `untrusted` or `mismatch`, as for a ci tag. */
|
|
205
|
+
status: CiTagFailure;
|
|
206
|
+
reason: string;
|
|
155
207
|
}
|
|
156
208
|
|
|
157
209
|
export interface TickPlanInput {
|
|
@@ -159,8 +211,12 @@ export interface TickPlanInput {
|
|
|
159
211
|
commits: readonly { sha: string; committed: string }[];
|
|
160
212
|
greenTags: ReadonlySet<string>;
|
|
161
213
|
revokedTags: ReadonlySet<string>;
|
|
214
|
+
/** Tags that exist but were ignored (#4031), by commit. The tick does not make a tag whose name one of them holds. */
|
|
215
|
+
ignored?: ReadonlyMap<string, readonly IgnoredCiTag[]>;
|
|
162
216
|
checkRuns(sha: string): Promise<CheckRun[]>;
|
|
163
217
|
now: Date;
|
|
218
|
+
/** Signs each tag's record (#4031). Without it the tags carry no statement. */
|
|
219
|
+
signer?: CiTagSigner;
|
|
164
220
|
}
|
|
165
221
|
|
|
166
222
|
/** A time as an annotation and a tick report write it: UTC, to the second. */
|
|
@@ -173,25 +229,41 @@ export function windowMs(window: string): number {
|
|
|
173
229
|
return Number(m[1]) * { m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as "m" | "h" | "d"];
|
|
174
230
|
}
|
|
175
231
|
|
|
176
|
-
|
|
232
|
+
/**
|
|
233
|
+
* A run as a tag's record holds it. An Op run (#4044) has no forge id; it
|
|
234
|
+
* names the Op run's id, env and signer, and carries the run's statement.
|
|
235
|
+
*/
|
|
236
|
+
const runRecord = (r: RunVerdict) =>
|
|
237
|
+
r.opRun
|
|
238
|
+
? { name: r.name, id: null, conclusion: r.conclusion, url: r.url, opRun: { id: r.opRun.id, env: r.opRun.env, signer: r.opRun.signer, statement: r.opRun.statement } }
|
|
239
|
+
: { name: r.name, id: r.id, conclusion: r.conclusion, url: r.url };
|
|
240
|
+
|
|
241
|
+
/** `source: "runs"` on a record made from chant runs (#4044); nothing for check runs, so their records read as before. */
|
|
242
|
+
const sourceField = (green: CiGreen) => (green.source === "runs" ? { source: "runs" as const } : {});
|
|
243
|
+
|
|
244
|
+
/** The record as JSON, with the statement that signs it when a signer is given (#4031). */
|
|
245
|
+
function recordJson(record: CiTagRecord, signer: CiTagSigner | undefined): string {
|
|
246
|
+
return JSON.stringify(signer ? { ...record, statement: signCiTagRecord(record, signer) } : record, null, 2);
|
|
247
|
+
}
|
|
177
248
|
|
|
178
|
-
/** The annotation of a green tag: a subject line, then the record as JSON. */
|
|
179
|
-
export function greenAnnotation(green: CiGreen, sha: string, verdict: CommitVerdict, at: Date): string {
|
|
180
|
-
const record = {
|
|
249
|
+
/** The annotation of a green tag: a subject line, then the record as JSON, signed when `signer` is given (#4031). */
|
|
250
|
+
export function greenAnnotation(green: CiGreen, sha: string, verdict: CommitVerdict, at: Date, signer?: CiTagSigner): string {
|
|
251
|
+
const record: CiTagRecord = {
|
|
181
252
|
schema: 1,
|
|
182
253
|
tag: "green",
|
|
183
254
|
commit: sha,
|
|
184
255
|
branch: green.branch,
|
|
185
256
|
at: iso(at),
|
|
257
|
+
...sourceField(green),
|
|
186
258
|
phases: Object.fromEntries(verdict.phases.map((p) => [p.phase, p.runs.map(runRecord)])),
|
|
187
259
|
};
|
|
188
|
-
return `ci: every required phase passed (${green.require.join(", ")})\n\n${
|
|
260
|
+
return `ci: every required phase passed (${green.require.join(", ")})\n\n${recordJson(record, signer)}\n`;
|
|
189
261
|
}
|
|
190
262
|
|
|
191
|
-
/** The annotation of a revoked tag: the phase and the run that failed, and the time. */
|
|
192
|
-
export function revokedAnnotation(green: CiGreen, sha: string, failure: NonNullable<CommitVerdict["failure"]>, at: Date): string {
|
|
193
|
-
const record = { schema: 1, tag: "revoked", commit: sha, branch: green.branch, at: iso(at), phase: failure.phase, run: runRecord(failure.run) };
|
|
194
|
-
return `ci: phase ${failure.phase} failed in ${failure.run.name} (${failure.run.conclusion ?? failure.run.status})\n\n${
|
|
263
|
+
/** The annotation of a revoked tag: the phase and the run that failed, and the time, signed when `signer` is given (#4031). */
|
|
264
|
+
export function revokedAnnotation(green: CiGreen, sha: string, failure: NonNullable<CommitVerdict["failure"]>, at: Date, signer?: CiTagSigner): string {
|
|
265
|
+
const record: CiTagRecord = { schema: 1, tag: "revoked", commit: sha, branch: green.branch, at: iso(at), ...sourceField(green), phase: failure.phase, run: runRecord(failure.run) };
|
|
266
|
+
return `ci: phase ${failure.phase} failed in ${failure.run.name} (${failure.run.conclusion ?? failure.run.status})\n\n${recordJson(record, signer)}\n`;
|
|
195
267
|
}
|
|
196
268
|
|
|
197
269
|
/**
|
|
@@ -205,17 +277,21 @@ export async function planTick(input: TickPlanInput): Promise<TickCommit[]> {
|
|
|
205
277
|
for (const c of input.commits) {
|
|
206
278
|
const before: TagState = input.revokedTags.has(c.sha) ? "revoked" : input.greenTags.has(c.sha) ? "green" : "none";
|
|
207
279
|
if (before === "revoked") {
|
|
208
|
-
|
|
280
|
+
const ignored = input.ignored?.get(c.sha);
|
|
281
|
+
out.push({ ...c, before, made: null, ...(ignored && ignored.length > 0 ? { ignored: [...ignored] } : {}) });
|
|
209
282
|
continue;
|
|
210
283
|
}
|
|
211
284
|
const verdict = evaluateCommit(input.green, await input.checkRuns(c.sha));
|
|
212
285
|
let made: TickCommit["made"] = null;
|
|
213
286
|
if (before === "none" && verdict.verdict === "pass") {
|
|
214
|
-
made = { tag: `${CI_GREEN_TAG_PREFIX}${c.sha}`, message: greenAnnotation(input.green, c.sha, verdict, input.now) };
|
|
287
|
+
made = { tag: `${CI_GREEN_TAG_PREFIX}${c.sha}`, message: greenAnnotation(input.green, c.sha, verdict, input.now, input.signer) };
|
|
215
288
|
} else if (before === "green" && verdict.verdict === "fail") {
|
|
216
|
-
made = { tag: `${CI_REVOKED_TAG_PREFIX}${c.sha}`, message: revokedAnnotation(input.green, c.sha, verdict.failure!, input.now) };
|
|
289
|
+
made = { tag: `${CI_REVOKED_TAG_PREFIX}${c.sha}`, message: revokedAnnotation(input.green, c.sha, verdict.failure!, input.now, input.signer) };
|
|
217
290
|
}
|
|
218
|
-
|
|
291
|
+
const ignored = input.ignored?.get(c.sha);
|
|
292
|
+
// A tag is never moved or deleted by chant, so an ignored tag holding the name stops the tick making it.
|
|
293
|
+
const blocked = made !== null && ignored?.some((t) => t.tag === made!.tag) ? made.tag : undefined;
|
|
294
|
+
out.push({ ...c, before, verdict, made: blocked ? null : made, ...(ignored && ignored.length > 0 ? { ignored: [...ignored] } : {}), ...(blocked ? { blocked } : {}) });
|
|
219
295
|
}
|
|
220
296
|
return out;
|
|
221
297
|
}
|
|
@@ -238,17 +314,130 @@ function tryGit(top: string, args: string[]): string | null {
|
|
|
238
314
|
}
|
|
239
315
|
}
|
|
240
316
|
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
317
|
+
/** A ci tag as git holds it, before it is judged. */
|
|
318
|
+
interface CiTagRef {
|
|
319
|
+
kind: "green" | "revoked";
|
|
320
|
+
/** The commit the tag's name gives. */
|
|
321
|
+
sha: string;
|
|
322
|
+
tag: string;
|
|
323
|
+
/** The tagger's date, or null for a lightweight tag. */
|
|
324
|
+
tagged: string | null;
|
|
325
|
+
/** The annotation's JSON record, when the tag is annotated and it parses. */
|
|
326
|
+
record: Record<string, unknown> | null;
|
|
327
|
+
/** The commit the tag points at. */
|
|
328
|
+
target: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Every ci tag in the repository, read from local refs. */
|
|
332
|
+
function readCiTagRefs(top: string): CiTagRef[] {
|
|
333
|
+
const out: CiTagRef[] = [];
|
|
334
|
+
const format = "%(refname:lstrip=2)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(taggerdate:iso-strict)%00%(contents)%01";
|
|
335
|
+
const raw = git(top, ["for-each-ref", `--format=${format}`, `refs/tags/${CI_GREEN_TAG_PREFIX}`, `refs/tags/${CI_REVOKED_TAG_PREFIX}`]);
|
|
336
|
+
for (const entry of raw.split("\u0001")) {
|
|
337
|
+
const [tag, type, object, peeled, date, contents] = entry.replace(/^\n/, "").split("\u0000");
|
|
338
|
+
if (!tag) continue;
|
|
339
|
+
const annotated = type === "tag";
|
|
340
|
+
let record: CiTagRef["record"] = null;
|
|
341
|
+
// A lightweight tag has no tagger date, and its "contents" are the commit's message.
|
|
342
|
+
if (annotated && date && contents && contents.includes("{")) {
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(contents.slice(contents.indexOf("{")));
|
|
345
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) record = parsed as Record<string, unknown>;
|
|
346
|
+
} catch {
|
|
347
|
+
record = null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const green = tag.startsWith(CI_GREEN_TAG_PREFIX);
|
|
351
|
+
out.push({
|
|
352
|
+
kind: green ? "green" : "revoked",
|
|
353
|
+
sha: tag.slice((green ? CI_GREEN_TAG_PREFIX : CI_REVOKED_TAG_PREFIX).length),
|
|
354
|
+
tag,
|
|
355
|
+
tagged: date || null,
|
|
356
|
+
record,
|
|
357
|
+
target: (annotated ? peeled : object) ?? "",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** A ci tag that exists and does not count (#4031), and why. */
|
|
364
|
+
export interface IgnoredCiTag {
|
|
365
|
+
tag: string;
|
|
366
|
+
/** `unsigned`, `invalid`, `untrusted` or `mismatch`, as a run statement's attestation says it. */
|
|
367
|
+
status: CiTagFailure;
|
|
368
|
+
reason: string;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** A tag that counts, with what its annotation recorded. */
|
|
372
|
+
export interface CountedCiTag {
|
|
373
|
+
tagged: string | null;
|
|
374
|
+
/** The annotation's JSON record, when the tag is annotated and it parses. */
|
|
375
|
+
record: Record<string, unknown> | null;
|
|
376
|
+
/** The runner whose statement it carries, or null when it counts unsigned (#4031). */
|
|
377
|
+
signer: string | null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The ci tags as readers count them (#4031). */
|
|
381
|
+
export interface CiTags {
|
|
382
|
+
green: Map<string, CountedCiTag>;
|
|
383
|
+
revoked: Map<string, CountedCiTag>;
|
|
384
|
+
/** Tags that exist and do not count, by the commit their name gives. */
|
|
385
|
+
ignored: Map<string, IgnoredCiTag[]>;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The commits whose tags count unsigned under `signedSince` (#4031): its
|
|
390
|
+
* strict first-parent ancestors, the history the green branch had before the
|
|
391
|
+
* first commit whose tags must be signed. Position in history is something a
|
|
392
|
+
* committer can't set, unlike a commit's or a tag's date (ws-069 d). Throws
|
|
393
|
+
* `declaration-invalid` when `signedSince` names no commit here.
|
|
394
|
+
*/
|
|
395
|
+
export function unsignedHistory(top: string, signedSince: string): Set<string> {
|
|
396
|
+
if (tryGit(top, ["rev-parse", "--verify", "--quiet", `${signedSince}^{commit}`]) === null) {
|
|
397
|
+
throw new WorkspaceReadError(
|
|
398
|
+
"declaration-invalid",
|
|
399
|
+
`ci.green.signedSince names ${signedSince}, which is not a commit in this repository; it must be the full sha of the first commit on the green branch whose tags must be signed (fetch the branch if it is newer than this clone)`,
|
|
400
|
+
);
|
|
250
401
|
}
|
|
251
|
-
return
|
|
402
|
+
return new Set(git(top, ["rev-list", "--first-parent", `${signedSince}^@`]).split("\n").filter(Boolean));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Read the ci tags and judge each one (#4031). Without `signedSince` in
|
|
407
|
+
* `green` every tag counts, as before. With it, a tag on a strict
|
|
408
|
+
* first-parent ancestor of `signedSince` counts unsigned, and any other tag
|
|
409
|
+
* counts only when it points at the commit its name gives and its annotation
|
|
410
|
+
* carries a statement a runner key at `base` signed for that commit, tag and
|
|
411
|
+
* branch. The runner keys and roles are read from `.chant/trust.json` at
|
|
412
|
+
* `base`, the commit the caller reads the branch at. Throws
|
|
413
|
+
* `declaration-invalid` when `signedSince` names no commit here.
|
|
414
|
+
*/
|
|
415
|
+
export function readCiTags(top: string, green: CiGreen | null = null, base: string | null = null): CiTags {
|
|
416
|
+
const out: CiTags = { green: new Map(), revoked: new Map(), ignored: new Map() };
|
|
417
|
+
const before = green?.signedSince ? unsignedHistory(top, green.signedSince) : null;
|
|
418
|
+
let policy: ReturnType<typeof readTrustPolicy> | undefined;
|
|
419
|
+
const policyAtBase = () => (policy ??= base ? readTrustPolicy(gitRevisionSource(top, base), base) : emptyPolicy(null));
|
|
420
|
+
for (const t of readCiTagRefs(top)) {
|
|
421
|
+
const counted = (signer: string | null) => (t.kind === "green" ? out.green : out.revoked).set(t.sha, { tagged: t.tagged, record: t.record, signer });
|
|
422
|
+
if (before === null) {
|
|
423
|
+
counted(null);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const ignore = (status: CiTagFailure, reason: string) => out.ignored.set(t.sha, [...(out.ignored.get(t.sha) ?? []), { tag: t.tag, status, reason }]);
|
|
427
|
+
if (t.target !== t.sha) {
|
|
428
|
+
ignore("mismatch", `${t.tag} points at ${t.target.slice(0, 12) || "nothing"}, not at the commit its name gives`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (before.has(t.sha)) {
|
|
432
|
+
counted(null);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const p = policyAtBase();
|
|
436
|
+
const v = verifyCiTagStatement(t.record, { sha: t.sha, kind: t.kind, branch: green!.branch }, p.runners, p.roles);
|
|
437
|
+
if (v.status === "signed") counted(v.signer);
|
|
438
|
+
else ignore(v.status, `${v.reason}; ci.green.signedSince is ${green!.signedSince}, so a tag on a commit that is not a strict first-parent ancestor of it needs one`);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
252
441
|
}
|
|
253
442
|
|
|
254
443
|
/** The declaration's `ci.green`, read where `cwd` is, and the git top. */
|
|
@@ -264,12 +453,26 @@ function readGreen(cwd: string): { green: CiGreen; top: string; workspace: { nam
|
|
|
264
453
|
|
|
265
454
|
export interface TickRequest {
|
|
266
455
|
cwd: string;
|
|
267
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Where check runs come from. A function is called only when the source
|
|
458
|
+
* is `checks`, so a tick from runs needs no forge token. Required for
|
|
459
|
+
* `checks`.
|
|
460
|
+
*/
|
|
461
|
+
forge?: CiForge | (() => CiForge);
|
|
462
|
+
/** Where phase results come from (#4044): overrides `ci.green.source`. */
|
|
463
|
+
source?: CiSource;
|
|
268
464
|
/** The git remote the branch and the tags are fetched from and pushed to. Default `origin`. */
|
|
269
465
|
remote?: string;
|
|
270
466
|
/** Decide and report, and make and push no tag. */
|
|
271
467
|
dryRun?: boolean;
|
|
272
468
|
now?: Date;
|
|
469
|
+
/**
|
|
470
|
+
* A runner's Ed25519 private key in PEM (#4031). The tick signs each tag
|
|
471
|
+
* it makes with it, and refuses a key `.chant/trust.json` at the branch tip
|
|
472
|
+
* does not list as a runner whose principal holds the `runner` role. When
|
|
473
|
+
* `ci.green.signedSince` is set the tick needs one.
|
|
474
|
+
*/
|
|
475
|
+
runnerKey?: string;
|
|
273
476
|
}
|
|
274
477
|
|
|
275
478
|
export interface TickResult {
|
|
@@ -280,6 +483,34 @@ export interface TickResult {
|
|
|
280
483
|
commits: TickCommit[];
|
|
281
484
|
/** The tags made and pushed, or that would be under `dryRun`. */
|
|
282
485
|
made: string[];
|
|
486
|
+
/** The runner the tags were signed as (#4031), or null when they carry no statement. */
|
|
487
|
+
signer: string | null;
|
|
488
|
+
/** Where the phase results came from (#4044). */
|
|
489
|
+
source: CiSource;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The signer a runner key makes (#4031): the principal `.chant/trust.json`
|
|
494
|
+
* at `base` lists the key for, who must hold the `runner` role. Throws when
|
|
495
|
+
* the key is not one, so a tick never makes a tag its readers would ignore.
|
|
496
|
+
*/
|
|
497
|
+
export function ciTagSigner(top: string, base: string, pem: string, what = "the tags it signed"): CiTagSigner {
|
|
498
|
+
let loaded: ReturnType<typeof loadRunnerKey>;
|
|
499
|
+
try {
|
|
500
|
+
loaded = loadRunnerKey(pem);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
throw new Error(`the runner key is not an Ed25519 private key in PEM: ${err instanceof Error ? err.message : String(err)}`);
|
|
503
|
+
}
|
|
504
|
+
const policy = readTrustPolicy(gitRevisionSource(top, base), base);
|
|
505
|
+
const at = `.chant/trust.json at ${base.slice(0, 12)}`;
|
|
506
|
+
if (policy.signers.some((s) => s.key === loaded.publicKey)) {
|
|
507
|
+
throw new Error(`the runner key is a signer's key in ${policy.signersPath}; ci tags and runs are signed by a CI identity's key, never by a person's`);
|
|
508
|
+
}
|
|
509
|
+
const listed = policy.runners.find((r) => r.key === loaded.publicKey);
|
|
510
|
+
if (!listed) throw new Error(`${at} lists no runner with this key, so ${what} would be ignored. Add it under "runners" first:\n ${loaded.publicKey}`);
|
|
511
|
+
const holds = (policy.roles[CI_TAG_SIGNER_ROLE] ?? []).some((p) => p.normalize("NFKC").trim().toLowerCase() === listed.principal.normalize("NFKC").trim().toLowerCase());
|
|
512
|
+
if (!holds) throw new Error(`${listed.principal} holds no ${CI_TAG_SIGNER_ROLE} role in ${at}, so ${what} would be ignored. Grant it: "roles": { "${CI_TAG_SIGNER_ROLE}": ["${listed.principal}"] }`);
|
|
513
|
+
return { principal: listed.principal, key: loaded.key, publicKey: loaded.publicKey };
|
|
283
514
|
}
|
|
284
515
|
|
|
285
516
|
/**
|
|
@@ -288,7 +519,11 @@ export interface TickResult {
|
|
|
288
519
|
* window, make the tags that are due and push them, never forced.
|
|
289
520
|
*/
|
|
290
521
|
export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
291
|
-
const
|
|
522
|
+
const read = readGreen(req.cwd);
|
|
523
|
+
const top = read.top;
|
|
524
|
+
const green: CiGreen = { ...read.green, source: req.source ?? read.green.source };
|
|
525
|
+
const forge = green.source === "checks" ? (typeof req.forge === "function" ? req.forge() : req.forge) : undefined;
|
|
526
|
+
if (green.source === "checks" && !forge) throw new Error("ci.green.source is checks, so the tick reads check runs and needs a forge");
|
|
292
527
|
const remote = req.remote ?? "origin";
|
|
293
528
|
const now = req.now ?? new Date();
|
|
294
529
|
const tracking = `refs/remotes/${remote}/${green.branch}`;
|
|
@@ -309,15 +544,44 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
309
544
|
.filter(([, ct]) => Number(ct) >= cutoff)
|
|
310
545
|
.map(([sha, ct]) => ({ sha, committed: iso(new Date(Number(ct) * 1000)) }));
|
|
311
546
|
|
|
312
|
-
const
|
|
547
|
+
const base = git(top, ["rev-parse", `${tracking}^{commit}`]);
|
|
548
|
+
if (green.signedSince && req.runnerKey === undefined && !req.dryRun) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`ci.green.signedSince is ${green.signedSince}, so every tag the tick makes needs a statement signed by a runner key, and none was given: pass --key <runner key.pem> or set CHANT_RUNNER_KEY`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (green.source === "runs" && req.runnerKey === undefined && !req.dryRun) {
|
|
554
|
+
throw new Error("the tick reads chant runs (source runs), and every tag it makes from them is signed by a runner key, and none was given: pass --key <runner key.pem> or set CHANT_RUNNER_KEY");
|
|
555
|
+
}
|
|
556
|
+
const signer = req.runnerKey !== undefined ? ciTagSigner(top, base, req.runnerKey) : undefined;
|
|
557
|
+
const links = await commitLinks(req.cwd, top, tracking, commits.map((c) => c.sha));
|
|
558
|
+
const tags = readCiTags(top, green, base);
|
|
559
|
+
let runs: OpRunSource | undefined;
|
|
560
|
+
if (green.source === "runs") {
|
|
561
|
+
// The runs live on chant/lifecycle, which a CI checkout may not have yet.
|
|
562
|
+
const fetched = await fetchLifecycleStatus({ cwd: top });
|
|
563
|
+
if (fetched.status === "diverged" || fetched.status === "unreachable") {
|
|
564
|
+
throw new Error(`could not fetch chant/lifecycle, where the Op runs are recorded, from ${fetched.remote}: ${fetched.status === "diverged" ? "the local branch and the remote's have diverged" : fetched.stderr}`);
|
|
565
|
+
}
|
|
566
|
+
runs = await readOpRunSource(req.cwd, top, base);
|
|
567
|
+
}
|
|
313
568
|
const planned = await planTick({
|
|
314
569
|
green,
|
|
315
570
|
commits,
|
|
316
571
|
greenTags: new Set(tags.green.keys()),
|
|
317
|
-
revokedTags: tags.revoked,
|
|
318
|
-
|
|
572
|
+
revokedTags: new Set(tags.revoked.keys()),
|
|
573
|
+
ignored: tags.ignored,
|
|
574
|
+
checkRuns: async (sha) => (runs ? runs.checkRuns(sha) : forge!.checkRuns(sha)),
|
|
319
575
|
now,
|
|
576
|
+
signer,
|
|
320
577
|
});
|
|
578
|
+
if (links) for (const c of planned) c.work = links.get(c.sha);
|
|
579
|
+
if (runs) {
|
|
580
|
+
for (const c of planned) {
|
|
581
|
+
const ignored = runs.ignored.get(c.sha);
|
|
582
|
+
if (ignored && ignored.length > 0) c.ignoredRuns = ignored;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
321
585
|
const due = planned.filter((c) => c.made !== null);
|
|
322
586
|
if (!req.dryRun && due.length > 0) {
|
|
323
587
|
const ident = taggerConfig(top);
|
|
@@ -331,7 +595,80 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
331
595
|
throw new Error(`could not push ${due.map((c) => c.made!.tag).join(", ")} to ${remote}: ${cause ? `${cause}\n` : ""}${stderr}`);
|
|
332
596
|
}
|
|
333
597
|
}
|
|
334
|
-
return { branch: green.branch, window: green.window, ref: tracking, commits: planned, made: due.map((c) => c.made!.tag) };
|
|
598
|
+
return { branch: green.branch, window: green.window, ref: tracking, commits: planned, made: due.map((c) => c.made!.tag), signer: signer?.principal ?? null, source: green.source };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── Chant runs as the source (#4044) ─────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
/** The Op runs on the ledger, judged, as check runs by commit. */
|
|
604
|
+
export interface OpRunSource {
|
|
605
|
+
/** The signed runs on `sha`, each as a check run named for its Op. */
|
|
606
|
+
checkRuns(sha: string): CheckRun[];
|
|
607
|
+
/** The runs on each commit that do not count, by commit. */
|
|
608
|
+
ignored: Map<string, IgnoredOpRun[]>;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const RUN_LEDGER_FILE = /^runs__(.+)\.jsonl$/;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* An Op run as a check run (#4044): `ok` passes and `fail` fails once the
|
|
615
|
+
* run has settled; a run stopped at a gate or a decision point is still
|
|
616
|
+
* going. The id is the run's end time in milliseconds, so the latest run of
|
|
617
|
+
* an Op is its latest attempt.
|
|
618
|
+
*/
|
|
619
|
+
export function opRunCheckRun(record: OpRunRecord, signer: string): CheckRun {
|
|
620
|
+
const settled = record.status === "ok" || record.status === "fail";
|
|
621
|
+
const ended = Date.parse(record.ended);
|
|
622
|
+
return {
|
|
623
|
+
id: Number.isFinite(ended) ? ended : 0,
|
|
624
|
+
name: record.op,
|
|
625
|
+
status: settled ? "completed" : record.status,
|
|
626
|
+
conclusion: settled ? (record.status === "ok" ? "success" : "failure") : null,
|
|
627
|
+
completedAt: settled ? record.ended : null,
|
|
628
|
+
url: record.runOrigin?.url ?? null,
|
|
629
|
+
opRun: { id: record.id, env: record.env, signer, statement: record.statement! },
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Judge every Op run record that names a commit against `.chant/trust.json`
|
|
635
|
+
* at `base` (#4044), from the run ledger the project at `cwd` writes to. A
|
|
636
|
+
* record with a valid statement becomes a check run on its `gitSha`; any
|
|
637
|
+
* other is kept as an ignored run. Reads local refs only.
|
|
638
|
+
*/
|
|
639
|
+
export async function readOpRunSource(cwd: string, top: string, base: string): Promise<OpRunSource> {
|
|
640
|
+
const policy = readTrustPolicy(gitRevisionSource(top, base), base);
|
|
641
|
+
const byCommit = new Map<string, CheckRun[]>();
|
|
642
|
+
const ignored = new Map<string, IgnoredOpRun[]>();
|
|
643
|
+
for (const env of await listLedgerTrees({ cwd })) {
|
|
644
|
+
for (const file of await listFilesInDir(env, { cwd })) {
|
|
645
|
+
const op = RUN_LEDGER_FILE.exec(file)?.[1];
|
|
646
|
+
if (!op) continue;
|
|
647
|
+
const { records } = await readRunLedger(env, op, { cwd });
|
|
648
|
+
for (const record of records) {
|
|
649
|
+
if (!record.gitSha) continue;
|
|
650
|
+
const v = verifyOpRunStatement(record, policy.runners, policy.roles);
|
|
651
|
+
if (v.status === "signed") byCommit.set(record.gitSha, [...(byCommit.get(record.gitSha) ?? []), opRunCheckRun(record, v.signer)]);
|
|
652
|
+
else ignored.set(record.gitSha, [...(ignored.get(record.gitSha) ?? []), { run: record.id, op: record.op, env: record.env, status: v.status, reason: v.reason }]);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { checkRuns: (sha) => byCommit.get(sha) ?? [], ignored };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* What `chant run <op> --key` signs a run's record with (#4044): the runner
|
|
661
|
+
* key `.chant/trust.json` at the checkout's `HEAD` lists for a principal
|
|
662
|
+
* holding the `runner` role. Throws when the key is not one, or `cwd` is in
|
|
663
|
+
* no git checkout.
|
|
664
|
+
*/
|
|
665
|
+
export function opRunSigner(cwd: string, pem: string): (record: OpRunRecord) => NonNullable<OpRunRecord["statement"]> {
|
|
666
|
+
const top = gitTop(cwd);
|
|
667
|
+
if (!top) throw new Error("a run is signed against .chant/trust.json at HEAD, and this directory is not in a git repository");
|
|
668
|
+
const head = tryGit(top, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
669
|
+
if (head === null) throw new Error("a run is signed against .chant/trust.json at HEAD, and this repository has no commit yet");
|
|
670
|
+
const signer = ciTagSigner(top, head, pem, "the runs it signed");
|
|
671
|
+
return (record) => signOpRunRecord(record, signer);
|
|
335
672
|
}
|
|
336
673
|
|
|
337
674
|
/**
|
|
@@ -361,11 +698,37 @@ function stderrOf(err: unknown): string {
|
|
|
361
698
|
return String(e.stderr ?? e.message ?? err).trim();
|
|
362
699
|
}
|
|
363
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Whether each commit names its work item (#4029), judged against the work
|
|
703
|
+
* records at `ref`. Undefined when the workspace declares no work kind or
|
|
704
|
+
* they can't be read: then no commit has a link state to report.
|
|
705
|
+
*/
|
|
706
|
+
async function commitLinks(cwd: string, top: string, ref: string, shas: string[]): Promise<Map<string, CommitLink> | undefined> {
|
|
707
|
+
if (shas.length === 0) return undefined;
|
|
708
|
+
try {
|
|
709
|
+
const index = await readWorkIndex(cwd, top, ref);
|
|
710
|
+
if (!index) return undefined;
|
|
711
|
+
return judgeCommits(top, shas, index, mergeCommits(top, shas));
|
|
712
|
+
} catch {
|
|
713
|
+
// A tag and a green list stand without the link, which only explains a commit.
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Why a commit's work item is unknown, for the text of `ci tick` and `ci list`; undefined for a commit that names one. */
|
|
719
|
+
export function whyWorkUnknown(work: CommitLink | null | undefined): string | undefined {
|
|
720
|
+
if (!work) return undefined;
|
|
721
|
+
if (work.state === "unlinked") return "no Chant-Record or Chant-Lease trailer names a work item";
|
|
722
|
+
if (work.state === "wrong") return `the work item named is not there: ${work.problems.map((p) => `${p.trailer} (${p.message})`).join("; ")}`;
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
|
|
364
726
|
/** The text `chant ci tick` prints. */
|
|
365
727
|
export function formatTick(r: TickResult, dryRun = false): string {
|
|
366
|
-
const lines = [`${r.branch}, commits within ${r.window}: ${r.commits.length}`];
|
|
728
|
+
const lines = [`${r.branch}, commits within ${r.window}${r.source === "runs" ? ", judged from chant runs" : ""}: ${r.commits.length}`];
|
|
367
729
|
for (const c of r.commits) {
|
|
368
730
|
const short = c.sha.slice(0, 12);
|
|
731
|
+
const before = lines.length;
|
|
369
732
|
if (c.made) {
|
|
370
733
|
lines.push(` ${short} ${dryRun ? "would make" : "made"} ${c.made.tag.slice(0, c.made.tag.lastIndexOf("/"))}${c.verdict?.failure ? `: phase ${c.verdict.failure.phase}, ${c.verdict.failure.run.name} ${c.verdict.failure.run.conclusion}` : ""}`);
|
|
371
734
|
} else if (c.before === "revoked") {
|
|
@@ -376,12 +739,26 @@ export function formatTick(r: TickResult, dryRun = false): string {
|
|
|
376
739
|
const what = c.before === "green" ? "green" : v.verdict === "fail" ? `failed: phase ${v.failure!.phase}, ${v.failure!.run.name} ${v.failure!.run.conclusion}` : `undecided: ${waiting.join(", ")}`;
|
|
377
740
|
lines.push(` ${short} ${what}`);
|
|
378
741
|
}
|
|
742
|
+
const why = whyWorkUnknown(c.work);
|
|
743
|
+
if (why) lines[before] += `; why unknown: ${why}`;
|
|
744
|
+
for (const t of c.ignored ?? []) lines.push(` ignored ${t.tag} (${t.status}): ${t.reason}`);
|
|
745
|
+
for (const r of c.ignoredRuns ?? []) lines.push(` ignored run ${r.run} of ${r.op} in ${r.env} (${r.status}): ${r.reason}`);
|
|
746
|
+
if (c.blocked) lines.push(` did not make ${c.blocked}: an ignored tag holds the name. Delete it to let the tick tag the commit: git push origin :refs/tags/${c.blocked}`);
|
|
379
747
|
}
|
|
748
|
+
if (r.signer && r.made.length > 0) lines.push(`signed as ${r.signer}`);
|
|
380
749
|
return lines.join("\n");
|
|
381
750
|
}
|
|
382
751
|
|
|
383
752
|
// ── last-green ───────────────────────────────────────────────────────────────
|
|
384
753
|
|
|
754
|
+
/** The ref a reader walks: the remote-tracking branch when there is one, else the local branch. */
|
|
755
|
+
function branchRefOf(top: string, green: CiGreen, remote = "origin"): string {
|
|
756
|
+
const candidates = [`refs/remotes/${remote}/${green.branch}`, `refs/heads/${green.branch}`];
|
|
757
|
+
const ref = candidates.find((r) => tryGit(top, ["rev-parse", "--verify", "--quiet", `${r}^{commit}`]) !== null);
|
|
758
|
+
if (!ref) throw new CiGreenError("ci-branch-unknown", `this repository has neither ${candidates.join(" nor ")}`);
|
|
759
|
+
return ref;
|
|
760
|
+
}
|
|
761
|
+
|
|
385
762
|
export type LastGreenDocument =
|
|
386
763
|
| {
|
|
387
764
|
$schema: string;
|
|
@@ -389,9 +766,11 @@ export type LastGreenDocument =
|
|
|
389
766
|
chant: string;
|
|
390
767
|
workspace: { name: string; root: string; file: string };
|
|
391
768
|
branch: string;
|
|
392
|
-
/** The ref walked: the remote-tracking branch when there is one, else the local branch. */
|
|
769
|
+
/** The ref walked: the remote-tracking branch when there is one, else the local branch, or the revision `from` named. */
|
|
393
770
|
ref: string;
|
|
394
771
|
commit: { sha: string; tag: string; tagged: string | null; committed: string } | null;
|
|
772
|
+
/** Tags on the commits walked, down to the one found, that were ignored for want of a valid statement (#4031). Newest first. */
|
|
773
|
+
ignored: IgnoredCiTag[];
|
|
395
774
|
}
|
|
396
775
|
| { $schema: string; contract: number; chant: string; error: { code: CiGreenErrorCode; message: string } };
|
|
397
776
|
|
|
@@ -400,25 +779,228 @@ export type LastGreenDocument =
|
|
|
400
779
|
* green tag and no revoked tag. It reads local refs and tags and never
|
|
401
780
|
* fetches, so a reader fetches first (`git fetch origin --tags`).
|
|
402
781
|
*/
|
|
403
|
-
export function lastGreen(req: { cwd: string; remote?: string }): LastGreenDocument {
|
|
782
|
+
export function lastGreen(req: { cwd: string; remote?: string; from?: string }): LastGreenDocument {
|
|
404
783
|
const head = { $schema: CI_LAST_GREEN_SCHEMA_ID, contract: CI_LAST_GREEN_CONTRACT_VERSION, chant: readerVersion() };
|
|
405
784
|
try {
|
|
406
785
|
const { green, top, workspace } = readGreen(req.cwd);
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
786
|
+
const branchRef = branchRefOf(top, green, req.remote);
|
|
787
|
+
let ref = branchRef;
|
|
788
|
+
if (req.from !== undefined) {
|
|
789
|
+
if (req.from.startsWith("-") || tryGit(top, ["rev-parse", "--verify", "--quiet", `${req.from}^{commit}`]) === null) {
|
|
790
|
+
throw new WorkspaceReadError("revision-unknown", `--from ${req.from} names no commit in this repository`);
|
|
791
|
+
}
|
|
792
|
+
ref = req.from;
|
|
793
|
+
}
|
|
794
|
+
// The runner keys are read where the branch is, whichever commit the walk starts from.
|
|
795
|
+
const tags = readCiTags(top, green, git(top, ["rev-parse", `${branchRef}^{commit}`]));
|
|
411
796
|
let commit: Extract<LastGreenDocument, { branch: string }>["commit"] = null;
|
|
412
|
-
|
|
797
|
+
const ignored: IgnoredCiTag[] = [];
|
|
798
|
+
if (tags.green.size > 0 || tags.ignored.size > 0) {
|
|
413
799
|
for (const sha of git(top, ["rev-list", "--first-parent", ref]).split("\n")) {
|
|
800
|
+
ignored.push(...(tags.ignored.get(sha) ?? []));
|
|
414
801
|
if (!tags.green.has(sha) || tags.revoked.has(sha)) continue;
|
|
415
|
-
commit = { sha, tag: `${CI_GREEN_TAG_PREFIX}${sha}`, tagged: tags.green.get(sha)
|
|
802
|
+
commit = { sha, tag: `${CI_GREEN_TAG_PREFIX}${sha}`, tagged: tags.green.get(sha)!.tagged, committed: git(top, ["show", "-s", "--format=%cI", sha]) };
|
|
416
803
|
break;
|
|
417
804
|
}
|
|
418
805
|
}
|
|
419
|
-
return { ...head, workspace, branch: green.branch, ref, commit };
|
|
806
|
+
return { ...head, workspace, branch: green.branch, ref, commit, ignored };
|
|
807
|
+
} catch (err) {
|
|
808
|
+
if (err instanceof CiGreenError || err instanceof WorkspaceReadError) return { ...head, error: { code: err.code, message: err instanceof WorkspaceReadError ? err.describe() : err.message } };
|
|
809
|
+
throw err;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
814
|
+
|
|
815
|
+
export const CI_LIST_CONTRACT_VERSION = 1;
|
|
816
|
+
export const CI_LIST_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/ci-list/v1/ci-list.schema.json";
|
|
817
|
+
|
|
818
|
+
/** A commit's state from its tags: `green`, `revoked` (a revoked tag wins), or `undecided` (no ci tag). */
|
|
819
|
+
export type CiCommitState = "green" | "revoked" | "undecided";
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* One required phase's result on a commit. `pass` and `fail` are what a tag
|
|
823
|
+
* recorded or a live read found, `pending` is a live read with runs still
|
|
824
|
+
* going or not yet reported, and `unrecorded` means no tag holds the result
|
|
825
|
+
* and the forge was not asked.
|
|
826
|
+
*/
|
|
827
|
+
export type CiPhaseResult = "pass" | "fail" | "pending" | "unrecorded";
|
|
828
|
+
|
|
829
|
+
export interface CiListRun {
|
|
830
|
+
name: string;
|
|
831
|
+
id: number | null;
|
|
832
|
+
conclusion: string | null;
|
|
833
|
+
url: string | null;
|
|
834
|
+
/** The Op run, when the run is a chant run (#4044): its id, env and the runner whose statement it carries. Absent for a check run. */
|
|
835
|
+
opRun?: { id: string; env: string; signer: string };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export interface CiListPhase {
|
|
839
|
+
phase: string;
|
|
840
|
+
result: CiPhaseResult;
|
|
841
|
+
runs: CiListRun[];
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export interface CiListCommit {
|
|
845
|
+
sha: string;
|
|
846
|
+
/** Committer time, ISO 8601. */
|
|
847
|
+
committed: string;
|
|
848
|
+
state: CiCommitState;
|
|
849
|
+
/**
|
|
850
|
+
* The green tag's name and time, or null when the commit has none that
|
|
851
|
+
* counts. `tagged` is null for a lightweight tag. `signer` is the runner
|
|
852
|
+
* whose statement it carries, or null when it counts unsigned (#4031).
|
|
853
|
+
*/
|
|
854
|
+
green: { tag: string; tagged: string | null; signer: string | null } | null;
|
|
855
|
+
/** The revoked tag's name, time, the phase and run its annotation names, and its signer as for `green`. */
|
|
856
|
+
revoked: { tag: string; tagged: string | null; phase: string | null; run: CiListRun | null; signer: string | null } | null;
|
|
857
|
+
/** The commit's ci tags that do not count, each for want of a valid statement (#4031). Empty unless ci.green.signedSince is set. */
|
|
858
|
+
ignored: IgnoredCiTag[];
|
|
859
|
+
/** Each required phase in `require` order. */
|
|
860
|
+
phases: CiListPhase[];
|
|
861
|
+
/**
|
|
862
|
+
* Whether the commit names its work item (#4029), or null when the checkout
|
|
863
|
+
* is in no workspace that declares a work kind.
|
|
864
|
+
*/
|
|
865
|
+
work: CommitLink | null;
|
|
866
|
+
/** The Op runs on the commit that do not count, each for want of a valid statement (#4044). Present only when the source is chant runs. */
|
|
867
|
+
ignoredRuns?: IgnoredOpRun[];
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export type CiListDocument =
|
|
871
|
+
| {
|
|
872
|
+
$schema: string;
|
|
873
|
+
contract: number;
|
|
874
|
+
chant: string;
|
|
875
|
+
workspace: { name: string; root: string; file: string };
|
|
876
|
+
branch: string;
|
|
877
|
+
ref: string;
|
|
878
|
+
/** ci.green.window, or null when `since` replaced it. */
|
|
879
|
+
window: string | null;
|
|
880
|
+
/** The revision the walk stopped at (exclusive), when `since` was given. */
|
|
881
|
+
since: string | null;
|
|
882
|
+
/** ci.green.require. */
|
|
883
|
+
require: string[];
|
|
884
|
+
/** Newest first. */
|
|
885
|
+
commits: CiListCommit[];
|
|
886
|
+
}
|
|
887
|
+
| { $schema: string; contract: number; chant: string; error: { code: CiGreenErrorCode; message: string } };
|
|
888
|
+
|
|
889
|
+
const asRun = (r: unknown): CiListRun | null => {
|
|
890
|
+
if (!r || typeof r !== "object") return null;
|
|
891
|
+
const o = r as Record<string, unknown>;
|
|
892
|
+
if (typeof o.name !== "string") return null;
|
|
893
|
+
const op = o.opRun as Record<string, unknown> | undefined;
|
|
894
|
+
const opRun = op && typeof op === "object" && typeof op.id === "string" && typeof op.env === "string" && typeof op.signer === "string" ? { opRun: { id: op.id, env: op.env, signer: op.signer } } : {};
|
|
895
|
+
return { name: o.name, id: typeof o.id === "number" ? o.id : null, conclusion: typeof o.conclusion === "string" ? o.conclusion : null, url: typeof o.url === "string" ? o.url : null, ...opRun };
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
/** A judged run as `ci list` shows it. */
|
|
899
|
+
const listRun = (r: RunVerdict): CiListRun => ({ name: r.name, id: r.opRun ? null : r.id, conclusion: r.conclusion, url: r.url, ...(r.opRun ? { opRun: { id: r.opRun.id, env: r.opRun.env, signer: r.opRun.signer } } : {}) });
|
|
900
|
+
|
|
901
|
+
/** What the green tag's annotation recorded for a phase: its runs, all of which passed when the tag was made. */
|
|
902
|
+
function recordedPhase(green: CountedCiTag | undefined, phase: string): CiListPhase {
|
|
903
|
+
const runs = (green?.record?.phases as Record<string, unknown> | undefined)?.[phase];
|
|
904
|
+
if (!Array.isArray(runs)) return { phase, result: "unrecorded", runs: [] };
|
|
905
|
+
return { phase, result: "pass", runs: runs.map(asRun).filter((r): r is CiListRun => r !== null) };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export interface ListRequest {
|
|
909
|
+
cwd: string;
|
|
910
|
+
remote?: string;
|
|
911
|
+
/** Keep only the newest n commits. */
|
|
912
|
+
limit?: number;
|
|
913
|
+
/** Walk back to this revision, exclusive, instead of the declared window. */
|
|
914
|
+
since?: string;
|
|
915
|
+
/** Judge undecided commits from the forge's check runs. Without it they list as `unrecorded`. */
|
|
916
|
+
forge?: CiForge;
|
|
917
|
+
/**
|
|
918
|
+
* Where phase results come from (#4044): overrides `ci.green.source`. With
|
|
919
|
+
* `runs`, undecided commits are judged from the signed Op runs on the
|
|
920
|
+
* local `chant/lifecycle`, and `forge` is refused.
|
|
921
|
+
*/
|
|
922
|
+
source?: CiSource;
|
|
923
|
+
now?: Date;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* `chant ci list`: the branch's first-parent commits within the window, each
|
|
928
|
+
* with its state from the ci tags and each required phase's result. Tags
|
|
929
|
+
* give the green and revoked results offline; only undecided commits need
|
|
930
|
+
* the forge, and only when a `forge` is given. It never fetches.
|
|
931
|
+
*/
|
|
932
|
+
export async function ciList(req: ListRequest): Promise<CiListDocument> {
|
|
933
|
+
const head = { $schema: CI_LIST_SCHEMA_ID, contract: CI_LIST_CONTRACT_VERSION, chant: readerVersion() };
|
|
934
|
+
try {
|
|
935
|
+
const { green, top, workspace } = readGreen(req.cwd);
|
|
936
|
+
const ref = branchRefOf(top, green, req.remote);
|
|
937
|
+
|
|
938
|
+
const logArgs = ["log", "--first-parent", "--format=%H %ct"];
|
|
939
|
+
let cutoff = 0;
|
|
940
|
+
if (req.since !== undefined) {
|
|
941
|
+
if (tryGit(top, ["rev-parse", "--verify", "--quiet", `${req.since}^{commit}`]) === null) throw new WorkspaceReadError("revision-unknown", `--since ${req.since} names no commit in this repository`);
|
|
942
|
+
logArgs.push(`${req.since}..${ref}`);
|
|
943
|
+
} else {
|
|
944
|
+
cutoff = Math.floor(((req.now ?? new Date()).getTime() - windowMs(green.window)) / 1000);
|
|
945
|
+
logArgs.push(`--max-age=${cutoff}`, ref);
|
|
946
|
+
}
|
|
947
|
+
let commits = git(top, logArgs)
|
|
948
|
+
.split("\n")
|
|
949
|
+
.filter(Boolean)
|
|
950
|
+
.map((line) => line.split(" "))
|
|
951
|
+
.filter(([, ct]) => Number(ct) >= cutoff)
|
|
952
|
+
.map(([sha, ct]) => ({ sha, committed: iso(new Date(Number(ct) * 1000)) }));
|
|
953
|
+
if (req.limit !== undefined) commits = commits.slice(0, req.limit);
|
|
954
|
+
|
|
955
|
+
const links = await commitLinks(req.cwd, top, ref, commits.map((c) => c.sha));
|
|
956
|
+
const base = git(top, ["rev-parse", `${ref}^{commit}`]);
|
|
957
|
+
const tags = readCiTags(top, green, base);
|
|
958
|
+
const source = req.source ?? green.source;
|
|
959
|
+
if (source === "runs" && req.forge) throw new Error("--forge reads check runs, and the source is chant runs (ci.green.source or --source runs); drop one");
|
|
960
|
+
const runs = source === "runs" ? await readOpRunSource(req.cwd, top, base) : undefined;
|
|
961
|
+
const out: CiListCommit[] = [];
|
|
962
|
+
for (const c of commits) {
|
|
963
|
+
const g = tags.green.get(c.sha);
|
|
964
|
+
const r = tags.revoked.get(c.sha);
|
|
965
|
+
const state: CiCommitState = r ? "revoked" : g ? "green" : "undecided";
|
|
966
|
+
let phases = green.require.map((name) => recordedPhase(g, name));
|
|
967
|
+
let revoked: CiListCommit["revoked"] = null;
|
|
968
|
+
if (r) {
|
|
969
|
+
const phase = typeof r.record?.phase === "string" ? r.record.phase : null;
|
|
970
|
+
const run = asRun(r.record?.run);
|
|
971
|
+
revoked = { tag: `${CI_REVOKED_TAG_PREFIX}${c.sha}`, tagged: r.tagged, phase, run, signer: r.signer };
|
|
972
|
+
phases = phases.map((p) => (p.phase === phase ? { phase: p.phase, result: "fail", runs: run ? [run] : [] } : p));
|
|
973
|
+
} else if (!g && (runs || req.forge)) {
|
|
974
|
+
const verdict = evaluateCommit(green, runs ? runs.checkRuns(c.sha) : await req.forge!.checkRuns(c.sha));
|
|
975
|
+
phases = verdict.phases.map((p) => ({ phase: p.phase, result: p.verdict, runs: p.runs.map(listRun) }));
|
|
976
|
+
}
|
|
977
|
+
out.push({
|
|
978
|
+
...c,
|
|
979
|
+
state,
|
|
980
|
+
green: g ? { tag: `${CI_GREEN_TAG_PREFIX}${c.sha}`, tagged: g.tagged, signer: g.signer } : null,
|
|
981
|
+
revoked,
|
|
982
|
+
phases,
|
|
983
|
+
work: links?.get(c.sha) ?? null,
|
|
984
|
+
ignored: tags.ignored.get(c.sha) ?? [],
|
|
985
|
+
...(runs ? { ignoredRuns: runs.ignored.get(c.sha) ?? [] } : {}),
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return { ...head, workspace, branch: green.branch, ref, window: req.since === undefined ? green.window : null, since: req.since ?? null, require: [...green.require], commits: out };
|
|
420
989
|
} catch (err) {
|
|
421
990
|
if (err instanceof CiGreenError || err instanceof WorkspaceReadError) return { ...head, error: { code: err.code, message: err instanceof WorkspaceReadError ? err.describe() : err.message } };
|
|
422
991
|
throw err;
|
|
423
992
|
}
|
|
424
993
|
}
|
|
994
|
+
|
|
995
|
+
/** The text `chant ci list` prints: one line per commit. */
|
|
996
|
+
export function formatList(doc: Extract<CiListDocument, { branch: string }>): string {
|
|
997
|
+
const lines = [`${doc.branch}, ${doc.since ? `commits after ${doc.since}` : `commits within ${doc.window}`}: ${doc.commits.length}`];
|
|
998
|
+
for (const c of doc.commits) {
|
|
999
|
+
const phases = c.phases.map((p) => `${p.phase}=${p.result}`).join(" ");
|
|
1000
|
+
const why = whyWorkUnknown(c.work);
|
|
1001
|
+
lines.push(` ${c.sha.slice(0, 12)} ${c.state.padEnd(9)} ${phases}${why ? `; why unknown: ${why}` : ""}`);
|
|
1002
|
+
for (const t of c.ignored) lines.push(` ignored ${t.tag} (${t.status}): ${t.reason}`);
|
|
1003
|
+
for (const r of c.ignoredRuns ?? []) lines.push(` ignored run ${r.run} of ${r.op} in ${r.env} (${r.status}): ${r.reason}`);
|
|
1004
|
+
}
|
|
1005
|
+
return lines.join("\n");
|
|
1006
|
+
}
|