@intentius/chant 0.124.46 → 0.124.48
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/dist/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-pass.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-skip.json +10 -0
- package/dist/__golden__/wave-loop/op-cost-member-clash.json +6 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +13 -0
- package/dist/__golden__/wave-loop/op-denied-source.json +16 -0
- package/dist/__golden__/wave-loop/op-denied.json +16 -0
- package/dist/__golden__/wave-loop/op-discover-rest-short.json +6 -0
- package/dist/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
- package/dist/__golden__/wave-loop/op-discover-rest.json +26 -0
- package/dist/__golden__/wave-loop/op-discover-wide.json +26 -0
- package/dist/__golden__/wave-loop/op-held-on-again.json +16 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
- package/dist/__golden__/wave-loop/op-held-on.json +16 -0
- package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-pass.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +33 -0
- package/dist/__golden__/wave-loop/op-outcome-held.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-replan.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
- package/dist/__golden__/wave-loop/op-overridden.json +16 -0
- package/dist/__golden__/wave-loop/op-per-run.json +20 -0
- package/dist/__golden__/wave-loop/op-point.json +13 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +10 -0
- package/dist/__golden__/wave-loop/op-sealed.json +10 -0
- package/dist/__golden__/wave-loop/op-spend-change-only.json +10 -0
- package/dist/__golden__/wave-loop/op-spend.json +10 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +55 -0
- package/dist/__golden__/wave-loop/op-wide-refused.json +26 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +29 -0
- package/dist/__golden__/wave-loop/op-wide.json +39 -0
- package/dist/action-pins.d.ts +61 -0
- package/dist/action-pins.d.ts.map +1 -0
- package/dist/action-pins.js +77 -0
- package/dist/cli/commands/carve.d.ts +3 -0
- package/dist/cli/commands/carve.d.ts.map +1 -1
- package/dist/cli/commands/carve.js +36 -7
- package/dist/cli/commands/import-report.d.ts +26 -0
- package/dist/cli/commands/import-report.d.ts.map +1 -0
- package/dist/cli/commands/import-report.js +65 -0
- package/dist/cli/commands/import.d.ts +11 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +22 -3
- package/dist/cli/commands/migrate.d.ts +13 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +134 -10
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +21 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +6 -1
- package/dist/cli/handlers/ledger.d.ts +3 -0
- package/dist/cli/handlers/ledger.d.ts.map +1 -0
- package/dist/cli/handlers/ledger.js +55 -0
- package/dist/cli/handlers/migrate.d.ts.map +1 -1
- package/dist/cli/handlers/migrate.js +18 -11
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/misc.js +12 -1
- package/dist/cli/handlers/pr-env.js +2 -2
- package/dist/cli/handlers/pr.d.ts +15 -1
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +143 -39
- package/dist/cli/handlers/query.d.ts +7 -2
- package/dist/cli/handlers/query.d.ts.map +1 -1
- package/dist/cli/handlers/query.js +16 -4
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +6 -3
- package/dist/cli/handlers/search.d.ts +2 -4
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +13 -39
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +65 -14
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +36 -2
- package/dist/cli/registry.d.ts +14 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/builders.d.ts +5 -1
- package/dist/components/builders.d.ts.map +1 -1
- package/dist/components/builders.js +5 -0
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.js +16 -1
- package/dist/components/capability-plugin.d.ts +14 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/pr-hooks.d.ts +21 -1
- package/dist/components/pr-hooks.d.ts.map +1 -1
- package/dist/components/pr-hooks.js +72 -4
- package/dist/components/pr-pipeline.d.ts +45 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +47 -14
- package/dist/components/pr-policy.d.ts +36 -0
- package/dist/components/pr-policy.d.ts.map +1 -1
- package/dist/components/pr-policy.js +36 -0
- package/dist/components/starter-plugin.d.ts +1 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/starter-plugin.js +5 -1
- package/dist/components/verbs/index.d.ts +2 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/index.js +2 -0
- package/dist/components/verbs/publish-artifact.d.ts +65 -0
- package/dist/components/verbs/publish-artifact.d.ts.map +1 -0
- package/dist/components/verbs/publish-artifact.js +74 -0
- package/dist/components/verbs/publish-image.d.ts +129 -0
- package/dist/components/verbs/publish-image.d.ts.map +1 -0
- package/dist/components/verbs/publish-image.js +145 -0
- package/dist/config-environments.d.ts +20 -0
- package/dist/config-environments.d.ts.map +1 -1
- package/dist/config-environments.js +12 -0
- package/dist/config.d.ts +9 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -2
- package/dist/gate-decision.d.ts +11 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +15 -3
- package/dist/graph-ir.d.ts +25 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-walk.d.ts +66 -0
- package/dist/graph-walk.d.ts.map +1 -0
- package/dist/graph-walk.js +70 -0
- package/dist/import/parser.d.ts +19 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/lexicon.d.ts +71 -6
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +11 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +2 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/ledger-read.d.ts +65 -0
- package/dist/lifecycle/ledger-read.d.ts.map +1 -0
- package/dist/lifecycle/ledger-read.js +160 -0
- package/dist/lifecycle/release-workstation.d.ts +7 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -1
- package/dist/lifecycle/release-workstation.js +25 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -3
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/discover.js +3 -1
- package/dist/lint/post-synth.d.ts +127 -1
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/post-synth.js +84 -2
- package/dist/lint/rules/op/index.d.ts +1 -0
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +2 -0
- package/dist/lint/rules/op/ops019-workspace-op-target.d.ts +32 -0
- package/dist/lint/rules/op/ops019-workspace-op-target.d.ts.map +1 -0
- package/dist/lint/rules/op/ops019-workspace-op-target.js +63 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -2
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +5 -2
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +1 -1
- package/dist/op/activities/review-agent.d.ts +90 -4
- package/dist/op/activities/review-agent.d.ts.map +1 -1
- package/dist/op/activities/review-agent.js +217 -19
- package/dist/op/activities/work-evidence.d.ts +8 -1
- package/dist/op/activities/work-evidence.d.ts.map +1 -1
- package/dist/op/activities/work-evidence.js +1 -0
- package/dist/op/builders.d.ts +9 -5
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -5
- package/dist/op/composites/apply-op.d.ts +29 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/apply-op.js +20 -1
- package/dist/op/gate-approval.d.ts +11 -5
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +11 -6
- package/dist/op/gate-quorum.d.ts +100 -3
- package/dist/op/gate-quorum.d.ts.map +1 -1
- package/dist/op/gate-quorum.js +108 -6
- package/dist/op/gate-review.d.ts +9 -0
- package/dist/op/gate-review.d.ts.map +1 -1
- package/dist/op/gate-review.js +13 -0
- package/dist/op/gate.d.ts +44 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +25 -4
- package/dist/op/index.d.ts +3 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +38 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +48 -6
- package/dist/op/op-waves-run.d.ts +26 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +259 -13
- package/dist/op/op-waves.d.ts +72 -5
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +110 -15
- package/dist/op/runtime.d.ts +11 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/schedule-overdue.d.ts +16 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -1
- package/dist/op/schedule-overdue.js +36 -0
- package/dist/op/stage-hooks.d.ts +9 -2
- package/dist/op/stage-hooks.d.ts.map +1 -1
- package/dist/op/stage-hooks.js +7 -5
- package/dist/op/workspace-run-env.d.ts +22 -0
- package/dist/op/workspace-run-env.d.ts.map +1 -0
- package/dist/op/workspace-run-env.js +21 -0
- package/dist/pr-forge.d.ts +24 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +47 -0
- package/dist/pr-post.d.ts +42 -0
- package/dist/pr-post.d.ts.map +1 -1
- package/dist/pr-post.js +67 -1
- package/dist/read-filter.d.ts +65 -0
- package/dist/read-filter.d.ts.map +1 -0
- package/dist/read-filter.js +179 -0
- package/dist/run-event.d.ts +1 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/terraform/carve.d.ts +24 -3
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/carve.js +32 -14
- package/dist/terraform/cuts.d.ts +97 -0
- package/dist/terraform/cuts.d.ts.map +1 -0
- package/dist/terraform/cuts.js +138 -0
- package/dist/terraform/folds.d.ts +26 -0
- package/dist/terraform/folds.d.ts.map +1 -0
- package/dist/terraform/folds.js +38 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/graph.js +10 -0
- package/dist/terraform/reach.d.ts +140 -0
- package/dist/terraform/reach.d.ts.map +1 -0
- package/dist/terraform/reach.js +170 -0
- package/dist/terraform/score.d.ts +14 -0
- package/dist/terraform/score.d.ts.map +1 -1
- package/dist/terraform/score.js +14 -31
- package/dist/terraform/state.d.ts.map +1 -1
- package/dist/terraform/state.js +4 -2
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/wave-blocked.d.ts +10 -1
- package/dist/wave-blocked.d.ts.map +1 -1
- package/dist/wave-blocked.js +31 -3
- package/dist/wave-runtime.mjs +263 -254
- package/dist/workspace/check.schema.json +11 -2
- package/dist/workspace/checks/ci-branch.d.ts +44 -0
- package/dist/workspace/checks/ci-branch.d.ts.map +1 -0
- package/dist/workspace/checks/ci-branch.js +133 -0
- package/dist/workspace/checks/links.d.ts +3 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/links.js +12 -1
- package/dist/workspace/checks/org.d.ts +48 -0
- package/dist/workspace/checks/org.d.ts.map +1 -0
- package/dist/workspace/checks/org.js +156 -0
- package/dist/workspace/checks.d.ts +11 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +59 -7
- package/dist/workspace/ci-branches.d.ts +16 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -1
- package/dist/workspace/ci-branches.js +27 -0
- package/dist/workspace/ci-green-cli.js +7 -7
- package/dist/workspace/ci-green-workflow.d.ts +1 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +13 -8
- package/dist/workspace/ci-green.d.ts +14 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +44 -11
- package/dist/workspace/ci-run.d.ts +9 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +67 -6
- package/dist/workspace/ci-serve.d.ts +9 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +114 -76
- package/dist/workspace/compose-graph.d.ts +14 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +4 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/composites.js +5 -2
- package/dist/workspace/composites.schema.json +22 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
- package/dist/workspace/cross-root-lint.d.ts +56 -0
- package/dist/workspace/cross-root-lint.d.ts.map +1 -0
- package/dist/workspace/cross-root-lint.js +111 -0
- package/dist/workspace/dbt-models-reader.d.ts +44 -0
- package/dist/workspace/dbt-models-reader.d.ts.map +1 -0
- package/dist/workspace/dbt-models-reader.js +356 -0
- package/dist/workspace/dbt-project-common.d.ts +40 -0
- package/dist/workspace/dbt-project-common.d.ts.map +1 -0
- package/dist/workspace/dbt-project-common.js +106 -0
- package/dist/workspace/dbt-project-reader.d.ts +4 -3
- package/dist/workspace/dbt-project-reader.d.ts.map +1 -1
- package/dist/workspace/dbt-project-reader.js +14 -108
- package/dist/workspace/decision-rank.d.ts +26 -0
- package/dist/workspace/decision-rank.d.ts.map +1 -0
- package/dist/workspace/decision-rank.js +16 -0
- package/dist/workspace/declaration.d.ts +43 -2
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +45 -11
- package/dist/workspace/declaration.schema.json +133 -24
- package/dist/workspace/environments.d.ts +26 -2
- package/dist/workspace/environments.d.ts.map +1 -1
- package/dist/workspace/environments.js +26 -2
- package/dist/workspace/flyway-dir.d.ts +39 -0
- package/dist/workspace/flyway-dir.d.ts.map +1 -0
- package/dist/workspace/flyway-dir.js +104 -0
- package/dist/workspace/flyway-layout.d.ts +96 -0
- package/dist/workspace/flyway-layout.d.ts.map +1 -0
- package/dist/workspace/flyway-layout.js +308 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +26 -8
- package/dist/workspace/graph.schema.json +34 -0
- package/dist/workspace/intent.d.ts +2 -3
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +5 -9
- package/dist/workspace/kind-link-readers.d.ts +59 -2
- package/dist/workspace/kind-link-readers.d.ts.map +1 -1
- package/dist/workspace/kind-link-readers.js +17 -1
- package/dist/workspace/kinds.d.ts +66 -2
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +98 -4
- package/dist/workspace/ledger.schema.json +158 -0
- package/dist/workspace/lexicon-outputs.d.ts +20 -1
- package/dist/workspace/lexicon-outputs.d.ts.map +1 -1
- package/dist/workspace/lexicon-outputs.js +102 -10
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-check.js +23 -4
- package/dist/workspace/links.d.ts +8 -1
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/links.js +25 -1
- package/dist/workspace/ls.d.ts +8 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +7 -1
- package/dist/workspace/ls.schema.json +7 -0
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +54 -5
- package/dist/workspace/migrate-links.d.ts +45 -0
- package/dist/workspace/migrate-links.d.ts.map +1 -0
- package/dist/workspace/migrate-links.js +100 -0
- package/dist/workspace/migration-dirs.d.ts +74 -0
- package/dist/workspace/migration-dirs.d.ts.map +1 -0
- package/dist/workspace/migration-dirs.js +254 -0
- package/dist/workspace/reason-codes.d.ts +8 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +11 -0
- package/dist/workspace/records-cli.d.ts +11 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.js +57 -5
- package/dist/workspace/records.schema.json +5 -0
- package/dist/workspace/release-candidates.d.ts +2 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -1
- package/dist/workspace/release-candidates.js +5 -2
- package/dist/workspace/release-train.d.ts +53 -0
- package/dist/workspace/release-train.d.ts.map +1 -0
- package/dist/workspace/release-train.js +134 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -1
- package/dist/workspace/release-workflow.js +4 -3
- package/dist/workspace/repo-ref.d.ts +45 -0
- package/dist/workspace/repo-ref.d.ts.map +1 -0
- package/dist/workspace/repo-ref.js +114 -0
- package/dist/workspace/run-event.schema.json +2 -2
- package/dist/workspace/runs-cli.d.ts +9 -0
- package/dist/workspace/runs-cli.d.ts.map +1 -1
- package/dist/workspace/runs-cli.js +48 -3
- package/dist/workspace/runs.schema.json +7 -0
- package/dist/workspace/search-contract.d.ts +1 -1
- package/dist/workspace/search-contract.d.ts.map +1 -1
- package/dist/workspace/status-decisions.d.ts +37 -0
- package/dist/workspace/status-decisions.d.ts.map +1 -0
- package/dist/workspace/status-decisions.js +147 -0
- package/dist/workspace/status-gates.d.ts +9 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +4 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +14 -2
- package/dist/workspace/status.schema.json +6 -0
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-cli.js +5 -3
- package/dist/workspace/work-evidence-ops.d.ts +83 -0
- package/dist/workspace/work-evidence-ops.d.ts.map +1 -0
- package/dist/workspace/work-evidence-ops.js +169 -0
- package/dist/workspace/work-evidence.d.ts +12 -2
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work-evidence.js +21 -3
- package/dist/workspace/work-evidence.schema.json +49 -2
- package/dist/workspace/workspace-gates.d.ts +110 -0
- package/dist/workspace/workspace-gates.d.ts.map +1 -0
- package/dist/workspace/workspace-gates.js +140 -0
- package/dist/workspace/workspace-kinds.schema.json +58 -3
- package/dist/workspace/workspace-ledger.d.ts +76 -0
- package/dist/workspace/workspace-ledger.d.ts.map +1 -0
- package/dist/workspace/workspace-ledger.js +91 -0
- package/dist/workspace/workspace-run.d.ts +112 -0
- package/dist/workspace/workspace-run.d.ts.map +1 -0
- package/dist/workspace/workspace-run.js +432 -0
- package/package.json +1 -1
- package/src/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-pass.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-skip.json +10 -0
- package/src/__golden__/wave-loop/op-cost-member-clash.json +6 -0
- package/src/__golden__/wave-loop/op-cost-member.json +13 -0
- package/src/__golden__/wave-loop/op-denied-source.json +16 -0
- package/src/__golden__/wave-loop/op-denied.json +16 -0
- package/src/__golden__/wave-loop/op-discover-rest-short.json +6 -0
- package/src/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
- package/src/__golden__/wave-loop/op-discover-rest.json +26 -0
- package/src/__golden__/wave-loop/op-discover-wide.json +26 -0
- package/src/__golden__/wave-loop/op-held-on-again.json +16 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
- package/src/__golden__/wave-loop/op-held-on.json +16 -0
- package/src/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-pass.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +33 -0
- package/src/__golden__/wave-loop/op-outcome-held.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-replan.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
- package/src/__golden__/wave-loop/op-overridden.json +16 -0
- package/src/__golden__/wave-loop/op-per-run.json +20 -0
- package/src/__golden__/wave-loop/op-point.json +13 -0
- package/src/__golden__/wave-loop/op-pr-review.json +10 -0
- package/src/__golden__/wave-loop/op-sealed.json +10 -0
- package/src/__golden__/wave-loop/op-spend-change-only.json +10 -0
- package/src/__golden__/wave-loop/op-spend.json +10 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +55 -0
- package/src/__golden__/wave-loop/op-wide-refused.json +26 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +29 -0
- package/src/__golden__/wave-loop/op-wide.json +39 -0
- package/src/action-pins.test.ts +78 -0
- package/src/action-pins.ts +92 -0
- package/src/cli/commands/carve-score-once.test.ts +99 -0
- package/src/cli/commands/carve.ts +37 -6
- package/src/cli/commands/check-lexicon.test.ts +2 -2
- package/src/cli/commands/import-report.ts +71 -0
- package/src/cli/commands/import.ts +33 -3
- package/src/cli/commands/migrate-dialect.test.ts +145 -0
- package/src/cli/commands/migrate.ts +132 -11
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +4 -4
- package/src/cli/handlers/approve.ts +23 -2
- package/src/cli/handlers/build.ts +7 -1
- package/src/cli/handlers/ledger.ts +54 -0
- package/src/cli/handlers/migrate.ts +21 -12
- package/src/cli/handlers/misc.ts +10 -1
- package/src/cli/handlers/pr-env.ts +2 -2
- package/src/cli/handlers/pr.ts +138 -36
- package/src/cli/handlers/query.ts +16 -4
- package/src/cli/handlers/release.ts +6 -3
- package/src/cli/handlers/search.ts +13 -34
- package/src/cli/main.test.ts +15 -0
- package/src/cli/main.ts +65 -14
- package/src/cli/parse-args.ts +27 -2
- package/src/cli/registry.ts +14 -4
- package/src/components/builders.ts +8 -0
- package/src/components/capability-plugin-loader.ts +13 -1
- package/src/components/capability-plugin.ts +16 -0
- package/src/components/pr-hooks.test.ts +73 -5
- package/src/components/pr-hooks.ts +95 -5
- package/src/components/pr-pipeline.ts +64 -16
- package/src/components/pr-policy.test.ts +64 -1
- package/src/components/pr-policy.ts +53 -0
- package/src/components/registry.test.ts +1 -1
- package/src/components/starter-plugin.ts +6 -0
- package/src/components/verbs/index.ts +2 -0
- package/src/components/verbs/publish-artifact.test.ts +60 -0
- package/src/components/verbs/publish-artifact.ts +122 -0
- package/src/components/verbs/publish-image.test.ts +101 -0
- package/src/components/verbs/publish-image.ts +265 -0
- package/src/config-environments.ts +28 -1
- package/src/config.ts +8 -1
- package/src/gate-decision-bundle.test.ts +1 -0
- package/src/gate-decision-identity.test.ts +9 -0
- package/src/gate-decision.ts +26 -3
- package/src/graph-ir.ts +27 -0
- package/src/graph-walk.test.ts +44 -0
- package/src/graph-walk.ts +102 -0
- package/src/import/parser.ts +20 -0
- package/src/lexicon.ts +74 -6
- package/src/lifecycle/gate-ledger.ts +8 -0
- package/src/lifecycle/git.ts +2 -2
- package/src/lifecycle/ledger-read.test.ts +107 -0
- package/src/lifecycle/ledger-read.ts +195 -0
- package/src/lifecycle/release-workstation.ts +24 -0
- package/src/lifecycle/run-ledger.ts +5 -2
- package/src/lint/cross-root.test.ts +122 -0
- package/src/lint/discover.ts +3 -1
- package/src/lint/post-synth.ts +212 -2
- package/src/lint/rules/op/index.ts +2 -0
- package/src/lint/rules/op/ops019-workspace-op-target.ts +89 -0
- package/src/op/activities/activity-contracts.ts +5 -2
- package/src/op/activities/index.ts +2 -2
- package/src/op/activities/review-agent.test.ts +221 -4
- package/src/op/activities/review-agent.ts +265 -22
- package/src/op/activities/work-evidence.ts +9 -1
- package/src/op/builders.ts +9 -5
- package/src/op/composites/apply-op.test.ts +108 -0
- package/src/op/composites/apply-op.ts +37 -2
- package/src/op/gate-approval.test.ts +6 -1
- package/src/op/gate-approval.ts +12 -6
- package/src/op/gate-quorum-at-base.test.ts +211 -0
- package/src/op/gate-quorum.ts +156 -8
- package/src/op/gate-review.ts +14 -0
- package/src/op/gate.ts +67 -8
- package/src/op/index.ts +3 -2
- package/src/op/local-executor.ts +69 -9
- package/src/op/op-waves-depends-on.test.ts +81 -0
- package/src/op/op-waves-hook-targets.test.ts +4 -2
- package/src/op/op-waves-quorum-product.test.ts +158 -0
- package/src/op/op-waves-run.ts +288 -14
- package/src/op/op-waves.ts +143 -14
- package/src/op/runtime.ts +8 -0
- package/src/op/schedule-overdue.ts +33 -0
- package/src/op/stage-hooks.ts +13 -5
- package/src/op/workspace-run-env.ts +29 -0
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +47 -0
- package/src/pr-forge.ts +68 -0
- package/src/pr-post-open.e2e.test.ts +36 -0
- package/src/pr-post.test.ts +102 -3
- package/src/pr-post.ts +89 -1
- package/src/read-filter.test.ts +54 -0
- package/src/read-filter.ts +193 -0
- package/src/run-event.ts +1 -1
- package/src/stage-hooks-bundle.test.ts +1 -0
- package/src/terraform/carve.ts +52 -15
- package/src/terraform/cuts.test.ts +173 -0
- package/src/terraform/cuts.ts +188 -0
- package/src/terraform/folds.ts +44 -0
- package/src/terraform/graph.ts +9 -0
- package/src/terraform/radius.test.ts +90 -0
- package/src/terraform/reach.test.ts +146 -0
- package/src/terraform/reach.ts +247 -0
- package/src/terraform/score.ts +27 -29
- package/src/terraform/state.ts +3 -1
- package/src/terraform/types.ts +8 -0
- package/src/wave-blocked.ts +28 -3
- package/src/wave-loop-golden.test.ts +4 -3
- package/src/workspace/behold-kinds.test.ts +1 -1
- package/src/workspace/check-contract.test.ts +7 -6
- package/src/workspace/check.schema.json +11 -2
- package/src/workspace/checks/ci-branch.test.ts +128 -0
- package/src/workspace/checks/ci-branch.ts +141 -0
- package/src/workspace/checks/links.ts +12 -1
- package/src/workspace/checks/org.ts +169 -0
- package/src/workspace/checks.ts +64 -7
- package/src/workspace/ci-branches.ts +31 -0
- package/src/workspace/ci-green-branches.test.ts +153 -0
- package/src/workspace/ci-green-cli.ts +7 -7
- package/src/workspace/ci-green-workflow.test.ts +4 -3
- package/src/workspace/ci-green-workflow.ts +13 -9
- package/src/workspace/ci-green.ts +49 -12
- package/src/workspace/ci-run.test.ts +72 -1
- package/src/workspace/ci-run.ts +68 -8
- package/src/workspace/ci-serve.test.ts +95 -0
- package/src/workspace/ci-serve.ts +106 -65
- package/src/workspace/component-environments.test.ts +25 -2
- package/src/workspace/compose-graph.ts +9 -2
- package/src/workspace/composites.schema.json +22 -0
- package/src/workspace/composites.test.ts +39 -8
- package/src/workspace/composites.ts +6 -2
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
- package/src/workspace/cross-root-lint.test.ts +90 -0
- package/src/workspace/cross-root-lint.ts +126 -0
- package/src/workspace/dbt-kind.test.ts +3 -2
- package/src/workspace/dbt-models-reader.ts +362 -0
- package/src/workspace/dbt-models.test.ts +248 -0
- package/src/workspace/dbt-project-common.ts +113 -0
- package/src/workspace/dbt-project-reader.ts +29 -112
- package/src/workspace/decision-rank.ts +40 -0
- package/src/workspace/declaration.schema.json +133 -24
- package/src/workspace/declaration.ts +84 -15
- package/src/workspace/environments.ts +40 -4
- package/src/workspace/flyway-dir.ts +113 -0
- package/src/workspace/flyway-layout.ts +337 -0
- package/src/workspace/graph-cli.ts +25 -10
- package/src/workspace/graph-contract.test.ts +4 -2
- package/src/workspace/graph.schema.json +34 -0
- package/src/workspace/identity.test.ts +14 -2
- package/src/workspace/intent.ts +6 -14
- package/src/workspace/kind-link-readers.ts +68 -3
- package/src/workspace/kinds.test.ts +12 -7
- package/src/workspace/kinds.ts +132 -7
- package/src/workspace/ledger.schema.json +158 -0
- package/src/workspace/lexicon-outputs.ts +132 -13
- package/src/workspace/lineage-check.ts +30 -7
- package/src/workspace/links.ts +24 -1
- package/src/workspace/ls-contract.test.ts +1 -1
- package/src/workspace/ls.schema.json +7 -0
- package/src/workspace/ls.ts +10 -1
- package/src/workspace/member-commands.ts +60 -2
- package/src/workspace/member-file-schema.test.ts +124 -0
- package/src/workspace/migrate-links.test.ts +99 -0
- package/src/workspace/migrate-links.ts +108 -0
- package/src/workspace/migration-dirs.test.ts +272 -0
- package/src/workspace/migration-dirs.ts +267 -0
- package/src/workspace/org-scope.test.ts +238 -0
- package/src/workspace/reason-codes.test.ts +6 -1
- package/src/workspace/reason-codes.ts +11 -0
- package/src/workspace/records-cli.ts +58 -5
- package/src/workspace/records.schema.json +5 -0
- package/src/workspace/release-candidates.ts +7 -2
- package/src/workspace/release-train.ts +157 -0
- package/src/workspace/release-workflow.ts +4 -3
- package/src/workspace/repo-ref.ts +113 -0
- package/src/workspace/run-event.schema.json +2 -2
- package/src/workspace/runs-cli.ts +48 -4
- package/src/workspace/runs.schema.json +7 -0
- package/src/workspace/status-contract.test.ts +46 -1
- package/src/workspace/status-decisions.test.ts +100 -0
- package/src/workspace/status-decisions.ts +151 -0
- package/src/workspace/status-gates.ts +9 -0
- package/src/workspace/status.schema.json +6 -0
- package/src/workspace/status.ts +17 -3
- package/src/workspace/where-filters.test.ts +39 -0
- package/src/workspace/work-cli.ts +5 -3
- package/src/workspace/work-evidence-ops.test.ts +176 -0
- package/src/workspace/work-evidence-ops.ts +189 -0
- package/src/workspace/work-evidence.schema.json +49 -2
- package/src/workspace/work-evidence.ts +22 -3
- package/src/workspace/workspace-gates.ts +200 -0
- package/src/workspace/workspace-kinds.schema.json +58 -3
- package/src/workspace/workspace-ledger.ts +130 -0
- package/src/workspace/workspace-run.test.ts +322 -0
- package/src/workspace/workspace-run.ts +461 -0
|
@@ -504,7 +504,7 @@ describe("gate approval — a permit counts only under the context it was given
|
|
|
504
504
|
describe("gate approval — the request a policy is asked (#2508)", () => {
|
|
505
505
|
test("carries the approver, the gate and the context, with the plan digest", () => {
|
|
506
506
|
expect(gatePolicyRequest({
|
|
507
|
-
op: "release", gate: "ship", resolvedBy: "release-bot", approver: { kind: "agent", roles: ["deployer"]
|
|
507
|
+
op: "release", gate: "ship", resolvedBy: "release-bot", approver: { kind: "agent" }, roles: ["deployer"],
|
|
508
508
|
planDigest: PLAN_A, context: { risk: "low" },
|
|
509
509
|
})).toEqual({
|
|
510
510
|
principal: { kind: "agent", name: "release-bot", roles: ["deployer"] },
|
|
@@ -513,6 +513,11 @@ describe("gate approval — the request a policy is asked (#2508)", () => {
|
|
|
513
513
|
context: { risk: "low", planDigest: PLAN_A },
|
|
514
514
|
});
|
|
515
515
|
});
|
|
516
|
+
|
|
517
|
+
test("never carries the roles the approver claims (#4324)", () => {
|
|
518
|
+
const request = gatePolicyRequest({ op: "release", gate: "ship", resolvedBy: "github:sam", approver: { kind: "human", roles: ["maintainer"] } });
|
|
519
|
+
expect(request.principal.roles).toEqual([]);
|
|
520
|
+
});
|
|
516
521
|
});
|
|
517
522
|
|
|
518
523
|
describe("gate approval — the approval block is checked where it is written (#2508)", () => {
|
package/src/op/gate-approval.ts
CHANGED
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
* permit counts only while the run resolves that same context (#3774).
|
|
34
34
|
*
|
|
35
35
|
* A resolution is a local fact, not a signed one (`../lifecycle/gate-ledger.ts`).
|
|
36
|
-
* Roles on it are what the approver claimed, and
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* Roles on it are what the approver claimed, and nothing trusts them: since
|
|
37
|
+
* #4290 the quorum counts a role only when `.chant/trust.json` at base grants
|
|
38
|
+
* it and a seal proves the approver (`./gate-quorum.ts`), and since #4324 a
|
|
39
|
+
* policy's request carries those same roles, never the claimed ones.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { createHash } from "node:crypto";
|
|
@@ -282,17 +282,23 @@ export async function loadGatePolicyEvaluator(lexicon: string): Promise<GatePoli
|
|
|
282
282
|
return mod as GatePolicyEvaluator;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* The request a policy is asked about one approval. `roles` are the ones
|
|
287
|
+
* `.chant/trust.json` at base gives a proven approver (#4324), as a quorum
|
|
288
|
+
* judge reads them (`trustQuorumJudge` in `./gate-quorum.ts`). The roles the
|
|
289
|
+
* approver claims on `approver` are never read. Absent, the principal has none.
|
|
290
|
+
*/
|
|
286
291
|
export function gatePolicyRequest(input: {
|
|
287
292
|
op: string;
|
|
288
293
|
gate: string;
|
|
289
294
|
resolvedBy: string;
|
|
290
295
|
approver: GateApprover;
|
|
296
|
+
roles?: readonly string[];
|
|
291
297
|
planDigest?: string;
|
|
292
298
|
context?: Record<string, unknown>;
|
|
293
299
|
}): GatePolicyRequest {
|
|
294
300
|
return {
|
|
295
|
-
principal: { kind: input.approver.kind, name: input.resolvedBy, roles: input.
|
|
301
|
+
principal: { kind: input.approver.kind, name: input.resolvedBy, roles: [...(input.roles ?? [])] },
|
|
296
302
|
action: "PassGate",
|
|
297
303
|
resource: { op: input.op, gate: input.gate },
|
|
298
304
|
context: {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4324: an Op gate's quorum is `identity.gates.<gate>.quorum` in the
|
|
3
|
+
* workspace declaration at base, so a change can't lower the quorum its own
|
|
4
|
+
* run needs, and a gate policy's `principal.roles` come from
|
|
5
|
+
* `.chant/trust.json` at base for a proven approver, never from `chant
|
|
6
|
+
* approve --role`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
|
|
10
|
+
import type { GateStep, OpConfig } from "./types";
|
|
11
|
+
import type { ActivityFn } from "./activity-registry";
|
|
12
|
+
import { resolveGateApproval, runOpLocally } from "./local-executor";
|
|
13
|
+
import { gitGateLedgerPort, memoryGateLedgerPort, type GateLedgerPort } from "./gate";
|
|
14
|
+
import type { GatePolicyEvaluator, GatePolicyRef, GatePolicyRequest } from "./gate-approval";
|
|
15
|
+
import { appendPendingGate, type GateResolutionInput } from "../lifecycle/gate-ledger";
|
|
16
|
+
import { resetGateOrigin } from "../lifecycle/gate-origin";
|
|
17
|
+
import { sealGateApproval } from "../workspace/trust/seal";
|
|
18
|
+
import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
|
|
19
|
+
import { recordGateApproval } from "../cli/handlers/approve";
|
|
20
|
+
|
|
21
|
+
const repos: TestRepo[] = [];
|
|
22
|
+
const home = process.cwd();
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.chdir(home);
|
|
25
|
+
resetGateOrigin();
|
|
26
|
+
while (repos.length) repos.pop()!.cleanup();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let keys: TestRepo;
|
|
30
|
+
let alice: Key;
|
|
31
|
+
let bob: Key;
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
if (!hasSshKeygen) return;
|
|
34
|
+
keys = new TestRepo("4324-keys");
|
|
35
|
+
alice = keys.key("alice");
|
|
36
|
+
bob = keys.key("bob");
|
|
37
|
+
});
|
|
38
|
+
afterAll(() => keys?.cleanup());
|
|
39
|
+
|
|
40
|
+
/** A repository whose `main` is the base, with a git identity the ledger writes can use. */
|
|
41
|
+
function repo(label: string, files: Record<string, string>): TestRepo {
|
|
42
|
+
const r = new TestRepo(label);
|
|
43
|
+
repos.push(r);
|
|
44
|
+
r.git(["config", "user.email", "t@example.test"]);
|
|
45
|
+
r.git(["config", "user.name", "t"]);
|
|
46
|
+
r.git(["config", "commit.gpgsign", "false"]);
|
|
47
|
+
for (const [path, text] of Object.entries(files)) r.write(path, text);
|
|
48
|
+
r.commit("base");
|
|
49
|
+
return r;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const declaration = (gates: Record<string, unknown>): string =>
|
|
53
|
+
JSON.stringify({ name: "acme", schema: 1, members: [], identity: { gates } });
|
|
54
|
+
|
|
55
|
+
describe("resolveGateApproval reads the quorum at base (#4324)", () => {
|
|
56
|
+
const step = (approval?: GateStep["approval"]): GateStep => ({ kind: "gate", gate: "ship", ...(approval ? { approval } : {}) }) as GateStep;
|
|
57
|
+
|
|
58
|
+
test("the base quorum replaces the Op's own", () => {
|
|
59
|
+
expect(resolveGateApproval(step({ quorum: { count: 1 } }), new Map(), undefined, { count: 2 })).toEqual({ quorum: { count: 2 }, mode: "log-only" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a gate whose Op dropped its approval block still takes the base quorum", () => {
|
|
63
|
+
expect(resolveGateApproval(step(), new Map(), undefined, { count: 2 })).toEqual({ quorum: { count: 2 }, mode: "log-only" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a base list is matched against the run's environment", () => {
|
|
67
|
+
const atBase = [{ targets: ["prod"], count: 2 }, { count: 1 }];
|
|
68
|
+
expect(resolveGateApproval(step({ quorum: { count: 1 } }), new Map(), "prod", atBase)?.quorum).toEqual(atBase);
|
|
69
|
+
expect(resolveGateApproval(step({ quorum: { count: 1 } }), new Map(), "dev", atBase)?.quorum).toEqual([{ count: 1 }]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("with no quorum at base, the Op's own applies", () => {
|
|
73
|
+
expect(resolveGateApproval(step({ quorum: { count: 3 } }), new Map(), undefined, null)).toEqual({ quorum: { count: 3 }, mode: "log-only" });
|
|
74
|
+
expect(resolveGateApproval(step(), new Map(), undefined, null)).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe.skipIf(!hasSshKeygen)("an Op gate's quorum at base (#4324)", () => {
|
|
79
|
+
const NOW = "2026-10-23T12:00:00.000Z";
|
|
80
|
+
const config = (count: number): OpConfig => ({
|
|
81
|
+
name: "release",
|
|
82
|
+
overview: "",
|
|
83
|
+
phases: [
|
|
84
|
+
{ name: "Gate", steps: [{ kind: "gate", gate: "ship", approval: { quorum: { count } } }] },
|
|
85
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "ship", args: {} }] },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const activities = new Map<string, ActivityFn>([["ship", async () => ({})]]);
|
|
89
|
+
|
|
90
|
+
test("a change that lowers its own quorum still needs the quorum at base", async () => {
|
|
91
|
+
const r = repo("4324-quorum", {
|
|
92
|
+
"chant.workspace.json": declaration({ ship: { quorum: { count: 2 } } }),
|
|
93
|
+
".chant/allowed_signers": `github:alice ${alice.pub}\ngithub:bob ${bob.pub}\n`,
|
|
94
|
+
});
|
|
95
|
+
// The pull request lowers both the declaration's quorum and the Op's.
|
|
96
|
+
r.git(["checkout", "-q", "-b", "lower-quorum"]);
|
|
97
|
+
r.write("chant.workspace.json", declaration({ ship: { quorum: { count: 1 } } }));
|
|
98
|
+
r.commit("lower the quorum");
|
|
99
|
+
|
|
100
|
+
const memory = memoryGateLedgerPort();
|
|
101
|
+
const git = gitGateLedgerPort({ cwd: r.dir });
|
|
102
|
+
const gates: GateLedgerPort = { ...memory, approvalRule: git.approvalRule, quorumJudge: git.quorumJudge };
|
|
103
|
+
const approve = async (by: string, key: Key, timestamp: string) => {
|
|
104
|
+
const line = { op: "release", gate: "ship", resolvedBy: by, timestamp, approver: { kind: "human" as const } };
|
|
105
|
+
const input: GateResolutionInput = { ...line, seal: sealGateApproval(key.file, line) };
|
|
106
|
+
await memory.appendResolution!(input);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const first = await runOpLocally(config(1), activities, {}, undefined, { gates, now: NOW });
|
|
110
|
+
expect(first.status).toBe("gated");
|
|
111
|
+
expect(first.gate?.approval?.quorum).toEqual({ count: 2 });
|
|
112
|
+
expect(memory.appended[0]?.approval?.quorum).toEqual({ count: 2 });
|
|
113
|
+
|
|
114
|
+
await approve("github:alice", alice, "2026-10-23T12:01:00.000Z");
|
|
115
|
+
const second = await runOpLocally(config(1), activities, {}, undefined, { gates, now: "2026-10-23T12:02:00.000Z" });
|
|
116
|
+
expect(second.status).toBe("gated");
|
|
117
|
+
|
|
118
|
+
await approve("github:bob", bob, "2026-10-23T12:03:00.000Z");
|
|
119
|
+
const third = await runOpLocally(config(1), activities, {}, undefined, { gates, now: "2026-10-23T12:04:00.000Z" });
|
|
120
|
+
expect(third.status).toBe("ok");
|
|
121
|
+
expect(third.records.find((rec) => rec.fn === "gate:ship")?.approval).toMatchObject({ via: "quorum", approvers: ["github:alice", "github:bob"] });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a gate the declaration names with no quorum keeps the Op's", async () => {
|
|
125
|
+
const r = repo("4324-no-quorum", {
|
|
126
|
+
"chant.workspace.json": declaration({ ship: {} }),
|
|
127
|
+
".chant/allowed_signers": `github:alice ${alice.pub}\n`,
|
|
128
|
+
});
|
|
129
|
+
const git = gitGateLedgerPort({ cwd: r.dir });
|
|
130
|
+
const gates: GateLedgerPort = { ...memoryGateLedgerPort(), approvalRule: git.approvalRule };
|
|
131
|
+
const run = await runOpLocally(config(3), activities, {}, undefined, { gates, now: NOW });
|
|
132
|
+
expect(run.gate?.approval?.quorum).toEqual({ count: 3 });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe.skipIf(!hasSshKeygen)("a gate policy's roles come from trust.json at base (#4324)", () => {
|
|
137
|
+
const POLICY: GatePolicyRef = { kind: "gate-policy", lexicon: "cedar", name: "ship", version: "sha256:v1", text: "permit when maintainer" };
|
|
138
|
+
/** Permits only a principal with the maintainer role, and remembers what it was asked. */
|
|
139
|
+
function maintainersOnly(): GatePolicyEvaluator & { asked: GatePolicyRequest[] } {
|
|
140
|
+
const asked: GatePolicyRequest[] = [];
|
|
141
|
+
return {
|
|
142
|
+
asked,
|
|
143
|
+
evaluateGatePolicy: (_policy, request) => {
|
|
144
|
+
asked.push(request);
|
|
145
|
+
return request.principal.roles.includes("maintainer")
|
|
146
|
+
? { decision: "allow", determining: ["maintainers"], errors: [] }
|
|
147
|
+
: { decision: "deny", determining: [], errors: [] };
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function gated(): Promise<TestRepo> {
|
|
153
|
+
const r = repo("4324-policy", {
|
|
154
|
+
"README.md": "x\n",
|
|
155
|
+
".chant/allowed_signers": `github:alice ${alice.pub}\n`,
|
|
156
|
+
".chant/trust.json": JSON.stringify({ schema: 1, roles: { maintainer: ["github:alice"] } }),
|
|
157
|
+
});
|
|
158
|
+
await appendPendingGate(
|
|
159
|
+
{
|
|
160
|
+
op: "release",
|
|
161
|
+
gate: "ship",
|
|
162
|
+
timestamp: "2026-10-23T12:00:00.000Z",
|
|
163
|
+
origin: "cli",
|
|
164
|
+
expiresAt: "2999-01-01T00:00:00.000Z",
|
|
165
|
+
approval: { policy: POLICY, mode: "enforce" },
|
|
166
|
+
},
|
|
167
|
+
{ cwd: r.dir },
|
|
168
|
+
);
|
|
169
|
+
process.chdir(r.dir);
|
|
170
|
+
return r;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test("a claimed --role the trust file does not grant fails a policy that requires it", async () => {
|
|
174
|
+
const r = await gated();
|
|
175
|
+
const evaluator = maintainersOnly();
|
|
176
|
+
const outcome = await recordGateApproval("release", "ship", { origin: "cli", actor: "github:sam", roles: ["maintainer"], evaluator, cwd: r.dir });
|
|
177
|
+
expect(outcome.ok).toBe(true);
|
|
178
|
+
expect(evaluator.asked[0]?.principal).toEqual({ kind: "human", name: "github:sam", roles: [] });
|
|
179
|
+
if (!outcome.ok) return;
|
|
180
|
+
expect(outcome.record.policyDecision?.decision).toBe("deny");
|
|
181
|
+
// The claim is still recorded, as data nothing trusts.
|
|
182
|
+
expect(outcome.record.approver).toEqual({ kind: "human", roles: ["maintainer"] });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("an unsealed approval by a granted principal gets no roles either", async () => {
|
|
186
|
+
const r = await gated();
|
|
187
|
+
const evaluator = maintainersOnly();
|
|
188
|
+
const outcome = await recordGateApproval("release", "ship", { origin: "cli", actor: "github:alice", roles: ["maintainer"], evaluator, cwd: r.dir });
|
|
189
|
+
expect(evaluator.asked[0]?.principal.roles).toEqual([]);
|
|
190
|
+
expect(outcome.ok && outcome.record.policyDecision?.decision).toBe("deny");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a sealed approval gets the roles trust.json at base grants, and only those", async () => {
|
|
194
|
+
const r = await gated();
|
|
195
|
+
const evaluator = maintainersOnly();
|
|
196
|
+
const outcome = await recordGateApproval("release", "ship", { origin: "cli", actor: "github:alice", roles: ["admin"], sign: alice.file, evaluator, cwd: r.dir });
|
|
197
|
+
expect(evaluator.asked[0]?.principal).toEqual({ kind: "human", name: "github:alice", roles: ["maintainer"] });
|
|
198
|
+
expect(outcome.ok && outcome.record.policyDecision?.decision).toBe("allow");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a grant the change adds to trust.json does not count until it is at base", async () => {
|
|
202
|
+
const r = await gated();
|
|
203
|
+
r.git(["checkout", "-q", "-b", "grant-sam"]);
|
|
204
|
+
r.write(".chant/allowed_signers", `github:alice ${alice.pub}\ngithub:bob ${bob.pub}\n`);
|
|
205
|
+
r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { maintainer: ["github:alice", "github:bob"] } }));
|
|
206
|
+
r.commit("grant bob");
|
|
207
|
+
const evaluator = maintainersOnly();
|
|
208
|
+
await recordGateApproval("release", "ship", { origin: "cli", actor: "github:bob", sign: bob.file, evaluator, cwd: r.dir });
|
|
209
|
+
expect(evaluator.asked[0]?.principal.roles).toEqual([]);
|
|
210
|
+
});
|
|
211
|
+
});
|
package/src/op/gate-quorum.ts
CHANGED
|
@@ -18,10 +18,16 @@
|
|
|
18
18
|
* approver counts toward `roles`, and as someone who is not the author, only
|
|
19
19
|
* when the approval proves who they are: a seal that verifies against the
|
|
20
20
|
* signers file at base, or under `pr-review` the forge review of the merged
|
|
21
|
-
* pull request
|
|
21
|
+
* pull request, or a review a product recorded on a ledger the spec at base
|
|
22
|
+
* names (#4333). The judge of a gate decides that ({@link GateQuorumJudge});
|
|
22
23
|
* with none, no approval proves anything, so a rule naming roles or
|
|
23
24
|
* `notAuthor` is never met.
|
|
24
25
|
*
|
|
26
|
+
* `notAuthor` reads who was involved in the change (#4333): the pull
|
|
27
|
+
* request's author, each login a `Requested-by:` trailer on its commits
|
|
28
|
+
* names, and the accounts an agent acts as. With `excludeInvolved`, an
|
|
29
|
+
* approval by any of them does not count at all.
|
|
30
|
+
*
|
|
25
31
|
* This module is pure: no git, no file reads, no package, so
|
|
26
32
|
* `@intentius/chant/gate-decision` bundles it. The judge is built by the
|
|
27
33
|
* caller from the trust policy it read at base ({@link trustQuorumJudge}).
|
|
@@ -51,8 +57,14 @@ export interface QuorumJudgement {
|
|
|
51
57
|
proven: boolean;
|
|
52
58
|
/** The roles `.chant/trust.json` at base gives the approver. Empty unless `proven`. */
|
|
53
59
|
roles: string[];
|
|
54
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Whether the approver was involved in the change: its author, or anyone
|
|
62
|
+
* else {@link TrustQuorumInput.involved} names. Undefined when nobody knows
|
|
63
|
+
* who the author is and the approver is not named as involved otherwise.
|
|
64
|
+
*/
|
|
55
65
|
author?: boolean;
|
|
66
|
+
/** Why the approval does not count at all (#4333): the approver was involved and the judge excludes everyone involved. */
|
|
67
|
+
excluded?: string;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
/** Judges one approval for the rules. */
|
|
@@ -174,7 +186,7 @@ export function quorumRuleProgress(rule: GateQuorumRule, approvers: readonly Quo
|
|
|
174
186
|
const seen = new Set<string>();
|
|
175
187
|
const counted: QuorumApprover[] = [];
|
|
176
188
|
for (const a of [...approvers].sort((x, y) => Date.parse(x.at) - Date.parse(y.at))) {
|
|
177
|
-
if (seen.has(norm(a.who))) continue;
|
|
189
|
+
if (seen.has(norm(a.who)) || a.judged.excluded !== undefined) continue;
|
|
178
190
|
if (rule.roles?.length && !(a.judged.proven && a.judged.roles.some((r) => rule.roles!.includes(r)))) continue;
|
|
179
191
|
seen.add(norm(a.who));
|
|
180
192
|
counted.push(a);
|
|
@@ -216,6 +228,118 @@ export function quorumWaiting(progress: readonly GateQuorumRuleProgress[]): stri
|
|
|
216
228
|
/** Whether a rule needs a proven approver: it names roles, or `notAuthor`. */
|
|
217
229
|
export const ruleNeedsProof = (rule: GateQuorumRule): boolean => (rule.roles?.length ?? 0) > 0 || rule.notAuthor === true;
|
|
218
230
|
|
|
231
|
+
/** Someone involved in a change (#4333), and why: `github:bob asked for abc1234`. */
|
|
232
|
+
export interface QuorumInvolvement {
|
|
233
|
+
who: string;
|
|
234
|
+
why: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A login as a forge allows it, and a GitHub App's `[bot]` suffix. */
|
|
238
|
+
const LOGIN = /^[A-Za-z0-9][A-Za-z0-9._-]*(\[bot\])?$/;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The logins `Requested-by:` trailers on a commit message name, each once.
|
|
242
|
+
* Any line of the message counts, since a squash merge keeps the trailers of
|
|
243
|
+
* the commits it squashed in its body.
|
|
244
|
+
*/
|
|
245
|
+
export function requestedByTrailers(message: string): string[] {
|
|
246
|
+
const out: string[] = [];
|
|
247
|
+
for (const line of message.replace(/\r\n/g, "\n").split("\n")) {
|
|
248
|
+
const m = /^requested-by:\s*(\S+)\s*$/i.exec(line.trim());
|
|
249
|
+
if (m && LOGIN.test(m[1]!) && !out.some((o) => samePrincipal(o, m[1]!))) out.push(m[1]!);
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The names a principal goes by: itself, and for a forge principal
|
|
256
|
+
* (`github:alice`, `gitlab@gitlab.acme.com:bob`) the login after the colon,
|
|
257
|
+
* so a trailer's bare login and an approval's principal name one person.
|
|
258
|
+
*/
|
|
259
|
+
export function principalNames(who: string): string[] {
|
|
260
|
+
const m = /^(?:github|gitlab|forgejo|gitea|codeberg)(?:@[^:\s]+)?:(.+)$/i.exec(who.trim());
|
|
261
|
+
return m ? [who, m[1]!] : [who];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Why `who` was involved in the change, or undefined when `involved` does not name them. */
|
|
265
|
+
export function involvedWhy(involved: readonly QuorumInvolvement[], who: string): string | undefined {
|
|
266
|
+
const names = principalNames(who);
|
|
267
|
+
return involved.find((p) => principalNames(p.who).some((a) => names.some((b) => samePrincipal(a, b))))?.why;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Everyone involved in a change (#4333): the pull request's `author`, each
|
|
272
|
+
* login a `Requested-by:` trailer on `commits` names, and the `agents`'
|
|
273
|
+
* accounts. `principal` turns a trailer's login into the principal the
|
|
274
|
+
* forge's approvals name (`github:<login>`); absent, the login stands.
|
|
275
|
+
*/
|
|
276
|
+
export function changeInvolvement(o: {
|
|
277
|
+
author?: string;
|
|
278
|
+
commits?: readonly { sha: string; message: string }[];
|
|
279
|
+
agents?: readonly string[];
|
|
280
|
+
principal?: (login: string) => string;
|
|
281
|
+
}): QuorumInvolvement[] {
|
|
282
|
+
const out: QuorumInvolvement[] = [];
|
|
283
|
+
const add = (who: string, why: string): void => {
|
|
284
|
+
if (who.trim() && !out.some((p) => samePrincipal(p.who, who))) out.push({ who, why });
|
|
285
|
+
};
|
|
286
|
+
if (o.author) add(o.author, `${o.author} opened the change`);
|
|
287
|
+
for (const c of o.commits ?? []) {
|
|
288
|
+
for (const login of requestedByTrailers(c.message)) add(o.principal ? o.principal(login) : login, `${login} asked for ${c.sha.slice(0, 7)}`);
|
|
289
|
+
}
|
|
290
|
+
for (const a of o.agents ?? []) add(a, `${a} is an account an agent acts as`);
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A review a product recorded on its own ledger (#4333), as terragucci does
|
|
296
|
+
* under `pr-review`: a resolution with `via: "pr-review"`, the reviewed
|
|
297
|
+
* `head`, and the `reviewers` who approved it, by forge login. One line
|
|
298
|
+
* names every reviewer, so `resolvedBy` is not one person.
|
|
299
|
+
*/
|
|
300
|
+
export interface RecordedReview {
|
|
301
|
+
pr?: number;
|
|
302
|
+
head: string;
|
|
303
|
+
reviewers: string[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The review a ledger line records, when it is a product's review line; null otherwise. */
|
|
307
|
+
export function recordedReviewOf(r: GateResolutionRecord): RecordedReview | null {
|
|
308
|
+
const x = r as unknown as { via?: unknown; head?: unknown; reviewers?: unknown; pr?: unknown };
|
|
309
|
+
if (x.via !== "pr-review" || typeof x.head !== "string" || x.head === "") return null;
|
|
310
|
+
if (!Array.isArray(x.reviewers) || x.reviewers.length === 0 || x.reviewers.some((v) => typeof v !== "string" || v === "")) return null;
|
|
311
|
+
return { ...(typeof x.pr === "number" ? { pr: x.pr } : {}), head: x.head, reviewers: [...(x.reviewers as string[])] };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The approvals a product's review lines stand for (#4333): each line
|
|
316
|
+
* {@link recordedReviewOf} reads becomes one approval per reviewer, named by
|
|
317
|
+
* `principal(login)` (the login itself when absent), a human's, with the
|
|
318
|
+
* review on it. A seal on the line is dropped: it signs the line, not any
|
|
319
|
+
* one reviewer. Every other line is kept as it is. `reviews` lists the
|
|
320
|
+
* reviews read, for {@link TrustQuorumInput.recorded}.
|
|
321
|
+
*/
|
|
322
|
+
export function recordedReviewApprovals(
|
|
323
|
+
records: readonly GateResolutionRecord[],
|
|
324
|
+
principal: (login: string) => string = (login) => login,
|
|
325
|
+
): { records: GateResolutionRecord[]; reviews: { head: string; approvers: string[] }[] } {
|
|
326
|
+
const out: GateResolutionRecord[] = [];
|
|
327
|
+
const reviews: { head: string; approvers: string[] }[] = [];
|
|
328
|
+
for (const r of records) {
|
|
329
|
+
const review = recordedReviewOf(r);
|
|
330
|
+
if (!review) {
|
|
331
|
+
out.push(r);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const approvers = review.reviewers.map(principal);
|
|
335
|
+
reviews.push({ head: review.head, approvers });
|
|
336
|
+
const { seal: _seal, ...rest } = r;
|
|
337
|
+
const base = Object.fromEntries(Object.entries(rest).filter(([k]) => !["via", "reviewers", "head", "pr", "excluded"].includes(k))) as unknown as GateResolutionRecord;
|
|
338
|
+
for (const who of approvers) out.push({ ...base, resolvedBy: who, approver: { kind: "human" }, review: { pr: review.pr ?? 0, head: review.head, approvers } });
|
|
339
|
+
}
|
|
340
|
+
return { records: out, reviews };
|
|
341
|
+
}
|
|
342
|
+
|
|
219
343
|
/** What {@link trustQuorumJudge} judges by. */
|
|
220
344
|
export interface TrustQuorumInput {
|
|
221
345
|
/** The signers at base: an approval whose seal verifies for its approver against them is proven. */
|
|
@@ -224,6 +348,22 @@ export interface TrustQuorumInput {
|
|
|
224
348
|
roles: Readonly<Record<string, readonly string[]>>;
|
|
225
349
|
/** The change's authors, as principals. Absent, nobody knows who they are. */
|
|
226
350
|
authors?: readonly string[];
|
|
351
|
+
/**
|
|
352
|
+
* Everyone else involved in the change (#4333), from {@link
|
|
353
|
+
* changeInvolvement}: an approver named here is judged as an author, for
|
|
354
|
+
* `notAuthor`, whether or not `authors` is known.
|
|
355
|
+
*/
|
|
356
|
+
involved?: readonly QuorumInvolvement[];
|
|
357
|
+
/** An approval by an author or anyone `involved` names does not count at all (#4333), toward any rule. */
|
|
358
|
+
excludeInvolved?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Reviews a product recorded on the ledger the spec at base names (#4333),
|
|
361
|
+
* from {@link recordedReviewApprovals}: a resolution that names one's head
|
|
362
|
+
* and one of its approvers as `resolvedBy` is proven, as for `review`. Pass
|
|
363
|
+
* them only under `pr-review`, as the product proves its reviewers only
|
|
364
|
+
* then.
|
|
365
|
+
*/
|
|
366
|
+
recorded?: readonly { head: string; approvers: readonly string[] }[];
|
|
227
367
|
/**
|
|
228
368
|
* Under `pr-review`, the merged pull request's review, read from the forge
|
|
229
369
|
* in this run: a resolution that names this review's head and one of its
|
|
@@ -232,17 +372,25 @@ export interface TrustQuorumInput {
|
|
|
232
372
|
review?: { head: string; approvers: readonly string[] };
|
|
233
373
|
}
|
|
234
374
|
|
|
235
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* The judge a gate's rules are met by: proof from a seal, the live review or
|
|
377
|
+
* a review a product recorded, roles from trust.json at base, and the
|
|
378
|
+
* change's authors and everyone else involved in it left out.
|
|
379
|
+
*/
|
|
236
380
|
export function trustQuorumJudge(input: TrustQuorumInput): GateQuorumJudge {
|
|
381
|
+
const named = (list: readonly string[], who: string) => list.some((a) => samePrincipal(a, who));
|
|
237
382
|
return (r) => {
|
|
238
383
|
const sealed = r.seal !== undefined && r.seal !== null && gateSealRefusal(input.signers, r) === null;
|
|
239
|
-
const reviewed =
|
|
240
|
-
|
|
241
|
-
const proven = sealed || reviewed;
|
|
384
|
+
const reviewed = input.review !== undefined && r.review?.head === input.review.head && named(input.review.approvers, r.resolvedBy);
|
|
385
|
+
const recorded = r.review !== undefined && (input.recorded ?? []).some((x) => x.head === r.review!.head && named(x.approvers, r.resolvedBy));
|
|
386
|
+
const proven = sealed || reviewed || recorded;
|
|
387
|
+
const authored = input.authors !== undefined && input.authors.some((a) => samePrincipal(a, r.resolvedBy));
|
|
388
|
+
const why = authored ? `${r.resolvedBy} wrote the change` : involvedWhy(input.involved ?? [], r.resolvedBy);
|
|
242
389
|
return {
|
|
243
390
|
proven,
|
|
244
391
|
roles: proven ? rolesOf(r.resolvedBy, input.roles) : [],
|
|
245
|
-
...(
|
|
392
|
+
...(why !== undefined ? { author: true } : input.authors !== undefined ? { author: false } : {}),
|
|
393
|
+
...(why !== undefined && input.excludeInvolved ? { excluded: `${why}, so ${r.resolvedBy}'s approval does not count` } : {}),
|
|
246
394
|
};
|
|
247
395
|
};
|
|
248
396
|
}
|
package/src/op/gate-review.ts
CHANGED
|
@@ -300,6 +300,20 @@ export function reviewSourceFromEnv(env: Record<string, string | undefined> = pr
|
|
|
300
300
|
return forgejo ? forgejoReviews(opts) : githubReviews(opts);
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* The principal a forge login on the CI job's forge goes by (#4333):
|
|
305
|
+
* `github:<login>` on github.com, `gitlab@<host>:<login>` on another host,
|
|
306
|
+
* as {@link reviewSourceFromEnv}'s reviews name their approvers. A product
|
|
307
|
+
* records its reviewers and its `Requested-by:` trailers by login, and this
|
|
308
|
+
* reads them as the approvals they stand beside. A value that already has a
|
|
309
|
+
* forge's prefix is kept.
|
|
310
|
+
*/
|
|
311
|
+
export function forgePrincipalFromEnv(env: Record<string, string | undefined> = process.env): (login: string) => string {
|
|
312
|
+
const kind: ForgeKind = env.GITLAB_CI === "true" ? "gitlab" : env.FORGEJO_ACTIONS === "true" || env.GITEA_ACTIONS === "true" ? "forgejo" : "github";
|
|
313
|
+
const host = kind === "gitlab" ? (env.CI_SERVER_HOST ?? hostOf(env.CI_SERVER_URL, "gitlab.com")) : hostOf(env.GITHUB_SERVER_URL, "github.com");
|
|
314
|
+
return (login) => (/^(?:github|gitlab|forgejo|gitea|codeberg)(?:@[^:\s]+)?:/i.test(login.trim()) ? login : forgePrincipalOf(kind, host, login));
|
|
315
|
+
}
|
|
316
|
+
|
|
303
317
|
/**
|
|
304
318
|
* The head of the pull request a CI job runs for, from its environment: the
|
|
305
319
|
* event payload's `pull_request.head.sha` on GitHub and Forgejo (whose
|
package/src/op/gate.ts
CHANGED
|
@@ -53,7 +53,7 @@ import { sortedJsonReplacer } from "../utils";
|
|
|
53
53
|
import { gateContextDigest, type GateApprover, type ResolvedGateApproval } from "./gate-approval";
|
|
54
54
|
import {
|
|
55
55
|
quorumNeed, quorumRuleProgress, quorumRules, ruleNeedsProof, samePrincipal, trustQuorumJudge, UNPROVEN_JUDGE,
|
|
56
|
-
type GateQuorumJudge, type GateQuorumRuleProgress,
|
|
56
|
+
type GateQuorumJudge, type GateQuorumRuleProgress, type GateQuorumSpec,
|
|
57
57
|
} from "./gate-quorum";
|
|
58
58
|
import { pushLifecycleReport, readBlobBySha, readBlobFromPath, readPathSha, requireLifecycleLedger, writeBlobToPath, RefCASConflictError } from "../lifecycle/git";
|
|
59
59
|
import { parseDuration } from "./duration";
|
|
@@ -128,7 +128,12 @@ export interface GateLedgerPort {
|
|
|
128
128
|
|
|
129
129
|
/** A workspace's rule for which approvals of a gate count (#3163): a signed approval, from a class when it names one. */
|
|
130
130
|
export interface GateApprovalRule {
|
|
131
|
-
|
|
131
|
+
/**
|
|
132
|
+
* `quorum` (#4324) is the entry's `identity.gates.<gate>.quorum` at base:
|
|
133
|
+
* the rules an Op gate of that name takes in place of its own. Absent or
|
|
134
|
+
* null when the entry sets none.
|
|
135
|
+
*/
|
|
136
|
+
requirement: { gate: string; class: string | null; quorum?: GateQuorumSpec | null };
|
|
132
137
|
/** Why `approval` doesn't count, or null when it does. */
|
|
133
138
|
refuses(approval: GateResolutionRecord): string | null;
|
|
134
139
|
}
|
|
@@ -318,6 +323,12 @@ export interface GateCheckInput {
|
|
|
318
323
|
* and the merged pull request's review.
|
|
319
324
|
*/
|
|
320
325
|
judge?: GateQuorumJudge;
|
|
326
|
+
/**
|
|
327
|
+
* The port's {@link GateLedgerPort.approvalRule} for this gate, when the
|
|
328
|
+
* caller already read it (#4324): the Op executor reads it first, for the
|
|
329
|
+
* quorum at base. Absent, it is read here; null means none applies.
|
|
330
|
+
*/
|
|
331
|
+
rule?: GateApprovalRule | null;
|
|
321
332
|
/** ISO-8601 "now" — supplied by the caller, so the decision is deterministic under test. */
|
|
322
333
|
now?: string;
|
|
323
334
|
/**
|
|
@@ -371,6 +382,31 @@ export interface GateCheckInput {
|
|
|
371
382
|
* no `empty` resolution counts.
|
|
372
383
|
*/
|
|
373
384
|
verifyEmpty?: (planDigest: string) => Promise<boolean>;
|
|
385
|
+
/**
|
|
386
|
+
* A workspace gate that may stand in for this gate (#2554, #2524 D17,
|
|
387
|
+
* ws-039). Read only on a gate bound to a plan, and only when the gate's
|
|
388
|
+
* own approvals don't pass it. The stand-in decides: it answers an approval
|
|
389
|
+
* only when the member opted in at base and the workspace approval's
|
|
390
|
+
* digest covers this member's plan. Absent, a workspace gate never
|
|
391
|
+
* satisfies this gate.
|
|
392
|
+
*/
|
|
393
|
+
workspace?: WorkspaceGateStandIn;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** What a workspace gate approval that stands in for a member gate names (#2554). */
|
|
397
|
+
export interface WorkspaceGateStood {
|
|
398
|
+
/** The workspace gate's name. */
|
|
399
|
+
gate: string;
|
|
400
|
+
/** The digest of the member plans the approval covers. */
|
|
401
|
+
digest: string;
|
|
402
|
+
approvedBy: string;
|
|
403
|
+
/** When the workspace approval was recorded. */
|
|
404
|
+
timestamp: string;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** See {@link GateCheckInput.workspace}. */
|
|
408
|
+
export interface WorkspaceGateStandIn {
|
|
409
|
+
standIn(q: { op: string; gate: string; planDigest: string; environment?: string }): Promise<WorkspaceGateStood | null>;
|
|
374
410
|
}
|
|
375
411
|
|
|
376
412
|
/**
|
|
@@ -441,9 +477,10 @@ export type GateCheck =
|
|
|
441
477
|
* under one of the gate's aliases counted, on any gate. `"empty"`
|
|
442
478
|
* (#4045) when a release gate passed over an empty change set, on the
|
|
443
479
|
* `empty: true` resolution the promote wrote, after the stored
|
|
444
|
-
* document was read back and found empty.
|
|
480
|
+
* document was read back and found empty. `"workspace"` (#2554) when a
|
|
481
|
+
* workspace gate's approval stood in for it ({@link GateCheckInput.workspace}).
|
|
445
482
|
*/
|
|
446
|
-
via?: "quorum" | "policy" | "alias" | "empty";
|
|
483
|
+
via?: "quorum" | "policy" | "alias" | "empty" | "workspace";
|
|
447
484
|
/** Every approval that counted toward the quorum. Set with `via`. */
|
|
448
485
|
approvals?: GateResolutionRecord[];
|
|
449
486
|
/** The approvals this run carried from an alias and wrote under the gate's name (#3949). Set when `via` is `"alias"`. */
|
|
@@ -774,7 +811,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
774
811
|
// newest one since the standing fact comes back as a mismatch instead.
|
|
775
812
|
const forOtherRelease = (r: GateResolutionRecord) => r.gate === input.gate && r.releaseDigest !== input.releaseDigest;
|
|
776
813
|
// #3163: a gate the workspace's identity.gates names counts only signed approvals from its class.
|
|
777
|
-
const rule = (await port.approvalRule?.(input.gate)) ?? null;
|
|
814
|
+
const rule = input.rule !== undefined ? input.rule : ((await port.approvalRule?.(input.gate)) ?? null);
|
|
778
815
|
const standing = latestPendingGate(pending, input.gate);
|
|
779
816
|
const since = standing?.timestamp ?? EPOCH;
|
|
780
817
|
|
|
@@ -845,9 +882,8 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
845
882
|
? humanQuorumSince(pending, standing)
|
|
846
883
|
: since;
|
|
847
884
|
// #4290: who an approval proves, and their roles at base, for rules that read them.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
: UNPROVEN_JUDGE;
|
|
885
|
+
// #4333: a caller's judge applies to every rule, since it may leave out everyone involved in the change.
|
|
886
|
+
const judge = input.judge ?? (quorumRules(input.approval.quorum).some(ruleNeedsProof) ? ((await port.quorumJudge?.(input.gate)) ?? UNPROVEN_JUDGE) : UNPROVEN_JUDGE);
|
|
851
887
|
const tally = tallyGateApprovals(resolutions, input.gate, since, input.planDigest, input.approval, humansSince, judge);
|
|
852
888
|
if (tally.permit) {
|
|
853
889
|
return { satisfied: true, resolution: tally.permit, via: "policy", approvals: tally.counted };
|
|
@@ -887,6 +923,29 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
887
923
|
mismatched = found.mismatched;
|
|
888
924
|
}
|
|
889
925
|
|
|
926
|
+
// #2554, ws-039: a workspace gate stands in only when the member opted in
|
|
927
|
+
// at base and the workspace approval's digest covers this plan.
|
|
928
|
+
if (input.workspace && input.planDigest !== undefined) {
|
|
929
|
+
const stood = await input.workspace.standIn({ op: input.op, gate: input.gate, planDigest: input.planDigest, ...(env !== undefined ? { environment: env } : {}) });
|
|
930
|
+
if (stood) {
|
|
931
|
+
return {
|
|
932
|
+
satisfied: true,
|
|
933
|
+
via: "workspace",
|
|
934
|
+
resolution: {
|
|
935
|
+
version: 1,
|
|
936
|
+
kind: "resolution",
|
|
937
|
+
op: input.op,
|
|
938
|
+
gate: input.gate,
|
|
939
|
+
resolvedBy: stood.approvedBy,
|
|
940
|
+
timestamp: stood.timestamp,
|
|
941
|
+
planDigest: input.planDigest,
|
|
942
|
+
...(env !== undefined ? { environment: env } : {}),
|
|
943
|
+
workspaceGate: { gate: stood.gate, digest: stood.digest },
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
890
949
|
// #4045: an approval of this gate for another release stands as a mismatch
|
|
891
950
|
// when nothing closer does, so the refusal says an approval exists.
|
|
892
951
|
if (!mismatched && input.planDigest !== undefined) {
|
package/src/op/index.ts
CHANGED
|
@@ -69,7 +69,7 @@ export {
|
|
|
69
69
|
opWaveRecordPath,
|
|
70
70
|
opWaveShare,
|
|
71
71
|
} from "./op-waves";
|
|
72
|
-
export type { OpWave, OpWaveApplyHooksRunner, OpWaveGatePolicy, OpWaveHook, OpWaveHookFailure, OpWaveHookMoment, OpWaveHooks, OpWaveJob, OpWaveRun, OpWavesDigest, OpWavesOverride, OpWavesSpec } from "./op-waves";
|
|
72
|
+
export type { OpWave, OpWaveApplyHooksRunner, OpWaveGatePolicy, OpWaveHook, OpWaveHookFailure, OpWaveHookMoment, OpWaveHooks, OpWaveJob, OpWaveRun, OpWavesApprovals, OpWavesDigest, OpWavesInvolved, OpWavesOverride, OpWavesSpec } from "./op-waves";
|
|
73
73
|
export {
|
|
74
74
|
OP_WAVE_APPLY_OUTCOMES,
|
|
75
75
|
OP_WAVE_MAX_REPLANS,
|
|
@@ -142,9 +142,10 @@ export type {
|
|
|
142
142
|
} from "./gate-approval";
|
|
143
143
|
export {
|
|
144
144
|
quorumRules, quorumRulesFor, quorumNeed, quorumProblems, quorumRuleProgress, quorumRuleLine, quorumWaiting, rolesOf, trustQuorumJudge, UNPROVEN_JUDGE,
|
|
145
|
+
changeInvolvement, involvedWhy, principalNames, recordedReviewApprovals, recordedReviewOf, requestedByTrailers,
|
|
145
146
|
} from "./gate-quorum";
|
|
146
147
|
export type {
|
|
147
|
-
GateQuorumRule, GateQuorumSpec, GateQuorumJudge, GateQuorumRuleProgress, QuorumJudgement, QuorumApprover, TrustQuorumInput,
|
|
148
|
+
GateQuorumRule, GateQuorumSpec, GateQuorumJudge, GateQuorumRuleProgress, QuorumJudgement, QuorumApprover, TrustQuorumInput, QuorumInvolvement, RecordedReview,
|
|
148
149
|
} from "./gate-quorum";
|
|
149
150
|
export {
|
|
150
151
|
gatePlanSummary, gatePlanSummaryOfResult, changeSetOfResult, GATE_PLAN_CONTEXT_KEY, GATE_PLAN_ADDRESS_LIMIT,
|