@intentius/chant 0.124.40 → 0.124.42
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/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 +18 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +42 -5
- 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 +76 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +615 -55
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- 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.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +49 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +46 -2
- package/dist/cli/registry.d.ts +31 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +37 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +44 -4
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- 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 +92 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +150 -21
- 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 +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +8 -3
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- 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/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/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- 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/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/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/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- 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 +28 -43
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +170 -120
- 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/op/stage-hooks.d.ts +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -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 +64 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +331 -44
- package/dist/pr-loop.d.ts +107 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +84 -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/pr-requires.d.ts +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -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 +243 -243
- 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/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- 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-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- 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 +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +66 -4
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +69 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +149 -2
- 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 +94 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- 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.map +1 -1
- package/dist/workspace/status.js +4 -1
- 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/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +18 -1
- package/src/cli/handlers/fan-out.ts +43 -6
- 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-before-merge.test.ts +276 -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 +605 -61
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/parse-args.ts +34 -2
- package/src/cli/registry.ts +31 -2
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +58 -3
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/gate-plan.ts +3 -0
- 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 +194 -21
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +14 -2
- package/src/components/wave-plan.ts +38 -3
- 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/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- 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 +197 -183
- 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/op/stage-hooks.ts +251 -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 +347 -35
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +170 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/pr-requires.test.ts +184 -0
- package/src/pr-requires.ts +79 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -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/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- 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-init.ts +5 -4
- 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 +107 -8
- package/src/workspace/ci-run.ts +74 -5
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +149 -2
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +140 -5
- 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 +94 -3
- package/src/workspace/reason-codes.ts +1 -1
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- 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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate-time policy on the pull-request path (#4294): `ci.policy` in the
|
|
3
|
+
* workspace declaration, run by `chant components pr-plan` and `pr-apply`
|
|
4
|
+
* over the change-set document the gate binds (`CapabilityPlan.changeSet`,
|
|
5
|
+
* composed by `planPrSet`).
|
|
6
|
+
*
|
|
7
|
+
* The settings and the policy directory are both read at the base the job
|
|
8
|
+
* measures the change from, so a pull request cannot loosen, edit or delete
|
|
9
|
+
* the policy that judges it. The engine and its result shapes are
|
|
10
|
+
* `../gate-policy.ts`.
|
|
11
|
+
*
|
|
12
|
+
* - deny: pr-apply applies nothing unless an override counts
|
|
13
|
+
* (`../override-gate.ts`, subject the gate's op, such as `pr-12`). The
|
|
14
|
+
* first refusal records the denial on `_gates/policy-override.jsonl`, so
|
|
15
|
+
* `chant approve policy-override <op> --rule <id> --reason <text>` can
|
|
16
|
+
* answer it. The gate's own approval is still needed. The override is
|
|
17
|
+
* spent just before anything applies.
|
|
18
|
+
* - hold: the plan needs a person's approval of its own digest. pr-apply
|
|
19
|
+
* always asks for one, so a hold changes one path: a green commit's plan
|
|
20
|
+
* no longer applies under the approvals of the merges it carries.
|
|
21
|
+
* - error: the policy did not run or gave no readable answer. pr-apply
|
|
22
|
+
* applies nothing.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readDeclaration, WorkspaceReadError } from "../workspace/declaration";
|
|
26
|
+
import { locateWorkspace } from "../workspace/which-chant";
|
|
27
|
+
import type { WorkspaceTree } from "../workspace/tree";
|
|
28
|
+
import {
|
|
29
|
+
checkGatePolicy,
|
|
30
|
+
DEFAULT_POLICY_PATH,
|
|
31
|
+
exportGatePolicy,
|
|
32
|
+
type GatePolicyExec,
|
|
33
|
+
type GatePolicySettings,
|
|
34
|
+
type GatePolicyVerdict,
|
|
35
|
+
} from "../gate-policy";
|
|
36
|
+
import type { ChangeSetDocument } from "../change-set";
|
|
37
|
+
import type { GateLedgerPort } from "../op/gate";
|
|
38
|
+
import type { PrPolicyReport } from "../pr-loop";
|
|
39
|
+
|
|
40
|
+
/** `ci.policy` as read at base. */
|
|
41
|
+
export interface PrLoopPolicy {
|
|
42
|
+
/** Absent when the declaration at base sets none. */
|
|
43
|
+
settings?: GatePolicySettings;
|
|
44
|
+
/** The commit it was read at. */
|
|
45
|
+
base: string;
|
|
46
|
+
/** The tree of the declaration's directory at base, where the policy directory is read. */
|
|
47
|
+
tree?: WorkspaceTree;
|
|
48
|
+
/** A line for the log when no policy runs because the base has no declaration, or could not be read. */
|
|
49
|
+
note?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether the working tree's declaration names a `ci.policy`. */
|
|
53
|
+
function checkoutNamesPolicy(cwd: string): boolean {
|
|
54
|
+
try {
|
|
55
|
+
return readDeclaration(locateWorkspace(cwd).tree).ci?.policy !== undefined;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The policy a pull-request job in `cwd` runs, from the declaration at
|
|
63
|
+
* `base`. Throws when the declaration at base cannot be read and the
|
|
64
|
+
* working tree's names a policy: running none would let the change waive it.
|
|
65
|
+
*/
|
|
66
|
+
export function prLoopPolicyAt(cwd: string, base: string): PrLoopPolicy {
|
|
67
|
+
try {
|
|
68
|
+
const located = locateWorkspace(cwd, base);
|
|
69
|
+
const settings = readDeclaration(located.tree).ci?.policy;
|
|
70
|
+
return { base, ...(settings ? { settings, tree: located.tree } : {}) };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const missing = err instanceof WorkspaceReadError && (err.code === "declaration-missing" || err.code === "not-a-git-repository");
|
|
73
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
74
|
+
if (checkoutNamesPolicy(cwd)) {
|
|
75
|
+
if (missing) return { base, note: `ci.policy: the base ${base.slice(0, 12)} has no workspace declaration, so the policy this change declares runs once it merges` };
|
|
76
|
+
throw new Error(`ci.policy is read from the workspace declaration at ${base.slice(0, 12)}, and it could not be read (${why}), so the policy cannot be run`);
|
|
77
|
+
}
|
|
78
|
+
return missing ? { base } : { base, note: `ci.policy: the declaration at ${base.slice(0, 12)} could not be read (${why}); this checkout names no policy, so none runs` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What the engine reads as `input`. */
|
|
83
|
+
export interface PrPolicyInput {
|
|
84
|
+
version: 1;
|
|
85
|
+
stage: "plan" | "apply";
|
|
86
|
+
/** The gate's op, `pr-<n>`. */
|
|
87
|
+
op: string;
|
|
88
|
+
gate: string;
|
|
89
|
+
env: string;
|
|
90
|
+
pr: number | null;
|
|
91
|
+
base: string;
|
|
92
|
+
head: string;
|
|
93
|
+
/** The change-set document the gate binds. Its `entries` are every change the plan proposes. */
|
|
94
|
+
changeSet: ChangeSetDocument;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Run the policy over `input`. Undefined when the base sets no policy. */
|
|
98
|
+
export function runPrPolicy(read: PrLoopPolicy, input: PrPolicyInput, exec?: GatePolicyExec): GatePolicyVerdict | undefined {
|
|
99
|
+
if (!read.settings || !read.tree) return undefined;
|
|
100
|
+
const engine = read.settings.engine ?? "conftest";
|
|
101
|
+
const path = read.settings.path ?? DEFAULT_POLICY_PATH;
|
|
102
|
+
const exported = exportGatePolicy(read.tree, path);
|
|
103
|
+
if ("error" in exported) return { result: "error", engine, denials: [], rules: [], holds: [], warnings: [], error: exported.error };
|
|
104
|
+
try {
|
|
105
|
+
return checkGatePolicy({ settings: read.settings, input, policyDir: exported.dir, cwd: process.cwd(), ...(exec ? { exec } : {}) });
|
|
106
|
+
} finally {
|
|
107
|
+
exported.cleanup();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The verdict as the report keeps it. */
|
|
112
|
+
export function prPolicyReport(verdict: GatePolicyVerdict, base: string): PrPolicyReport {
|
|
113
|
+
return {
|
|
114
|
+
result: verdict.result,
|
|
115
|
+
engine: verdict.engine,
|
|
116
|
+
base,
|
|
117
|
+
denials: verdict.denials,
|
|
118
|
+
...(verdict.rules.length > 0 ? { rules: verdict.rules } : {}),
|
|
119
|
+
holds: verdict.holds,
|
|
120
|
+
warnings: verdict.warnings,
|
|
121
|
+
...(verdict.error !== undefined ? { error: verdict.error } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface PrPolicyOverrideInput {
|
|
126
|
+
port: GateLedgerPort;
|
|
127
|
+
/** The override's subject: the gate's op. */
|
|
128
|
+
subject: string;
|
|
129
|
+
/** The digest the gate binds: the change-set document's. */
|
|
130
|
+
digest: string;
|
|
131
|
+
rules: readonly string[];
|
|
132
|
+
/** The commit the overriders and signers are read at. */
|
|
133
|
+
base: string;
|
|
134
|
+
cwd: string;
|
|
135
|
+
/** Count only sealed overrides, as a sealed gate counts only sealed approvals. */
|
|
136
|
+
sealed: boolean;
|
|
137
|
+
/** Record the denial when no override counts and none stands. The plan stage records nothing. */
|
|
138
|
+
record: boolean;
|
|
139
|
+
description?: string;
|
|
140
|
+
now?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Where an override of a denial stands. */
|
|
144
|
+
export type PrPolicyOverride =
|
|
145
|
+
| { status: "overridden"; override: NonNullable<PrPolicyReport["override"]> }
|
|
146
|
+
| { status: "none"; refusals: string[]; command: string; overriders: string[]; recorded: boolean; moved?: { by: string } };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Decide a denied plan against the override ledger, as an Op waves job
|
|
150
|
+
* decides a denied run (`settleDenials` in `../op/op-waves-run.ts`).
|
|
151
|
+
*/
|
|
152
|
+
export async function decidePrPolicyOverride(input: PrPolicyOverrideInput): Promise<PrPolicyOverride> {
|
|
153
|
+
const og = await import("../override-gate");
|
|
154
|
+
const overriders = og.overridersAt(input.cwd, input.base).principals;
|
|
155
|
+
const signers = input.sealed ? (await import("../gate-decision")).gateSignersAt(input.cwd, input.base) : undefined;
|
|
156
|
+
const ledger = await input.port.read(og.OVERRIDE_OP);
|
|
157
|
+
const applied = ((await input.port.readApplied?.(og.OVERRIDE_OP)) ?? []) as unknown as import("../override-gate").AppliedOverrideRecord[];
|
|
158
|
+
const now = input.now ?? new Date().toISOString();
|
|
159
|
+
const verdict = og.decideOverride({
|
|
160
|
+
ledger,
|
|
161
|
+
applied,
|
|
162
|
+
subject: input.subject,
|
|
163
|
+
planDigest: input.digest,
|
|
164
|
+
rules: input.rules,
|
|
165
|
+
overriders,
|
|
166
|
+
mode: input.sealed ? "sealed" : "ledger",
|
|
167
|
+
...(signers ? { signers } : {}),
|
|
168
|
+
now,
|
|
169
|
+
});
|
|
170
|
+
if (verdict.status === "overridden") {
|
|
171
|
+
const o = verdict.override;
|
|
172
|
+
return { status: "overridden", override: { by: o.by, at: o.at, reason: o.reason, digest: o.digest } };
|
|
173
|
+
}
|
|
174
|
+
let recorded = false;
|
|
175
|
+
if (input.record && !verdict.standing) {
|
|
176
|
+
const { version: _v, kind: _k, ...fact } = og.overridePending({
|
|
177
|
+
subject: input.subject,
|
|
178
|
+
planDigest: input.digest,
|
|
179
|
+
rules: input.rules,
|
|
180
|
+
timestamp: now,
|
|
181
|
+
expiresAt: new Date(new Date(now).getTime() + 48 * 3600 * 1000).toISOString(),
|
|
182
|
+
...(input.description ? { description: input.description } : {}),
|
|
183
|
+
});
|
|
184
|
+
await input.port.appendPending(fact);
|
|
185
|
+
recorded = true;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
status: "none",
|
|
189
|
+
refusals: verdict.refusals,
|
|
190
|
+
command: og.overrideCommand(input.subject, input.rules, input.sealed),
|
|
191
|
+
overriders,
|
|
192
|
+
recorded,
|
|
193
|
+
...(verdict.status === "moved" ? { moved: { by: verdict.by } } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Spend the override the apply runs under: append its applied record before
|
|
199
|
+
* anything applies. Throws when it could not be written or did not reach the
|
|
200
|
+
* remote, and the caller applies nothing.
|
|
201
|
+
*/
|
|
202
|
+
export async function spendPrPolicyOverride(port: GateLedgerPort, subject: string, override: NonNullable<PrPolicyReport["override"]>, at: { now?: string; commit?: string } = {}): Promise<void> {
|
|
203
|
+
const og = await import("../override-gate");
|
|
204
|
+
if (!port.appendApplied) throw new Error("this gate ledger cannot record an applied override");
|
|
205
|
+
const record = og.appliedOverride({ subject, digest: override.digest, at: override.at, by: override.by }, { timestamp: at.now ?? new Date().toISOString(), ...(at.commit ? { commit: at.commit } : {}) });
|
|
206
|
+
const pushed = await port.appendApplied(record as unknown as Parameters<NonNullable<GateLedgerPort["appendApplied"]>>[0]);
|
|
207
|
+
const { isLifecycleLocalNote } = await import("../lifecycle/local-note");
|
|
208
|
+
if (!pushed.pushed && !isLifecycleLocalNote(pushed.pushWarning)) {
|
|
209
|
+
throw new Error(`the applied record did not reach the remote the next run reads: ${pushed.pushWarning ?? "the push did not land"}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/components/pr-run.ts
CHANGED
|
@@ -110,6 +110,8 @@ export interface PrPlanSetOptions {
|
|
|
110
110
|
vars?: Record<string, unknown>;
|
|
111
111
|
/** Outputs supplied by the caller (`--seed-outputs`). They win over outputs read here. */
|
|
112
112
|
seededOutputs?: Record<string, Record<string, unknown>>;
|
|
113
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
114
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
113
115
|
/**
|
|
114
116
|
* Where the planning's events go (#4041): a `plan` phase with no
|
|
115
117
|
* component, and in it one `plan` step per component, started as its wave
|
|
@@ -384,6 +386,8 @@ export interface PrApplyOptions {
|
|
|
384
386
|
vars?: Record<string, unknown>;
|
|
385
387
|
/** The set {@link planPrSet} planned and the gate approved, or {@link resumePrSet}'s set. */
|
|
386
388
|
set: PrPlanSet;
|
|
389
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
390
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
387
391
|
onProgress?: (event: RunProgressEvent) => void;
|
|
388
392
|
/** Where the apply's events go (#4041): the fan-out's run events. */
|
|
389
393
|
onRunEvent?: RunEventSink;
|
|
@@ -467,6 +471,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
|
|
|
467
471
|
env: options.env,
|
|
468
472
|
...(options.vars ? { vars: options.vars } : {}),
|
|
469
473
|
plans,
|
|
474
|
+
...(options.targets ? { targets: options.targets } : {}),
|
|
470
475
|
componentOutputs: outputs,
|
|
471
476
|
hold: (name, now) => heldReason(byName.get(name)!, read, now, inChange),
|
|
472
477
|
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
@@ -491,6 +496,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
|
|
|
491
496
|
env: options.env,
|
|
492
497
|
...(options.vars ? { vars: options.vars } : {}),
|
|
493
498
|
seededOutputs: run.componentOutputs,
|
|
499
|
+
...(options.targets ? { targets: options.targets } : {}),
|
|
494
500
|
});
|
|
495
501
|
for (const name of plan.order) roundOf.set(name, { round, set });
|
|
496
502
|
const record = (status: PrRound["status"], approval: PrApproval, message?: string): void => {
|
|
@@ -514,7 +520,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
|
|
|
514
520
|
|
|
515
521
|
const completed = new Set(run.completed);
|
|
516
522
|
const held = new Map((run.held ?? []).map((h) => [h.component, h]));
|
|
517
|
-
const blocked = new
|
|
523
|
+
const blocked = new Map(run.blocked.map((b) => [b.component, b.blockedBy]));
|
|
518
524
|
const errorOf = (component: string): string | undefined => {
|
|
519
525
|
const result = [...run.results].reverse().find((r) => r.component === component);
|
|
520
526
|
if (result?.status === "gated") return `stopped at its own gate ${result.gate?.gate ?? ""}`.trim();
|
|
@@ -529,7 +535,10 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
|
|
|
529
535
|
const h = held.get(m.component);
|
|
530
536
|
if (h) return { ...asPlanned, status: "waiting", held: h.heldBy ? `${h.heldBy} is held: ${h.reason}` : h.reason };
|
|
531
537
|
if (fresh?.status === "plan-failed") return { ...asPlanned, status: "plan-failed", ...(fresh.error ? { error: fresh.error } : {}) };
|
|
532
|
-
if (blocked.has(m.component))
|
|
538
|
+
if (blocked.has(m.component)) {
|
|
539
|
+
const by = blocked.get(m.component);
|
|
540
|
+
return { ...asPlanned, status: "blocked", ...(by ? { blockedBy: by } : {}) };
|
|
541
|
+
}
|
|
533
542
|
const error = errorOf(m.component);
|
|
534
543
|
return { ...asPlanned, status: "failed", ...(error ? { error } : {}) };
|
|
535
544
|
});
|
|
@@ -623,6 +632,8 @@ export interface PrResumeOptions {
|
|
|
623
632
|
env: string;
|
|
624
633
|
vars?: Record<string, unknown>;
|
|
625
634
|
seededOutputs?: Record<string, Record<string, unknown>>;
|
|
635
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
636
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
626
637
|
/** The approved set, from the attempt record. */
|
|
627
638
|
record: PrApplyRecord;
|
|
628
639
|
/** Components an earlier attempt applied. */
|
|
@@ -674,6 +685,7 @@ export async function resumePrSet(options: PrResumeOptions): Promise<PrResume> {
|
|
|
674
685
|
env: options.env,
|
|
675
686
|
...(options.vars ? { vars: options.vars } : {}),
|
|
676
687
|
seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs },
|
|
688
|
+
...(options.targets ? { targets: options.targets } : {}),
|
|
677
689
|
});
|
|
678
690
|
const reasons = fresh.doc.members
|
|
679
691
|
.map((m) => planCoveredBy(options.record.changeSet, fresh.doc, m.member))
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
* `@<component>.publish.*`) resolves.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
|
|
30
|
+
import { assertTargetsAccepted, resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
|
|
31
31
|
import { componentPlanDigest } from "./gate-plan";
|
|
32
32
|
import { componentDeployInputs, type ComponentDeployInputs } from "./deploy-inputs";
|
|
33
33
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
34
|
-
import type { CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
|
|
34
|
+
import type { CapabilityPlan, CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
|
|
35
35
|
import type { WaveMember } from "../gated-waves";
|
|
36
36
|
import type { ChangeSetPart } from "../change-set";
|
|
37
37
|
|
|
@@ -101,12 +101,13 @@ export async function planWaveComponent(
|
|
|
101
101
|
const plans: Record<string, unknown> = {};
|
|
102
102
|
const parts: ChangeSetPart[] = [];
|
|
103
103
|
let outputs: PlannedOutputs | undefined;
|
|
104
|
+
assertTargetsAccepted(component, registry, ctx);
|
|
104
105
|
for (const step of capabilitySteps(component.deploy)) {
|
|
105
106
|
const capability = registry.resolve(step.kind);
|
|
106
107
|
if (!capability.plan) continue;
|
|
107
108
|
const { kind: _kind, ...rest } = step;
|
|
108
109
|
const input = resolveStepInput(rest, {}, componentOutputs);
|
|
109
|
-
const planned = await capability.plan(planContext(ctx, rest, componentOutputs), input as never);
|
|
110
|
+
const planned = bindTargets(step.kind, ctx, await capability.plan(planContext(ctx, rest, componentOutputs), input as never));
|
|
110
111
|
const member = { member: planned.member, planDigest: planned.planDigest };
|
|
111
112
|
members.push(member);
|
|
112
113
|
parts.push(planned.changeSet ?? emptyPart(member));
|
|
@@ -124,6 +125,7 @@ export async function planWaveComponent(
|
|
|
124
125
|
component,
|
|
125
126
|
...(ctx.vars ? { vars: ctx.vars } : {}),
|
|
126
127
|
inputs: read.inputs,
|
|
128
|
+
...(ctx.targets ? { targets: ctx.targets } : {}),
|
|
127
129
|
}),
|
|
128
130
|
};
|
|
129
131
|
members.push(member);
|
|
@@ -145,6 +147,39 @@ export async function planWaveComponent(
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A member's digest with its targets in it (#4300), the way terragucci's
|
|
152
|
+
* cone plan digest binds a cone: the digest over the plan's own digest and
|
|
153
|
+
* the sorted targets, so an approval of one narrowing covers no other.
|
|
154
|
+
*/
|
|
155
|
+
export function targetedPlanDigest(planDigest: string, targets: readonly string[]): string {
|
|
156
|
+
return computePlanDigest("capability-targets", { plan: planDigest, targets: [...new Set(targets)].sort() });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A step's plan made with `ctx.targets` (#4300), its digest bound to them.
|
|
161
|
+
* The plan must give back the targets it was asked for, which says it planned
|
|
162
|
+
* those alone; one that gives back others, or none, refuses. Without
|
|
163
|
+
* `ctx.targets`, the plan as it came.
|
|
164
|
+
*/
|
|
165
|
+
export function bindTargets(kind: string, ctx: DeployContext, planned: CapabilityPlan): CapabilityPlan {
|
|
166
|
+
const asked = ctx.targets;
|
|
167
|
+
if (!asked || asked.length === 0) return planned;
|
|
168
|
+
const want = [...new Set(asked)].sort();
|
|
169
|
+
const got = [...new Set(planned.targets ?? [])].sort();
|
|
170
|
+
if (got.length !== want.length || got.some((t, i) => t !== want[i])) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`component "${ctx.component}": ${kind} was asked to plan ${want.join(", ")} but its plan of ${planned.member} ` +
|
|
173
|
+
(got.length === 0 ? "names no targets" : `names ${got.join(", ")}`),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const planDigest = targetedPlanDigest(planned.planDigest, want);
|
|
177
|
+
const changeSet = planned.changeSet
|
|
178
|
+
? { ...planned.changeSet, member: { ...planned.changeSet.member, planDigest } }
|
|
179
|
+
: undefined;
|
|
180
|
+
return { ...planned, planDigest, targets: want, ...(changeSet ? { changeSet } : {}) };
|
|
181
|
+
}
|
|
182
|
+
|
|
148
183
|
/** `next` over `prior`, as a later step's outputs win over an earlier one's in a run. */
|
|
149
184
|
function mergePlannedOutputs(prior: PlannedOutputs | undefined, next: PlannedOutputs): PlannedOutputs {
|
|
150
185
|
const known = { ...(prior?.known ?? {}), ...next.known };
|
|
@@ -41,20 +41,21 @@ describe("the gate-decision bundle", () => {
|
|
|
41
41
|
"packages/core/src/intrinsic.ts",
|
|
42
42
|
"packages/core/src/lifecycle/gate-applied.ts",
|
|
43
43
|
"packages/core/src/lifecycle/gate-ledger-select.ts",
|
|
44
|
+
"packages/core/src/lifecycle/gate-seal.ts",
|
|
44
45
|
"packages/core/src/lifecycle/plan-digest.ts",
|
|
45
46
|
"packages/core/src/notify-verify.ts",
|
|
46
47
|
"packages/core/src/notify.ts",
|
|
47
48
|
"packages/core/src/op/gate-point.ts",
|
|
49
|
+
"packages/core/src/op/gate-quorum.ts",
|
|
48
50
|
"packages/core/src/op/op-waves.ts",
|
|
49
51
|
"packages/core/src/wave-layout.ts",
|
|
50
52
|
"packages/core/src/workspace/class-data.ts",
|
|
51
53
|
"packages/core/src/workspace/identity-gate-rule.ts",
|
|
52
54
|
"packages/core/src/workspace/jsonc.ts",
|
|
53
|
-
"packages/core/src/workspace/trust/gate-seal.ts",
|
|
54
55
|
"packages/core/src/workspace/trust/trust-files.ts",
|
|
55
56
|
]);
|
|
56
|
-
// About
|
|
57
|
+
// About 49 KB unminified today: 7 KB of it the pinned classes read as data (#3888), 6 KB the approval rules (#4290).
|
|
57
58
|
const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
|
|
58
|
-
expect(bytes).toBeLessThan(
|
|
59
|
+
expect(bytes).toBeLessThan(54 * 1024);
|
|
59
60
|
});
|
|
60
61
|
});
|
package/src/gate-decision.ts
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
* counts for nothing, as terragucci's wave gate has it: the caller hands in
|
|
28
28
|
* the lines of `_gates/<op>/applied.jsonl` ({@link parseAppliedGateLines}).
|
|
29
29
|
*
|
|
30
|
+
* Approval rules (#4290), a quorum as a list of `{ targets, count, roles,
|
|
31
|
+
* notAuthor }`, are judged with `./op/gate-quorum.ts`, re-exported here:
|
|
32
|
+
* {@link trustRolesAt} and {@link gateSignersAt} read what
|
|
33
|
+
* `trustQuorumJudge` judges by, at base.
|
|
34
|
+
*
|
|
30
35
|
* The module reads the ledger lines and the review it is handed; reading
|
|
31
36
|
* them is the caller's. It imports no TypeScript, esbuild, zod, ajv, lint,
|
|
32
37
|
* fold or codegen module, no package's code, and none of chant's git ledger
|
|
@@ -46,7 +51,7 @@ import {
|
|
|
46
51
|
} from "./lifecycle/gate-ledger-select";
|
|
47
52
|
import { samePlanDigest } from "./lifecycle/plan-digest";
|
|
48
53
|
import { isApprovalSpent, type AppliedGateRecord } from "./lifecycle/gate-applied";
|
|
49
|
-
import { gateSealRefusal, type GateSigners, type SealedGateApproval } from "./
|
|
54
|
+
import { gateSealRefusal, type GateSigners, type SealedGateApproval } from "./lifecycle/gate-seal";
|
|
50
55
|
import {
|
|
51
56
|
DEFAULT_SIGNERS_PATH,
|
|
52
57
|
TRUST_CONFIG_PATH,
|
|
@@ -79,7 +84,7 @@ export {
|
|
|
79
84
|
type GateSigners,
|
|
80
85
|
type SealedGateApproval,
|
|
81
86
|
type SshSignatureCheck,
|
|
82
|
-
} from "./
|
|
87
|
+
} from "./lifecycle/gate-seal";
|
|
83
88
|
export {
|
|
84
89
|
DEFAULT_SIGNERS_PATH,
|
|
85
90
|
TRUST_CONFIG_PATH,
|
|
@@ -105,6 +110,28 @@ export {
|
|
|
105
110
|
type AppliedGateRecord,
|
|
106
111
|
} from "./lifecycle/gate-applied";
|
|
107
112
|
export type { MergedReview } from "./op/gate-review";
|
|
113
|
+
export {
|
|
114
|
+
quorumNeed,
|
|
115
|
+
quorumProblems,
|
|
116
|
+
quorumRuleLine,
|
|
117
|
+
quorumRuleProgress,
|
|
118
|
+
quorumRules,
|
|
119
|
+
quorumRulesFor,
|
|
120
|
+
quorumWaiting,
|
|
121
|
+
rolesOf,
|
|
122
|
+
ruleNeedsProof,
|
|
123
|
+
samePrincipal,
|
|
124
|
+
targetGlobMatch,
|
|
125
|
+
trustQuorumJudge,
|
|
126
|
+
UNPROVEN_JUDGE,
|
|
127
|
+
type GateQuorumJudge,
|
|
128
|
+
type GateQuorumRule,
|
|
129
|
+
type GateQuorumRuleProgress,
|
|
130
|
+
type GateQuorumSpec,
|
|
131
|
+
type QuorumApprover,
|
|
132
|
+
type QuorumJudgement,
|
|
133
|
+
type TrustQuorumInput,
|
|
134
|
+
} from "./op/gate-quorum";
|
|
108
135
|
|
|
109
136
|
// ── reading at base ──────────────────────────────────────────────────────
|
|
110
137
|
|
|
@@ -172,6 +199,16 @@ export function gateSignersAt(repo: string, base: string): GateSigners {
|
|
|
172
199
|
return trustAt(repo, base).signers;
|
|
173
200
|
}
|
|
174
201
|
|
|
202
|
+
/**
|
|
203
|
+
* The role grants of `.chant/trust.json` at `base` (#4290): role name to the
|
|
204
|
+
* principals that hold it, as chant reads them. A missing file gives none,
|
|
205
|
+
* and so does one that is not valid, which turns chant's trust off.
|
|
206
|
+
* {@link trustQuorumJudge} takes them with {@link gateSignersAt}'s signers.
|
|
207
|
+
*/
|
|
208
|
+
export function trustRolesAt(repo: string, base: string): Record<string, string[]> {
|
|
209
|
+
return trustAt(repo, base).roles;
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
/** A gate the declaration at base names under `identity.gates`, and the principal class its signer must be in, if any. */
|
|
176
213
|
export interface IdentityGate {
|
|
177
214
|
gate: string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate policy bundles alone (#4294), as the override gate does: a
|
|
3
|
+
* product runs the engine and reads its answer from one bundled file with
|
|
4
|
+
* no TypeScript toolchain. This bundles `@intentius/chant/gate-policy` and
|
|
5
|
+
* reads esbuild's list of what went in.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { build } from "esbuild";
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { REPO } from "./workspace/__fixtures__/contract-repo";
|
|
12
|
+
|
|
13
|
+
describe("the gate-policy bundle", () => {
|
|
14
|
+
test("holds the policy module and nothing else", async () => {
|
|
15
|
+
const result = await build({
|
|
16
|
+
entryPoints: [join(REPO, "packages", "core", "src", "gate-policy.ts")],
|
|
17
|
+
bundle: true,
|
|
18
|
+
platform: "node",
|
|
19
|
+
format: "esm",
|
|
20
|
+
write: false,
|
|
21
|
+
metafile: true,
|
|
22
|
+
outdir: "out",
|
|
23
|
+
logLevel: "silent",
|
|
24
|
+
});
|
|
25
|
+
expect(Object.keys(result.metafile.inputs).map((i) => i.replace(/^.*?packages\//, "packages/"))).toEqual(["packages/core/src/gate-policy.ts"]);
|
|
26
|
+
expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(16 * 1024);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
assertGatePolicySettings,
|
|
6
|
+
checkGatePolicy,
|
|
7
|
+
conftestFindings,
|
|
8
|
+
exportGatePolicy,
|
|
9
|
+
gatePolicyArgv,
|
|
10
|
+
opaFindings,
|
|
11
|
+
type GatePolicyExec,
|
|
12
|
+
type GatePolicyTree,
|
|
13
|
+
} from "./gate-policy";
|
|
14
|
+
|
|
15
|
+
/** conftest's JSON for one input: a deny with its query, an older deny without one, and a warning. */
|
|
16
|
+
const CONFTEST_DENY = JSON.stringify([
|
|
17
|
+
{
|
|
18
|
+
filename: "input.json",
|
|
19
|
+
namespace: "main",
|
|
20
|
+
successes: 2,
|
|
21
|
+
failures: [
|
|
22
|
+
{ msg: "bucket logs is public", metadata: { query: "data.main.deny_public_bucket" } },
|
|
23
|
+
{ msg: { msg: "no owner tag" } },
|
|
24
|
+
],
|
|
25
|
+
warnings: [{ msg: "the plan deletes 3 resources" }],
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const opa = (value: unknown): string => JSON.stringify({ result: [{ expressions: [{ value, text: "data.main", location: { row: 1, col: 1 } }] }] });
|
|
30
|
+
|
|
31
|
+
describe("reading the engine's answer (#4294)", () => {
|
|
32
|
+
test("conftest: failures deny by their query, warnings are advice", () => {
|
|
33
|
+
expect(conftestFindings(CONFTEST_DENY)).toEqual({
|
|
34
|
+
denials: [
|
|
35
|
+
{ rule: "main.deny_public_bucket", message: "bucket logs is public" },
|
|
36
|
+
{ rule: "main.deny", message: "no owner tag" },
|
|
37
|
+
],
|
|
38
|
+
holds: [],
|
|
39
|
+
warnings: ["the plan deletes 3 resources"],
|
|
40
|
+
});
|
|
41
|
+
expect(conftestFindings(JSON.stringify([{ filename: "input.json", namespace: "main", successes: 1 }]))).toEqual({ denials: [], holds: [], warnings: [] });
|
|
42
|
+
expect(conftestFindings("not json")).toBeUndefined();
|
|
43
|
+
expect(conftestFindings(JSON.stringify({ result: [] }))).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("opa: deny, violation, hold and warn rules by name; a child package one level down; allow_unattended is no rule", () => {
|
|
47
|
+
const found = opaFindings(
|
|
48
|
+
opa({
|
|
49
|
+
deny: ["public bucket"],
|
|
50
|
+
violation_size: "too many deletes",
|
|
51
|
+
hold_iam: ["an IAM role changes"],
|
|
52
|
+
hold: true,
|
|
53
|
+
warn: ["consider tags"],
|
|
54
|
+
allow_unattended: true,
|
|
55
|
+
allow_unattended_docs: ["docs only"],
|
|
56
|
+
other: 3,
|
|
57
|
+
aws: { deny_region: ["us-west-1 is not allowed"] },
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
expect(found).toEqual({
|
|
61
|
+
denials: [
|
|
62
|
+
{ rule: "main.aws.deny_region", message: "us-west-1 is not allowed" },
|
|
63
|
+
{ rule: "main.deny", message: "public bucket" },
|
|
64
|
+
{ rule: "main.violation_size", message: "too many deletes" },
|
|
65
|
+
],
|
|
66
|
+
holds: [
|
|
67
|
+
{ rule: "main.hold", message: "hold" },
|
|
68
|
+
{ rule: "main.hold_iam", message: "an IAM role changes" },
|
|
69
|
+
],
|
|
70
|
+
warnings: ["consider tags"],
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("opa: an undefined query matches nothing, an array is a deny set, errors and other shapes are no answer", () => {
|
|
75
|
+
expect(opaFindings(JSON.stringify({}))).toEqual({ denials: [], holds: [], warnings: [] });
|
|
76
|
+
expect(opaFindings(opa(["a", { msg: "b" }]), "policy.deny")).toEqual({ denials: [{ rule: "policy.deny", message: "a" }, { rule: "policy.deny", message: "b" }], holds: [], warnings: [] });
|
|
77
|
+
expect(opaFindings(JSON.stringify({ errors: [{ message: "rego_parse_error" }] }))).toBeUndefined();
|
|
78
|
+
expect(opaFindings(opa(7))).toBeUndefined();
|
|
79
|
+
expect(opaFindings("{")).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("running the engine (#4294)", () => {
|
|
84
|
+
const fake = (out: { status: number | null; stdout?: string; stderr?: string }, seen: string[][] = []): GatePolicyExec => (argv) => {
|
|
85
|
+
seen.push(argv);
|
|
86
|
+
return { status: out.status, stdout: out.stdout ?? "", stderr: out.stderr ?? "" };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
test("the engines' own argv, and a command's placeholders filled", () => {
|
|
90
|
+
expect(gatePolicyArgv({}, { policy: "/p", input: "/i.json" })).toEqual(["conftest", "test", "--no-color", "--output", "json", "--policy", "/p", "--all-namespaces", "/i.json"]);
|
|
91
|
+
expect(gatePolicyArgv({ engine: "conftest", namespace: "infra" }, { policy: "/p", input: "/i.json" })).toContain("--namespace");
|
|
92
|
+
expect(gatePolicyArgv({ engine: "opa" }, { policy: "/p", input: "/i.json" })).toEqual(["opa", "eval", "--format", "json", "--data", "/p", "--input", "/i.json", "data.main"]);
|
|
93
|
+
expect(gatePolicyArgv({ engine: "opa", command: ["./bin/check", "--rules={policy}", "{input}"] }, { policy: "/p", input: "/i.json" })).toEqual(["./bin/check", "--rules=/p", "/i.json"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("the input is written to the file the engine reads, and a denial is a verdict with its sorted rules", () => {
|
|
97
|
+
let input: unknown;
|
|
98
|
+
const exec: GatePolicyExec = (argv) => {
|
|
99
|
+
input = JSON.parse(readFileSync(argv[argv.length - 1]!, "utf-8"));
|
|
100
|
+
return { status: 1, stdout: CONFTEST_DENY, stderr: "" };
|
|
101
|
+
};
|
|
102
|
+
const verdict = checkGatePolicy({ settings: {}, input: { changeSet: { entries: [] } }, policyDir: "/p", exec });
|
|
103
|
+
expect(input).toEqual({ changeSet: { entries: [] } });
|
|
104
|
+
expect(verdict).toMatchObject({ result: "denied", engine: "conftest", rules: ["main.deny", "main.deny_public_bucket"], warnings: ["the plan deletes 3 resources"] });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a hold with no denial is held; nothing is passed", () => {
|
|
108
|
+
expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ hold_iam: ["iam"] }) }) })).toMatchObject({ result: "held", holds: [{ rule: "main.hold_iam" }] });
|
|
109
|
+
expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ deny: [], warn: [] }) }) })).toMatchObject({ result: "passed", denials: [], holds: [] });
|
|
110
|
+
expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ deny: ["x"], hold: true }) }) }).result).toBe("denied");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("an engine that does not start, prints no result or fails without a denial is an error", () => {
|
|
114
|
+
expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: null, stderr: "spawn conftest ENOENT" }) })).toMatchObject({ result: "error", error: expect.stringContaining("did not run") });
|
|
115
|
+
expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: 2, stdout: "Error: rego_parse_error" }) })).toMatchObject({ result: "error", error: expect.stringContaining("gave no conftest result") });
|
|
116
|
+
expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: 2, stdout: "[]", stderr: "could not load policy" }) })).toMatchObject({ result: "error", error: expect.stringContaining("exited 2") });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("the policy at base (#4294)", () => {
|
|
121
|
+
const files: Record<string, string> = { "policy/main.rego": "package main\n", "policy/lib/util.rego": "package lib\n", "src/app.ts": "" };
|
|
122
|
+
const tree: GatePolicyTree = {
|
|
123
|
+
stat: (p) => (files[p] !== undefined ? "file" : Object.keys(files).some((f) => f.startsWith(`${p}/`)) ? "dir" : undefined),
|
|
124
|
+
list: (p) => {
|
|
125
|
+
const names = new Map<string, "file" | "dir">();
|
|
126
|
+
for (const f of Object.keys(files).filter((f) => f.startsWith(`${p}/`))) {
|
|
127
|
+
const rest = f.slice(p.length + 1).split("/");
|
|
128
|
+
names.set(rest[0]!, rest.length > 1 ? "dir" : "file");
|
|
129
|
+
}
|
|
130
|
+
return names.size ? [...names].map(([name, type]) => ({ name, type })) : undefined;
|
|
131
|
+
},
|
|
132
|
+
read: (p) => files[p]!,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
test("the directory is copied out of the tree, and removed again", () => {
|
|
136
|
+
const out = exportGatePolicy(tree, "./policy/");
|
|
137
|
+
if ("error" in out) throw new Error(out.error);
|
|
138
|
+
expect(readFileSync(join(out.dir, "main.rego"), "utf-8")).toBe("package main\n");
|
|
139
|
+
expect(readFileSync(join(out.dir, "lib", "util.rego"), "utf-8")).toBe("package lib\n");
|
|
140
|
+
expect(existsSync(join(out.dir, "app.ts"))).toBe(false);
|
|
141
|
+
out.cleanup();
|
|
142
|
+
expect(existsSync(out.dir)).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a directory the base does not have is an error", () => {
|
|
146
|
+
expect(exportGatePolicy(tree, "rules")).toEqual({ error: "the policy directory rules does not exist at base" });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("ci.policy settings (#4294)", () => {
|
|
151
|
+
test("the four settings are read, and an unattended outcome is refused", () => {
|
|
152
|
+
expect(() => assertGatePolicySettings("ci.policy", { engine: "opa", path: "policy", namespace: "main", command: ["opa", "eval", "{input}"] })).not.toThrow();
|
|
153
|
+
expect(() => assertGatePolicySettings("ci.policy", {})).not.toThrow();
|
|
154
|
+
expect(() => assertGatePolicySettings("ci.policy", { engine: "sentinel" })).toThrow(/conftest or opa/);
|
|
155
|
+
expect(() => assertGatePolicySettings("ci.policy", { path: "../elsewhere" })).toThrow(/inside the workspace/);
|
|
156
|
+
expect(() => assertGatePolicySettings("ci.policy", { command: ["check"] })).toThrow(/\{input\}/);
|
|
157
|
+
expect(() => assertGatePolicySettings("ci.policy", { outcomes: ["deny", "allow_unattended"] })).toThrow(/no unattended outcome/);
|
|
158
|
+
expect(() => assertGatePolicySettings("ci.policy", { allow_unattended: true })).toThrow(/no unattended outcome/);
|
|
159
|
+
expect(() => assertGatePolicySettings("ci.policy", { rules: [] })).toThrow(/not a setting/);
|
|
160
|
+
expect(() => assertGatePolicySettings("ci.policy", [])).toThrow(/must be/);
|
|
161
|
+
});
|
|
162
|
+
});
|