@intentius/chant 0.124.39 → 0.124.41
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/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/component-endpoint.d.ts +20 -0
- package/dist/cli/component-endpoint.d.ts.map +1 -0
- package/dist/cli/component-endpoint.js +36 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +26 -4
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +74 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +514 -58
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +16 -2
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +17 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +41 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -2
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +35 -3
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +29 -2
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +2 -0
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/deploy-units.js +5 -0
- package/dist/components/fan-out-run.d.ts +28 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +27 -3
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +72 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +96 -5
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +5 -3
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +39 -16
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +6 -1
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +4 -3
- package/dist/lifecycle/publish-npm.d.ts +2 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +31 -8
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/release-ledger.d.ts +31 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +19 -1
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/status.d.ts +5 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/status.js +10 -5
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/live-endpoint.d.ts +18 -0
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +27 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +62 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +190 -11
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +36 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +82 -0
- package/dist/pr-loop.d.ts +90 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +77 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +239 -239
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +24 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +85 -6
- package/dist/workspace/declaration.d.ts +63 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +147 -0
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- 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 +23 -0
- package/dist/workspace/pr-report.schema.json +73 -2
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts +3 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +7 -3
- package/dist/workspace/status.schema.json +1 -0
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/component-endpoint.ts +43 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/fan-out.test.ts +10 -0
- package/src/cli/handlers/fan-out.ts +29 -4
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-endpoint.test.ts +169 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +508 -64
- package/src/cli/handlers/promote.ts +25 -2
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.test.ts +2 -1
- package/src/cli/handlers/run.ts +16 -5
- package/src/cli/main.ts +41 -2
- package/src/cli/parse-args.ts +23 -2
- package/src/cli/registry.ts +12 -2
- package/src/components/auto-release-infra.test.ts +131 -0
- package/src/components/auto-release.test.ts +40 -1
- package/src/components/auto-release.ts +50 -4
- package/src/components/cli-support.ts +4 -0
- package/src/components/deploy-units.ts +5 -0
- package/src/components/fan-out-run.ts +39 -2
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +130 -5
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +5 -2
- package/src/components/promote.test.ts +39 -0
- package/src/components/promote.ts +38 -14
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +6 -1
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/publish-declaration.ts +4 -3
- package/src/lifecycle/publish-npm.ts +33 -8
- package/src/lifecycle/publish-targets.test.ts +36 -3
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/release-ledger.ts +42 -1
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/status.test.ts +19 -0
- package/src/lifecycle/status.ts +14 -6
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/live-endpoint.ts +31 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +233 -11
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +114 -0
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +146 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +133 -8
- package/src/workspace/ci-run.ts +92 -7
- package/src/workspace/declaration.schema.json +147 -0
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +134 -2
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +73 -2
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.schema.json +1 -0
- package/src/workspace/status.ts +11 -4
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
package/dist/op/cron.js
CHANGED
|
@@ -144,3 +144,54 @@ export function cronDueBetween(expr, after, upTo) {
|
|
|
144
144
|
}
|
|
145
145
|
return false;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The minutes `expr` fires at in `(after, upTo]`, read in UTC as GitHub,
|
|
149
|
+
* GitLab and Forgejo read a CI cron, at most `max` of them (#4289). The
|
|
150
|
+
* plan job counts these to tell a schedule that stopped from one that ran
|
|
151
|
+
* late. A 6-field expression drops its seconds field, as {@link cronMatches}
|
|
152
|
+
* does, and an expression that is not 5 or 6 fields fires never.
|
|
153
|
+
*/
|
|
154
|
+
export function cronFiresUtc(expr, after, upTo, max) {
|
|
155
|
+
const fields = expr.trim().split(/\s+/);
|
|
156
|
+
const five = fields.length === 6 ? fields.slice(1) : fields;
|
|
157
|
+
if (five.length !== 5)
|
|
158
|
+
return [];
|
|
159
|
+
const minute = parseField(five[0], 0, 59);
|
|
160
|
+
const hour = parseField(five[1], 0, 23);
|
|
161
|
+
const dom = parseField(five[2], 1, 31);
|
|
162
|
+
const month = parseField(five[3], 1, 12);
|
|
163
|
+
const dow = parseField(five[4], 0, 7);
|
|
164
|
+
const dayMatches = (d) => {
|
|
165
|
+
const weekday = d.getUTCDay();
|
|
166
|
+
const domOk = fieldMatches(dom, d.getUTCDate());
|
|
167
|
+
const dowOk = fieldMatches(dow, weekday) || (weekday === 0 && fieldMatches(dow, 7));
|
|
168
|
+
if (dom.any && dow.any)
|
|
169
|
+
return true;
|
|
170
|
+
if (dom.any)
|
|
171
|
+
return dowOk;
|
|
172
|
+
if (dow.any)
|
|
173
|
+
return domOk;
|
|
174
|
+
return domOk || dowOk;
|
|
175
|
+
};
|
|
176
|
+
const out = [];
|
|
177
|
+
const t = new Date(Math.floor(after.getTime() / 60000) * 60000 + 60000);
|
|
178
|
+
while (t <= upTo && out.length < max) {
|
|
179
|
+
if (!fieldMatches(month, t.getUTCMonth() + 1)) {
|
|
180
|
+
t.setUTCMonth(t.getUTCMonth() + 1, 1);
|
|
181
|
+
t.setUTCHours(0, 0, 0, 0);
|
|
182
|
+
}
|
|
183
|
+
else if (!dayMatches(t)) {
|
|
184
|
+
t.setUTCDate(t.getUTCDate() + 1);
|
|
185
|
+
t.setUTCHours(0, 0, 0, 0);
|
|
186
|
+
}
|
|
187
|
+
else if (!fieldMatches(hour, t.getUTCHours())) {
|
|
188
|
+
t.setUTCHours(t.getUTCHours() + 1, 0, 0, 0);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
if (fieldMatches(minute, t.getUTCMinutes()))
|
|
192
|
+
out.push(new Date(t));
|
|
193
|
+
t.setTime(t.getTime() + 60000);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* more than that, as data that survives into the build output:
|
|
6
6
|
*
|
|
7
7
|
* - `quorum`: how many distinct human approvers the gate needs, and
|
|
8
|
-
* optionally which roles count toward it.
|
|
8
|
+
* optionally which roles count toward it. Since #4290 it may be a list of
|
|
9
|
+
* rules, each scoped by `targets` globs over the run's environment
|
|
10
|
+
* (`./gate-quorum.ts`).
|
|
9
11
|
* - `policy`: a policy set a lexicon rendered (the cedar lexicon's
|
|
10
12
|
* `gatePolicy()`), evaluated for each approval as a `PassGate` request.
|
|
11
13
|
* - `mode`: `"log-only"` records the policy's decision next to the approval
|
|
@@ -30,11 +32,15 @@
|
|
|
30
32
|
* decision also records the digest of the context it was asked about, and a
|
|
31
33
|
* permit counts only while the run resolves that same context (#3774).
|
|
32
34
|
*
|
|
33
|
-
*
|
|
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 since #4290 the quorum
|
|
37
|
+
* never reads them: a role counts only when `.chant/trust.json` at base
|
|
38
|
+
* grants it and a seal proves the approver (`./gate-quorum.ts`). A policy
|
|
39
|
+
* still sees the claimed roles in its request.
|
|
36
40
|
*/
|
|
37
41
|
import { type StepOutputRef } from "./step-output-ref.js";
|
|
42
|
+
import { type GateQuorumRule, type GateQuorumSpec } from "./gate-quorum.js";
|
|
43
|
+
export type { GateQuorumRule, GateQuorumSpec } from "./gate-quorum.js";
|
|
38
44
|
/** What a policy's decision does to the gate. */
|
|
39
45
|
export type GateApprovalMode = "log-only" | "enforce";
|
|
40
46
|
export declare const GATE_APPROVAL_MODES: readonly GateApprovalMode[];
|
|
@@ -54,18 +60,18 @@ export interface GatePolicyRef {
|
|
|
54
60
|
/** The policy set, in the lexicon's own language. */
|
|
55
61
|
text: string;
|
|
56
62
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
63
|
+
/**
|
|
64
|
+
* How many distinct human approvers a gate needs, and which roles count: one
|
|
65
|
+
* approval rule (#4290, `./gate-quorum.ts`). A role counts only when
|
|
66
|
+
* `.chant/trust.json` at base grants it to a proven approver.
|
|
67
|
+
*/
|
|
68
|
+
export type GateQuorum = GateQuorumRule;
|
|
64
69
|
/** A value a gate hands its policy as context. */
|
|
65
70
|
export type GateContextValue = string | number | boolean | string[] | StepOutputRef;
|
|
66
71
|
/** The `approval` block on a `gate` step. */
|
|
67
72
|
export interface GateApproval {
|
|
68
|
-
|
|
73
|
+
/** One rule, or a list of rules scoped by `targets` over the run's environment (#4290). */
|
|
74
|
+
quorum?: GateQuorumSpec;
|
|
69
75
|
policy?: GatePolicyRef;
|
|
70
76
|
/** Default `"log-only"`, so a new policy is observed against real gate traffic before it binds. */
|
|
71
77
|
mode?: GateApprovalMode;
|
|
@@ -85,7 +91,8 @@ export interface GateApproval {
|
|
|
85
91
|
* actually produced.
|
|
86
92
|
*/
|
|
87
93
|
export interface ResolvedGateApproval {
|
|
88
|
-
|
|
94
|
+
/** The rules this run's gate takes (#4290): a list, or one rule on a fact written before. */
|
|
95
|
+
quorum?: GateQuorumSpec;
|
|
89
96
|
policy?: GatePolicyRef;
|
|
90
97
|
mode: GateApprovalMode;
|
|
91
98
|
context?: Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-approval.d.ts","sourceRoot":"","sources":["../../src/op/gate-approval.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"gate-approval.d.ts","sourceRoot":"","sources":["../../src/op/gate-approval.ts"],"names":[],"mappings":"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"}
|
package/dist/op/gate-approval.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* more than that, as data that survives into the build output:
|
|
6
6
|
*
|
|
7
7
|
* - `quorum`: how many distinct human approvers the gate needs, and
|
|
8
|
-
* optionally which roles count toward it.
|
|
8
|
+
* optionally which roles count toward it. Since #4290 it may be a list of
|
|
9
|
+
* rules, each scoped by `targets` globs over the run's environment
|
|
10
|
+
* (`./gate-quorum.ts`).
|
|
9
11
|
* - `policy`: a policy set a lexicon rendered (the cedar lexicon's
|
|
10
12
|
* `gatePolicy()`), evaluated for each approval as a `PassGate` request.
|
|
11
13
|
* - `mode`: `"log-only"` records the policy's decision next to the approval
|
|
@@ -30,14 +32,17 @@
|
|
|
30
32
|
* decision also records the digest of the context it was asked about, and a
|
|
31
33
|
* permit counts only while the run resolves that same context (#3774).
|
|
32
34
|
*
|
|
33
|
-
*
|
|
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 since #4290 the quorum
|
|
37
|
+
* never reads them: a role counts only when `.chant/trust.json` at base
|
|
38
|
+
* grants it and a seal proves the approver (`./gate-quorum.ts`). A policy
|
|
39
|
+
* still sees the claimed roles in its request.
|
|
36
40
|
*/
|
|
37
41
|
import { createHash } from "node:crypto";
|
|
38
42
|
import { lexiconModulePath, importLexiconPackage } from "../lexicon-module.js";
|
|
39
43
|
import { isStepOutputRef } from "./step-output-ref.js";
|
|
40
44
|
import { GATE_PLAN_CONTEXT_KEY } from "./gate-plan-context.js";
|
|
45
|
+
import { quorumProblems } from "./gate-quorum.js";
|
|
41
46
|
export const GATE_APPROVAL_MODES = ["log-only", "enforce"];
|
|
42
47
|
/** The version of a gate policy: `sha256:` and the hex digest of its text. A lexicon's helper stamps it, and OPS015 checks it still matches. */
|
|
43
48
|
export function gatePolicyVersion(text) {
|
|
@@ -79,20 +84,10 @@ export function gateApprovalProblems(approval) {
|
|
|
79
84
|
const a = approval;
|
|
80
85
|
const problems = [];
|
|
81
86
|
if (a.quorum !== undefined) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
else {
|
|
87
|
-
if (typeof q.count !== "number" || !Number.isInteger(q.count) || q.count < 1) {
|
|
88
|
-
problems.push("`approval.quorum.count` must be an integer of at least 1");
|
|
89
|
-
}
|
|
90
|
-
if (q.roles !== undefined) {
|
|
91
|
-
if (!Array.isArray(q.roles) || q.roles.length === 0 || q.roles.some((r) => typeof r !== "string" || r === "")) {
|
|
92
|
-
problems.push("`approval.quorum.roles` must be a non-empty list of role names");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
87
|
+
if (typeof a.quorum !== "object" || a.quorum === null)
|
|
88
|
+
problems.push("`approval.quorum` must be an object or a list of rules");
|
|
89
|
+
else
|
|
90
|
+
problems.push(...quorumProblems(a.quorum, "approval.quorum", { notAuthor: false }));
|
|
96
91
|
}
|
|
97
92
|
if (a.policy !== undefined) {
|
|
98
93
|
if (!isGatePolicyRef(a.policy)) {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval rules (#4290): a gate's quorum as a list of rules, each with the
|
|
3
|
+
* targets it covers, how many approvers it needs, the roles that count, and
|
|
4
|
+
* whether one of them must not be the change's author.
|
|
5
|
+
*
|
|
6
|
+
* quorum: [
|
|
7
|
+
* { targets: ["prod-*"], count: 2, roles: ["platform"], notAuthor: true },
|
|
8
|
+
* { count: 1 },
|
|
9
|
+
* ]
|
|
10
|
+
*
|
|
11
|
+
* A gate takes every rule whose `targets` glob matches one of its targets
|
|
12
|
+
* (a rule with no `targets` covers every target), and passes once each rule
|
|
13
|
+
* it takes is met by distinct human approvers of its plan. A gate that takes
|
|
14
|
+
* no rule needs one human approval, the rule every gate had before.
|
|
15
|
+
*
|
|
16
|
+
* Roles come from `.chant/trust.json` at base (`roles`: role name to the
|
|
17
|
+
* principals that hold it), never from the approver's own claim. An
|
|
18
|
+
* approver counts toward `roles`, and as someone who is not the author, only
|
|
19
|
+
* when the approval proves who they are: a seal that verifies against the
|
|
20
|
+
* signers file at base, or under `pr-review` the forge review of the merged
|
|
21
|
+
* pull request. The judge of a gate decides that ({@link GateQuorumJudge});
|
|
22
|
+
* with none, no approval proves anything, so a rule naming roles or
|
|
23
|
+
* `notAuthor` is never met.
|
|
24
|
+
*
|
|
25
|
+
* This module is pure: no git, no file reads, no package, so
|
|
26
|
+
* `@intentius/chant/gate-decision` bundles it. The judge is built by the
|
|
27
|
+
* caller from the trust policy it read at base ({@link trustQuorumJudge}).
|
|
28
|
+
*/
|
|
29
|
+
import { type GateSigners } from "../lifecycle/gate-seal.js";
|
|
30
|
+
import type { GateResolutionRecord } from "../lifecycle/gate-ledger.js";
|
|
31
|
+
/** One approval rule. */
|
|
32
|
+
export interface GateQuorumRule {
|
|
33
|
+
/** Globs over the gate's targets (an Op wave's run targets, an Op gate's environment). Absent, the rule covers every target. */
|
|
34
|
+
targets?: string[];
|
|
35
|
+
/** Distinct human approvers the rule needs. At least 1. */
|
|
36
|
+
count: number;
|
|
37
|
+
/** When set, only an approver `.chant/trust.json` at base gives one of these roles counts, and only with a proven approval. */
|
|
38
|
+
roles?: string[];
|
|
39
|
+
/** When true, at least one counted approver is proven and is not the change's author. */
|
|
40
|
+
notAuthor?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** A gate's quorum: one rule (the form before #4290), or a list of them. */
|
|
43
|
+
export type GateQuorumSpec = GateQuorumRule | readonly GateQuorumRule[];
|
|
44
|
+
/** What a judge says about one approval. */
|
|
45
|
+
export interface QuorumJudgement {
|
|
46
|
+
/** The approval proves who `resolvedBy` is: a verified seal, or the forge's review. */
|
|
47
|
+
proven: boolean;
|
|
48
|
+
/** The roles `.chant/trust.json` at base gives the approver. Empty unless `proven`. */
|
|
49
|
+
roles: string[];
|
|
50
|
+
/** Whether the approver is the change's author. Undefined when nobody knows who the author is. */
|
|
51
|
+
author?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Judges one approval for the rules. */
|
|
54
|
+
export type GateQuorumJudge = (approval: GateResolutionRecord) => QuorumJudgement;
|
|
55
|
+
/** The judge with nothing to judge by: no approval proves anyone, and nobody's author is known. */
|
|
56
|
+
export declare const UNPROVEN_JUDGE: GateQuorumJudge;
|
|
57
|
+
/** How far one rule is met. */
|
|
58
|
+
export interface GateQuorumRuleProgress {
|
|
59
|
+
targets?: string[];
|
|
60
|
+
count: number;
|
|
61
|
+
roles?: string[];
|
|
62
|
+
notAuthor?: boolean;
|
|
63
|
+
/** The approvers it counts, oldest first. */
|
|
64
|
+
approvers: string[];
|
|
65
|
+
met: boolean;
|
|
66
|
+
/** Why the author could not be told apart, when `notAuthor` is set and nobody knows the author. */
|
|
67
|
+
authorUnknown?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** The rules of a quorum, as a list. Absent, none. */
|
|
70
|
+
export declare function quorumRules(quorum: GateQuorumSpec | undefined): GateQuorumRule[];
|
|
71
|
+
/**
|
|
72
|
+
* Whether `target` matches `glob`: `*` within a `/` segment, `**` across
|
|
73
|
+
* segments (and `a/**\/b` across none), `?` one character. The same reading
|
|
74
|
+
* as the terraform lexicon's unit globs, so a Terragrunt root's glob means
|
|
75
|
+
* one thing in both.
|
|
76
|
+
*/
|
|
77
|
+
export declare function targetGlobMatch(glob: string, target: string): boolean;
|
|
78
|
+
/** The rules a gate whose targets are `targets` takes: those with no `targets`, and those a glob of matches one. */
|
|
79
|
+
export declare function quorumRulesFor(rules: readonly GateQuorumRule[], targets: readonly string[]): GateQuorumRule[];
|
|
80
|
+
/** The most approvers any rule needs, 1 with none. */
|
|
81
|
+
export declare function quorumNeed(rules: readonly GateQuorumRule[]): number;
|
|
82
|
+
/**
|
|
83
|
+
* Everything wrong with a quorum, one line each. `where` names it
|
|
84
|
+
* (`approval.quorum`). With `notAuthor: false`, a rule naming `notAuthor` is
|
|
85
|
+
* refused, since that caller has no author to judge by.
|
|
86
|
+
*/
|
|
87
|
+
export declare function quorumProblems(value: unknown, where: string, opts: {
|
|
88
|
+
notAuthor: boolean;
|
|
89
|
+
}): string[];
|
|
90
|
+
/** Whether two principals name the same person, compared trimmed and lower-cased. */
|
|
91
|
+
export declare const samePrincipal: (a: string, b: string) => boolean;
|
|
92
|
+
/** The roles `roles` (`.chant/trust.json`'s role grants) gives `who`, sorted. */
|
|
93
|
+
export declare function rolesOf(who: string, roles: Readonly<Record<string, readonly string[]>>): string[];
|
|
94
|
+
/** One approver as a rule counts them. */
|
|
95
|
+
export interface QuorumApprover {
|
|
96
|
+
who: string;
|
|
97
|
+
at: string;
|
|
98
|
+
judged: QuorumJudgement;
|
|
99
|
+
}
|
|
100
|
+
/** How far `rule` is met by `approvers`: each counted once, by name, oldest first. */
|
|
101
|
+
export declare function quorumRuleProgress(rule: GateQuorumRule, approvers: readonly QuorumApprover[]): GateQuorumRuleProgress;
|
|
102
|
+
/**
|
|
103
|
+
* Where a rule stands, in one line: `prod-*: 1 of 2 approvals (github:alice),
|
|
104
|
+
* needs 1 more from platform`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function quorumRuleLine(p: GateQuorumRuleProgress): string;
|
|
107
|
+
/** The rules that are not met, one line each joined by `; `. */
|
|
108
|
+
export declare function quorumWaiting(progress: readonly GateQuorumRuleProgress[]): string;
|
|
109
|
+
/** Whether a rule needs a proven approver: it names roles, or `notAuthor`. */
|
|
110
|
+
export declare const ruleNeedsProof: (rule: GateQuorumRule) => boolean;
|
|
111
|
+
/** What {@link trustQuorumJudge} judges by. */
|
|
112
|
+
export interface TrustQuorumInput {
|
|
113
|
+
/** The signers at base: an approval whose seal verifies for its approver against them is proven. */
|
|
114
|
+
signers: GateSigners;
|
|
115
|
+
/** `.chant/trust.json`'s role grants at base. */
|
|
116
|
+
roles: Readonly<Record<string, readonly string[]>>;
|
|
117
|
+
/** The change's authors, as principals. Absent, nobody knows who they are. */
|
|
118
|
+
authors?: readonly string[];
|
|
119
|
+
/**
|
|
120
|
+
* Under `pr-review`, the merged pull request's review, read from the forge
|
|
121
|
+
* in this run: a resolution that names this review's head and one of its
|
|
122
|
+
* approvers as `resolvedBy` is proven.
|
|
123
|
+
*/
|
|
124
|
+
review?: {
|
|
125
|
+
head: string;
|
|
126
|
+
approvers: readonly string[];
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** The judge a gate's rules are met by: proof from a seal or the live review, roles from trust.json at base. */
|
|
130
|
+
export declare function trustQuorumJudge(input: TrustQuorumInput): GateQuorumJudge;
|
|
131
|
+
//# sourceMappingURL=gate-quorum.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-quorum.d.ts","sourceRoot":"","sources":["../../src/op/gate-quorum.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval rules (#4290): a gate's quorum as a list of rules, each with the
|
|
3
|
+
* targets it covers, how many approvers it needs, the roles that count, and
|
|
4
|
+
* whether one of them must not be the change's author.
|
|
5
|
+
*
|
|
6
|
+
* quorum: [
|
|
7
|
+
* { targets: ["prod-*"], count: 2, roles: ["platform"], notAuthor: true },
|
|
8
|
+
* { count: 1 },
|
|
9
|
+
* ]
|
|
10
|
+
*
|
|
11
|
+
* A gate takes every rule whose `targets` glob matches one of its targets
|
|
12
|
+
* (a rule with no `targets` covers every target), and passes once each rule
|
|
13
|
+
* it takes is met by distinct human approvers of its plan. A gate that takes
|
|
14
|
+
* no rule needs one human approval, the rule every gate had before.
|
|
15
|
+
*
|
|
16
|
+
* Roles come from `.chant/trust.json` at base (`roles`: role name to the
|
|
17
|
+
* principals that hold it), never from the approver's own claim. An
|
|
18
|
+
* approver counts toward `roles`, and as someone who is not the author, only
|
|
19
|
+
* when the approval proves who they are: a seal that verifies against the
|
|
20
|
+
* signers file at base, or under `pr-review` the forge review of the merged
|
|
21
|
+
* pull request. The judge of a gate decides that ({@link GateQuorumJudge});
|
|
22
|
+
* with none, no approval proves anything, so a rule naming roles or
|
|
23
|
+
* `notAuthor` is never met.
|
|
24
|
+
*
|
|
25
|
+
* This module is pure: no git, no file reads, no package, so
|
|
26
|
+
* `@intentius/chant/gate-decision` bundles it. The judge is built by the
|
|
27
|
+
* caller from the trust policy it read at base ({@link trustQuorumJudge}).
|
|
28
|
+
*/
|
|
29
|
+
import { gateSealRefusal } from "../lifecycle/gate-seal.js";
|
|
30
|
+
/** The judge with nothing to judge by: no approval proves anyone, and nobody's author is known. */
|
|
31
|
+
export const UNPROVEN_JUDGE = () => ({ proven: false, roles: [] });
|
|
32
|
+
/** The rules of a quorum, as a list. Absent, none. */
|
|
33
|
+
export function quorumRules(quorum) {
|
|
34
|
+
if (quorum === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
return Array.isArray(quorum) ? [...quorum] : [quorum];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether `target` matches `glob`: `*` within a `/` segment, `**` across
|
|
40
|
+
* segments (and `a/**\/b` across none), `?` one character. The same reading
|
|
41
|
+
* as the terraform lexicon's unit globs, so a Terragrunt root's glob means
|
|
42
|
+
* one thing in both.
|
|
43
|
+
*/
|
|
44
|
+
export function targetGlobMatch(glob, target) {
|
|
45
|
+
const g = glob.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
46
|
+
let re = "";
|
|
47
|
+
for (let i = 0; i < g.length; i++) {
|
|
48
|
+
const c = g[i];
|
|
49
|
+
if (c === "*" && g[i + 1] === "*") {
|
|
50
|
+
if (g[i + 2] === "/") {
|
|
51
|
+
re += "(?:.*/)?";
|
|
52
|
+
i += 2;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
re += ".*";
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (c === "*")
|
|
60
|
+
re += "[^/]*";
|
|
61
|
+
else if (c === "?")
|
|
62
|
+
re += "[^/]";
|
|
63
|
+
else
|
|
64
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
}
|
|
66
|
+
return new RegExp(`^${re}$`).test(target.replace(/\\/g, "/").replace(/\/+$/, ""));
|
|
67
|
+
}
|
|
68
|
+
/** The rules a gate whose targets are `targets` takes: those with no `targets`, and those a glob of matches one. */
|
|
69
|
+
export function quorumRulesFor(rules, targets) {
|
|
70
|
+
return rules.filter((r) => r.targets === undefined || r.targets.some((g) => targets.some((t) => targetGlobMatch(g, t))));
|
|
71
|
+
}
|
|
72
|
+
/** The most approvers any rule needs, 1 with none. */
|
|
73
|
+
export function quorumNeed(rules) {
|
|
74
|
+
return rules.reduce((n, r) => Math.max(n, r.count), 1);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Everything wrong with a quorum, one line each. `where` names it
|
|
78
|
+
* (`approval.quorum`). With `notAuthor: false`, a rule naming `notAuthor` is
|
|
79
|
+
* refused, since that caller has no author to judge by.
|
|
80
|
+
*/
|
|
81
|
+
export function quorumProblems(value, where, opts) {
|
|
82
|
+
const list = Array.isArray(value) ? value : [value];
|
|
83
|
+
const problems = [];
|
|
84
|
+
if (Array.isArray(value) && value.length === 0)
|
|
85
|
+
problems.push(`\`${where}\` must name at least one rule`);
|
|
86
|
+
list.forEach((rule, i) => {
|
|
87
|
+
const at = Array.isArray(value) ? `${where}[${i}]` : where;
|
|
88
|
+
if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
|
|
89
|
+
problems.push(`\`${at}\` must be an object`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const q = rule;
|
|
93
|
+
if (typeof q.count !== "number" || !Number.isInteger(q.count) || q.count < 1)
|
|
94
|
+
problems.push(`\`${at}.count\` must be an integer of at least 1`);
|
|
95
|
+
if (q.roles !== undefined && (!Array.isArray(q.roles) || q.roles.length === 0 || q.roles.some((r) => typeof r !== "string" || r === ""))) {
|
|
96
|
+
problems.push(`\`${at}.roles\` must be a non-empty list of role names`);
|
|
97
|
+
}
|
|
98
|
+
if (q.targets !== undefined && (!Array.isArray(q.targets) || q.targets.length === 0 || q.targets.some((t) => typeof t !== "string" || t === ""))) {
|
|
99
|
+
problems.push(`\`${at}.targets\` must be a non-empty list of globs`);
|
|
100
|
+
}
|
|
101
|
+
if (q.notAuthor !== undefined && typeof q.notAuthor !== "boolean")
|
|
102
|
+
problems.push(`\`${at}.notAuthor\` must be true or false`);
|
|
103
|
+
if (q.notAuthor === true && !opts.notAuthor) {
|
|
104
|
+
problems.push(`\`${at}.notAuthor\` is not read here: an Op gate has no author to judge by. An Op waves spec's \`approvals.quorum\` reads it from the merged pull request`);
|
|
105
|
+
}
|
|
106
|
+
const known = new Set(["targets", "count", "roles", "notAuthor"]);
|
|
107
|
+
for (const key of Object.keys(q))
|
|
108
|
+
if (!known.has(key))
|
|
109
|
+
problems.push(`\`${at}.${key}\` is not a quorum field; use targets, count, roles or notAuthor`);
|
|
110
|
+
});
|
|
111
|
+
return problems;
|
|
112
|
+
}
|
|
113
|
+
const norm = (s) => s.normalize("NFKC").trim().toLowerCase();
|
|
114
|
+
/** Whether two principals name the same person, compared trimmed and lower-cased. */
|
|
115
|
+
export const samePrincipal = (a, b) => norm(a) === norm(b);
|
|
116
|
+
/** The roles `roles` (`.chant/trust.json`'s role grants) gives `who`, sorted. */
|
|
117
|
+
export function rolesOf(who, roles) {
|
|
118
|
+
return Object.entries(roles)
|
|
119
|
+
.filter(([, principals]) => principals.some((p) => samePrincipal(p, who)))
|
|
120
|
+
.map(([role]) => role)
|
|
121
|
+
.sort();
|
|
122
|
+
}
|
|
123
|
+
/** How far `rule` is met by `approvers`: each counted once, by name, oldest first. */
|
|
124
|
+
export function quorumRuleProgress(rule, approvers) {
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
const counted = [];
|
|
127
|
+
for (const a of [...approvers].sort((x, y) => Date.parse(x.at) - Date.parse(y.at))) {
|
|
128
|
+
if (seen.has(norm(a.who)))
|
|
129
|
+
continue;
|
|
130
|
+
if (rule.roles?.length && !(a.judged.proven && a.judged.roles.some((r) => rule.roles.includes(r))))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(norm(a.who));
|
|
133
|
+
counted.push(a);
|
|
134
|
+
}
|
|
135
|
+
const notAuthor = !rule.notAuthor || counted.some((a) => a.judged.proven && a.judged.author === false);
|
|
136
|
+
const authorUnknown = rule.notAuthor === true && !notAuthor && counted.some((a) => a.judged.proven && a.judged.author === undefined);
|
|
137
|
+
return {
|
|
138
|
+
...(rule.targets ? { targets: [...rule.targets] } : {}),
|
|
139
|
+
count: rule.count,
|
|
140
|
+
...(rule.roles?.length ? { roles: [...rule.roles] } : {}),
|
|
141
|
+
...(rule.notAuthor ? { notAuthor: true } : {}),
|
|
142
|
+
approvers: counted.map((a) => a.who),
|
|
143
|
+
met: counted.length >= rule.count && notAuthor,
|
|
144
|
+
...(authorUnknown ? { authorUnknown: true } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Where a rule stands, in one line: `prod-*: 1 of 2 approvals (github:alice),
|
|
149
|
+
* needs 1 more from platform`.
|
|
150
|
+
*/
|
|
151
|
+
export function quorumRuleLine(p) {
|
|
152
|
+
const scope = p.targets ? p.targets.join(", ") : "every target";
|
|
153
|
+
const have = `${p.approvers.length} of ${p.count} approval${p.count === 1 ? "" : "s"}${p.approvers.length ? ` (${p.approvers.join(", ")})` : ""}`;
|
|
154
|
+
if (p.met)
|
|
155
|
+
return `${scope}: ${have}, met`;
|
|
156
|
+
const more = p.count - p.approvers.length;
|
|
157
|
+
const needs = [];
|
|
158
|
+
if (more > 0)
|
|
159
|
+
needs.push(`${more} more${p.roles?.length ? ` from ${p.roles.join(" or ")}` : ""}`);
|
|
160
|
+
if (p.notAuthor)
|
|
161
|
+
needs.push(more > 0 ? "one of them not the author" : "an approver who is not the author");
|
|
162
|
+
const unknown = p.authorUnknown ? " (the change's author is not known, so no approver can show they are someone else)" : "";
|
|
163
|
+
return `${scope}: ${have}, needs ${needs.join(", ")}${unknown}`;
|
|
164
|
+
}
|
|
165
|
+
/** The rules that are not met, one line each joined by `; `. */
|
|
166
|
+
export function quorumWaiting(progress) {
|
|
167
|
+
return progress.filter((p) => !p.met).map(quorumRuleLine).join("; ");
|
|
168
|
+
}
|
|
169
|
+
/** Whether a rule needs a proven approver: it names roles, or `notAuthor`. */
|
|
170
|
+
export const ruleNeedsProof = (rule) => (rule.roles?.length ?? 0) > 0 || rule.notAuthor === true;
|
|
171
|
+
/** The judge a gate's rules are met by: proof from a seal or the live review, roles from trust.json at base. */
|
|
172
|
+
export function trustQuorumJudge(input) {
|
|
173
|
+
return (r) => {
|
|
174
|
+
const sealed = r.seal !== undefined && r.seal !== null && gateSealRefusal(input.signers, r) === null;
|
|
175
|
+
const reviewed = input.review !== undefined && r.review?.head === input.review.head && input.review.approvers.some((a) => samePrincipal(a, r.resolvedBy));
|
|
176
|
+
const proven = sealed || reviewed;
|
|
177
|
+
return {
|
|
178
|
+
proven,
|
|
179
|
+
roles: proven ? rolesOf(r.resolvedBy, input.roles) : [],
|
|
180
|
+
...(input.authors !== undefined ? { author: input.authors.some((a) => samePrincipal(a, r.resolvedBy)) } : {}),
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
package/dist/op/gate-resume.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* `./gate`'s quorum rules.
|
|
9
9
|
*/
|
|
10
10
|
import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger.js";
|
|
11
|
+
import type { GateQuorumJudge } from "./gate-quorum.js";
|
|
11
12
|
export { isGateRunLocator, resolveGateRunLocator, resumeGateRun, resumeTokenFromEnv, type GateResumeOutcome, type GateRunLocator, type ResumeGateRunOptions, } from "../gate-resume-run.js";
|
|
12
13
|
/**
|
|
13
14
|
* The resolution that answers `standing`, read the way a run decides the gate
|
|
@@ -15,7 +16,8 @@ export { isGateRunLocator, resolveGateRunLocator, resumeGateRun, resumeTokenFrom
|
|
|
15
16
|
* gate ledger's pending facts, which place the human quorum's anchor (#3206);
|
|
16
17
|
* without them it is `standing` itself. A workspace's
|
|
17
18
|
* `identity.gates` rule is not applied here: the resumed job applies it, and
|
|
18
|
-
* waits again when the approval does not count.
|
|
19
|
+
* waits again when the approval does not count. `judge` proves approvers
|
|
20
|
+
* for approval rules that name roles (#4290); absent, none is proven.
|
|
19
21
|
*/
|
|
20
|
-
export declare function resolutionAnswering(resolutions: readonly GateResolutionRecord[], standing: PendingGateRecord, pending?: readonly PendingGateRecord[]): GateResolutionRecord | undefined;
|
|
22
|
+
export declare function resolutionAnswering(resolutions: readonly GateResolutionRecord[], standing: PendingGateRecord, pending?: readonly PendingGateRecord[], judge?: GateQuorumJudge): GateResolutionRecord | undefined;
|
|
21
23
|
//# sourceMappingURL=gate-resume.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-resume.d.ts","sourceRoot":"","sources":["../../src/op/gate-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"gate-resume.d.ts","sourceRoot":"","sources":["../../src/op/gate-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,oBAAoB,EAAE,EAC5C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,SAAS,iBAAiB,EAAe,EAClD,KAAK,CAAC,EAAE,eAAe,GACtB,oBAAoB,GAAG,SAAS,CASlC"}
|
package/dist/op/gate-resume.js
CHANGED
|
@@ -16,16 +16,17 @@ export { isGateRunLocator, resolveGateRunLocator, resumeGateRun, resumeTokenFrom
|
|
|
16
16
|
* gate ledger's pending facts, which place the human quorum's anchor (#3206);
|
|
17
17
|
* without them it is `standing` itself. A workspace's
|
|
18
18
|
* `identity.gates` rule is not applied here: the resumed job applies it, and
|
|
19
|
-
* waits again when the approval does not count.
|
|
19
|
+
* waits again when the approval does not count. `judge` proves approvers
|
|
20
|
+
* for approval rules that name roles (#4290); absent, none is proven.
|
|
20
21
|
*/
|
|
21
|
-
export function resolutionAnswering(resolutions, standing, pending = [standing]) {
|
|
22
|
+
export function resolutionAnswering(resolutions, standing, pending = [standing], judge) {
|
|
22
23
|
const env = standing.environment;
|
|
23
24
|
const mine = resolutions.filter((r) => env === undefined || r.environment === env);
|
|
24
25
|
if (standing.approval) {
|
|
25
|
-
const tally = tallyGateApprovals(mine, standing.gate, standing.timestamp, standing.planDigest, standing.approval, humanQuorumSince(pending, standing));
|
|
26
|
+
const tally = tallyGateApprovals(mine, standing.gate, standing.timestamp, standing.planDigest, standing.approval, humanQuorumSince(pending, standing), judge);
|
|
26
27
|
if (tally.permit)
|
|
27
28
|
return tally.permit;
|
|
28
|
-
return tally.
|
|
29
|
+
return tally.met ? tally.counted[tally.counted.length - 1] : undefined;
|
|
29
30
|
}
|
|
30
31
|
return latestResolutionForPlan(mine, standing.gate, standing.timestamp, standing.planDigest).resolution;
|
|
31
32
|
}
|
package/dist/op/gate-review.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* back. The author's own review never counts.
|
|
30
30
|
*/
|
|
31
31
|
import { readFileSync } from "node:fs";
|
|
32
|
-
import { gateSealRefusal } from "../
|
|
32
|
+
import { gateSealRefusal } from "../lifecycle/gate-seal.js";
|
|
33
33
|
import { readTrustPolicy } from "../workspace/trust/policy.js";
|
|
34
34
|
import { gitRevisionSource } from "../workspace/record-source.js";
|
|
35
35
|
import { forgePrincipalOf, ForgeApiError } from "../pr-forge.js";
|