@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
package/dist/op/op-waves.js
CHANGED
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
* `chant/lifecycle`, as terragucci's wave gate does, and that approval is
|
|
44
44
|
* spent: the same digest needs a new approval to apply again.
|
|
45
45
|
*
|
|
46
|
+
* ## Approval rules (#4290)
|
|
47
|
+
*
|
|
48
|
+
* With `approvals: { quorum: [...] }` (read at base), a wave takes each rule
|
|
49
|
+
* whose `targets` glob matches one of its runs' targets and waits until each
|
|
50
|
+
* is met: `count` distinct people, from `roles` when it names them (granted
|
|
51
|
+
* in `.chant/trust.json` at base), one of them not the author with
|
|
52
|
+
* `notAuthor`. See `./gate-quorum.ts`.
|
|
53
|
+
*
|
|
46
54
|
* ## Wave-level commands and hooks (#3762)
|
|
47
55
|
*
|
|
48
56
|
* A product whose tool plans a whole wave in one call (Terragrunt's `run
|
|
@@ -66,6 +74,7 @@
|
|
|
66
74
|
* the wave (exit 8). See `./op-waves-run.ts`.
|
|
67
75
|
*/
|
|
68
76
|
import { gatePointProblems } from "./gate-point.js";
|
|
77
|
+
import { quorumProblems } from "./gate-quorum.js";
|
|
69
78
|
import { notifyConfigProblem } from "../notify.js";
|
|
70
79
|
import { WAVE_GATE_NAME_PATTERN, waveGateNameFrom, waveShare } from "../wave-layout.js";
|
|
71
80
|
/**
|
|
@@ -102,11 +111,13 @@ export function opWavePointAt(spec, name) {
|
|
|
102
111
|
}
|
|
103
112
|
export const OP_WAVE_HOOK_FAILURES = ["fail", "approve"];
|
|
104
113
|
/**
|
|
105
|
-
* Every moment, in the order a wave reaches them. `
|
|
114
|
+
* Every moment, in the order a wave reaches them. `before-observe` (#4288)
|
|
115
|
+
* runs before anything reads live state: ahead of `before-plan` in every job
|
|
116
|
+
* that plans, the deciding job's and each share's. `after-gate` (#3821) runs
|
|
106
117
|
* once the gate decided, whatever it decided: approved, let through,
|
|
107
118
|
* waiting, or refused by a policy denial.
|
|
108
119
|
*/
|
|
109
|
-
export const OP_WAVE_HOOK_MOMENTS = ["before-plan", "after-plan", "after-gate", "before-apply", "after-apply"];
|
|
120
|
+
export const OP_WAVE_HOOK_MOMENTS = ["before-observe", "before-plan", "after-plan", "after-gate", "before-apply", "after-apply"];
|
|
110
121
|
export const OP_WAVE_APPLY_HOOKS_RUNNERS = ["chant", "apply"];
|
|
111
122
|
export const OP_WAVES_DIGEST_OVER = ["all", "changing"];
|
|
112
123
|
/** The default gate name pattern. */
|
|
@@ -171,13 +182,25 @@ export function assertOpWavesSpec(spec) {
|
|
|
171
182
|
if (spec.approvals !== undefined) {
|
|
172
183
|
const a = spec.approvals;
|
|
173
184
|
if (a === null || typeof a !== "object" || Array.isArray(a))
|
|
174
|
-
throw new Error(`${where}: \`approvals\` must be { "spend"?: true | false }.`);
|
|
185
|
+
throw new Error(`${where}: \`approvals\` must be { "spend"?: true | false, "quorum"?: [...] }.`);
|
|
175
186
|
for (const [key, value] of Object.entries(a)) {
|
|
187
|
+
if (key === "quorum") {
|
|
188
|
+
// #4290: approval rules, read at base like the policy.
|
|
189
|
+
if (!Array.isArray(value))
|
|
190
|
+
throw new Error(`${where}: approvals.quorum must be a list of rules, each { "targets"?, "count", "roles"?, "notAuthor"? }.`);
|
|
191
|
+
const problems = quorumProblems(value, "approvals.quorum", { notAuthor: true });
|
|
192
|
+
if (problems.length > 0)
|
|
193
|
+
throw new Error(`${where}: ${problems.join("; ")}.`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
176
196
|
if (key !== "spend")
|
|
177
|
-
throw new Error(`${where}: approvals.${key} is not a setting; the
|
|
197
|
+
throw new Error(`${where}: approvals.${key} is not a setting; the ones there are approvals.spend and approvals.quorum.`);
|
|
178
198
|
if (typeof value !== "boolean")
|
|
179
199
|
throw new Error(`${where}: approvals.spend must be true or false.`);
|
|
180
200
|
}
|
|
201
|
+
if (a.quorum?.length && (spec.point !== undefined || spec.waves.some((w) => w.point !== undefined))) {
|
|
202
|
+
throw new Error(`${where}: approvals.quorum and a decision point are both set; a gate that asks a point takes its deciders and quorum from the point, so drop one of them.`);
|
|
203
|
+
}
|
|
181
204
|
}
|
|
182
205
|
if (spec.gateName !== undefined) {
|
|
183
206
|
if (typeof spec.gateName !== "string" || !spec.gateName.includes("{k}") || /\s/.test(spec.gateName)) {
|
|
@@ -394,6 +417,15 @@ export function opWaveRunMatches(pattern, target) {
|
|
|
394
417
|
}
|
|
395
418
|
return new RegExp(`^${re}$`).test(target);
|
|
396
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* The runs of `runs` hook `hook` is for (#4288): those its `targets` match,
|
|
422
|
+
* or every run when it names none.
|
|
423
|
+
*/
|
|
424
|
+
export function opWaveHookRuns(hook, runs) {
|
|
425
|
+
if (!hook.targets)
|
|
426
|
+
return [...runs];
|
|
427
|
+
return runs.filter((run) => hook.targets.some((pattern) => opWaveRunMatches(pattern, run.target)));
|
|
428
|
+
}
|
|
397
429
|
/** The runs of `runs` that job group `group` holds (#3914). */
|
|
398
430
|
export function opWaveGroupRuns(group, runs) {
|
|
399
431
|
return runs.filter((run) => group.runs.some((pattern) => opWaveRunMatches(pattern, run.target)));
|
|
@@ -422,7 +454,7 @@ export function assertOpWaveHooks(where, hooks) {
|
|
|
422
454
|
if (hooks === undefined)
|
|
423
455
|
return;
|
|
424
456
|
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
425
|
-
throw new Error(`${where}: \`hooks\` must be { "before"?: { "plan"?, "apply"? }, "after"?: { "plan"?, "apply"? } }.`);
|
|
457
|
+
throw new Error(`${where}: \`hooks\` must be { "before"?: { "observe"?, "plan"?, "apply"? }, "after"?: { "plan"?, "gate"?, "apply"? } }.`);
|
|
426
458
|
}
|
|
427
459
|
for (const [side, stages] of Object.entries(hooks)) {
|
|
428
460
|
if (side === "apply") {
|
|
@@ -443,8 +475,10 @@ export function assertOpWaveHooks(where, hooks) {
|
|
|
443
475
|
const at = `hooks.${side}.${stage}`;
|
|
444
476
|
if (stage === "gate" && side === "before")
|
|
445
477
|
throw new Error(`${where}: ${at} is not a moment; a hook runs after the gate (hooks.after.gate), or before the plan or the apply.`);
|
|
446
|
-
if (stage
|
|
447
|
-
throw new Error(`${where}: ${at} is not a
|
|
478
|
+
if (stage === "observe" && side === "after")
|
|
479
|
+
throw new Error(`${where}: ${at} is not a moment; a hook runs before the observe (hooks.before.observe), or after the plan, which reads what the observe saw.`);
|
|
480
|
+
if (stage !== "observe" && stage !== "plan" && stage !== "apply" && stage !== "gate")
|
|
481
|
+
throw new Error(`${where}: ${at} is not a stage; a hook runs before the observe, before or after plan or apply, or after the gate.`);
|
|
448
482
|
if (!Array.isArray(list))
|
|
449
483
|
throw new Error(`${where}: ${at} must be a list of hooks.`);
|
|
450
484
|
list.forEach((hook, i) => {
|
|
@@ -463,18 +497,39 @@ export function assertOpWaveHooks(where, hooks) {
|
|
|
463
497
|
}
|
|
464
498
|
if (hook.perRun !== undefined && typeof hook.perRun !== "boolean")
|
|
465
499
|
throw new Error(`${where}: ${which}.perRun must be true or false.`);
|
|
500
|
+
if (hook.targets !== undefined && (!Array.isArray(hook.targets) || hook.targets.length === 0 || !hook.targets.every((t) => typeof t === "string" && t !== ""))) {
|
|
501
|
+
throw new Error(`${where}: ${which}.targets must be a non-empty list of targets or patterns, such as ["prod/*"].`);
|
|
502
|
+
}
|
|
466
503
|
if (!hook.perRun) {
|
|
467
504
|
const named = hook.command.find((part) => part.includes("{target}") || part.includes("{plan}"));
|
|
468
505
|
if (named)
|
|
469
506
|
throw new Error(`${where}: ${which} runs once for the wave, so it has no {target} or {plan} ("${named}"). Set "perRun": true, or read the runs from {runs}.`);
|
|
470
507
|
}
|
|
471
|
-
else if (side === "before" && stage === "plan" && hook.command.some((part) => part.includes("{plan}"))) {
|
|
508
|
+
else if (side === "before" && (stage === "plan" || stage === "observe") && hook.command.some((part) => part.includes("{plan}"))) {
|
|
472
509
|
throw new Error(`${where}: ${which} runs before the plan, so it has no {plan} yet.`);
|
|
473
510
|
}
|
|
474
511
|
});
|
|
475
512
|
}
|
|
476
513
|
}
|
|
477
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* The moments a pull-request job runs hooks at (#4288): `pr-plan` and
|
|
517
|
+
* `pr-record` run the observe and plan moments, and `pr-apply` the apply
|
|
518
|
+
* moments too. The pull request's gate has no `after-gate` moment.
|
|
519
|
+
*/
|
|
520
|
+
export const PR_LOOP_HOOK_MOMENTS = ["before-observe", "before-plan", "after-plan", "before-apply", "after-apply"];
|
|
521
|
+
/**
|
|
522
|
+
* Refuse `ci.hooks` a pull-request job could not run (#4288): what
|
|
523
|
+
* {@link assertOpWaveHooks} refuses, plus `after.gate` and `apply.runBy`,
|
|
524
|
+
* since chant itself runs a pull request's apply.
|
|
525
|
+
*/
|
|
526
|
+
export function assertPrLoopHooks(where, hooks) {
|
|
527
|
+
assertOpWaveHooks(where, hooks);
|
|
528
|
+
if (hooks?.after?.gate !== undefined)
|
|
529
|
+
throw new Error(`${where}: hooks.after.gate runs in an Op waves job only; a pull-request job runs hooks before the observe, before and after the plan, and before and after the apply.`);
|
|
530
|
+
if (hooks?.apply !== undefined)
|
|
531
|
+
throw new Error(`${where}: hooks.apply.runBy is for an Op waves spec; chant runs a pull request's apply, so it runs the apply-stage hooks itself.`);
|
|
532
|
+
}
|
|
478
533
|
/** The hooks of each moment for wave `name`: the spec's, then the wave's own. */
|
|
479
534
|
export function opWaveHooksAt(spec, name) {
|
|
480
535
|
const wave = spec.waves.find((w) => w.name === name);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { OpConfig } from "./types.js";
|
|
2
|
+
import type { ForgeKind, PrForge, ScheduledRuns } from "../pr-forge.js";
|
|
3
|
+
/** One scheduled watch or reconcile Op the plan job checks. */
|
|
4
|
+
export interface ScheduledWatch {
|
|
5
|
+
op: string;
|
|
6
|
+
cron: string;
|
|
7
|
+
}
|
|
8
|
+
/** An Op the plan note warns about. */
|
|
9
|
+
export interface OverdueOp {
|
|
10
|
+
op: string;
|
|
11
|
+
cron: string;
|
|
12
|
+
/** The time the count starts from: the job's last run, or the pipeline file's first commit (ISO 8601). */
|
|
13
|
+
since: string;
|
|
14
|
+
/** True when no run is on record and `since` is the pipeline file's first commit. */
|
|
15
|
+
neverRan?: boolean;
|
|
16
|
+
/** The line for the job log and the plan note. */
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The discovered Ops that are scheduled watch or reconcile Ops: a
|
|
21
|
+
* `schedule`, and the `Watch` label `WatchOp` sets (./composites/watch-op.ts)
|
|
22
|
+
* or the `Reconcile` label `ReconcileOp` sets (./composites/reconcile-op.ts).
|
|
23
|
+
* Sorted by name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scheduledWatches(ops: Iterable<OpConfig>): ScheduledWatch[];
|
|
26
|
+
/**
|
|
27
|
+
* The file `chant run --generate <forge>` writes for an Op, relative to the
|
|
28
|
+
* repository root: `<dir>/<op>.yml` on GitHub and Forgejo, `<member>-<op>.yml`
|
|
29
|
+
* in a workspace member. GitLab puts every Op in one file, so the root's
|
|
30
|
+
* `*.gitlab-ci.yml` files are read for the one whose rules select the Op.
|
|
31
|
+
* Undefined when the checkout has no such file, so an Op that some other
|
|
32
|
+
* scheduler runs is never checked.
|
|
33
|
+
*/
|
|
34
|
+
export declare function opPipelineFile(repoRoot: string, forge: ForgeKind, op: string, member?: string): string | undefined;
|
|
35
|
+
/** When `path` was first committed (ISO 8601), if the checkout's history has it. */
|
|
36
|
+
export declare function fileAdded(repoRoot: string, path: string): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Whether one Op is overdue: its cron has fired at least twice since its
|
|
39
|
+
* last run, or since `added` when no run is on record. Undefined when it is
|
|
40
|
+
* on time, or when nothing says when it should have run.
|
|
41
|
+
*/
|
|
42
|
+
export declare function overdueVerdict(watch: ScheduledWatch, forge: ForgeKind, runs: ScheduledRuns, added: string | undefined, now: Date): OverdueOp | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The plan job's check over every scheduled watch or reconcile Op that has a
|
|
45
|
+
* generated pipeline file. Each Op the forge will not answer for is a line
|
|
46
|
+
* in `log` and no verdict. A forge client with no `scheduledRuns` checks
|
|
47
|
+
* nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkOverdueOps(input: {
|
|
50
|
+
watches: readonly ScheduledWatch[];
|
|
51
|
+
forge: PrForge;
|
|
52
|
+
repoRoot: string;
|
|
53
|
+
member?: string;
|
|
54
|
+
now: Date;
|
|
55
|
+
log: (line: string) => void;
|
|
56
|
+
}): Promise<OverdueOp[]>;
|
|
57
|
+
//# sourceMappingURL=schedule-overdue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule-overdue.d.ts","sourceRoot":"","sources":["../../src/op/schedule-overdue.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAErE,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,0GAA0G;IAC1G,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,cAAc,EAAE,CAQ1E;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBlH;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI5E;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,CAkBxJ;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBvB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled watch and reconcile Ops that stopped running (#4289).
|
|
3
|
+
*
|
|
4
|
+
* A schedule can stop with no error anywhere. GitHub turns a scheduled
|
|
5
|
+
* workflow off after 60 days with no activity in the repository and drops
|
|
6
|
+
* scheduled runs under load. A GitLab schedule exists only once someone
|
|
7
|
+
* creates it. A watch Op cannot report its own absence, so the pull-request
|
|
8
|
+
* plan job does. For each discovered Op with a `schedule` and the `Watch` or
|
|
9
|
+
* `Reconcile` label, whose generated pipeline file is in the checkout, it
|
|
10
|
+
* asks the forge when the Op's job last ran. When the cron has fired at
|
|
11
|
+
* least twice since then, or since the pipeline file was added for a job
|
|
12
|
+
* that never ran, the job log and the plan note say the Op is overdue.
|
|
13
|
+
*
|
|
14
|
+
* One late or dropped run is not enough: two fires are. A forge that will
|
|
15
|
+
* not answer is a line in the log and no verdict.
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { cronFiresUtc } from "./cron.js";
|
|
21
|
+
/**
|
|
22
|
+
* The discovered Ops that are scheduled watch or reconcile Ops: a
|
|
23
|
+
* `schedule`, and the `Watch` label `WatchOp` sets (./composites/watch-op.ts)
|
|
24
|
+
* or the `Reconcile` label `ReconcileOp` sets (./composites/reconcile-op.ts).
|
|
25
|
+
* Sorted by name.
|
|
26
|
+
*/
|
|
27
|
+
export function scheduledWatches(ops) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const op of ops) {
|
|
30
|
+
if (!op.schedule?.cron)
|
|
31
|
+
continue;
|
|
32
|
+
if (op.labels?.Watch !== "true" && op.labels?.Reconcile !== "true")
|
|
33
|
+
continue;
|
|
34
|
+
out.push({ op: op.name, cron: op.schedule.cron });
|
|
35
|
+
}
|
|
36
|
+
return out.sort((a, b) => a.op.localeCompare(b.op));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The file `chant run --generate <forge>` writes for an Op, relative to the
|
|
40
|
+
* repository root: `<dir>/<op>.yml` on GitHub and Forgejo, `<member>-<op>.yml`
|
|
41
|
+
* in a workspace member. GitLab puts every Op in one file, so the root's
|
|
42
|
+
* `*.gitlab-ci.yml` files are read for the one whose rules select the Op.
|
|
43
|
+
* Undefined when the checkout has no such file, so an Op that some other
|
|
44
|
+
* scheduler runs is never checked.
|
|
45
|
+
*/
|
|
46
|
+
export function opPipelineFile(repoRoot, forge, op, member) {
|
|
47
|
+
const base = member ? `${member}-${op}` : op;
|
|
48
|
+
if (forge === "github" || forge === "forgejo") {
|
|
49
|
+
const path = `.${forge}/workflows/${base}.yml`;
|
|
50
|
+
return existsSync(join(repoRoot, path)) ? path : undefined;
|
|
51
|
+
}
|
|
52
|
+
// The gitlab lexicon's cron rule: `$CHANT_SCHEDULED_OP == "<op>"`.
|
|
53
|
+
const selector = `$CHANT_SCHEDULED_OP == "${op}"`;
|
|
54
|
+
let names;
|
|
55
|
+
try {
|
|
56
|
+
names = readdirSync(repoRoot).filter((n) => n.endsWith(".gitlab-ci.yml")).sort();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return names.find((n) => {
|
|
62
|
+
try {
|
|
63
|
+
return readFileSync(join(repoRoot, n), "utf8").includes(selector);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** When `path` was first committed (ISO 8601), if the checkout's history has it. */
|
|
71
|
+
export function fileAdded(repoRoot, path) {
|
|
72
|
+
const r = spawnSync("git", ["-C", repoRoot, "log", "--format=%cI", "--diff-filter=A", "--", path], { encoding: "utf-8" });
|
|
73
|
+
if (r.status !== 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
return r.stdout.trim().split("\n").filter(Boolean).pop();
|
|
76
|
+
}
|
|
77
|
+
const HINT = {
|
|
78
|
+
github: "GitHub turns a scheduled workflow off after 60 days with no activity in the repository. Turn the workflow on again under Actions, or run it by hand",
|
|
79
|
+
gitlab: "A GitLab project runs a scheduled Op only from a pipeline schedule. Check its schedule under CI/CD > Schedules",
|
|
80
|
+
forgejo: "Check that Actions is on for the repository and a runner is up, or run the workflow by hand",
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Whether one Op is overdue: its cron has fired at least twice since its
|
|
84
|
+
* last run, or since `added` when no run is on record. Undefined when it is
|
|
85
|
+
* on time, or when nothing says when it should have run.
|
|
86
|
+
*/
|
|
87
|
+
export function overdueVerdict(watch, forge, runs, added, now) {
|
|
88
|
+
const since = runs.last ?? added;
|
|
89
|
+
if (!since)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (cronFiresUtc(watch.cron, new Date(since), now, 2).length < 2)
|
|
92
|
+
return undefined;
|
|
93
|
+
const day = since.slice(0, 10);
|
|
94
|
+
const what = runs.last ? `its last run was on ${day}` : `no run is on record since its pipeline was added on ${day}`;
|
|
95
|
+
const why = runs.disabled
|
|
96
|
+
? `GitHub has turned the workflow off (${runs.disabled}). Turn it on again under Actions`
|
|
97
|
+
: forge === "gitlab" && runs.noSchedule
|
|
98
|
+
? `No active pipeline schedule sets CHANT_SCHEDULED_OP to ${watch.op}. Add one with the cron \`${watch.cron}\` under CI/CD > Schedules`
|
|
99
|
+
: HINT[forge];
|
|
100
|
+
return {
|
|
101
|
+
op: watch.op,
|
|
102
|
+
cron: watch.cron,
|
|
103
|
+
since,
|
|
104
|
+
...(runs.last ? {} : { neverRan: true }),
|
|
105
|
+
message: `Op \`${watch.op}\` is overdue: its schedule \`${watch.cron}\` has fired at least twice and ${what}. ${why}.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The plan job's check over every scheduled watch or reconcile Op that has a
|
|
110
|
+
* generated pipeline file. Each Op the forge will not answer for is a line
|
|
111
|
+
* in `log` and no verdict. A forge client with no `scheduledRuns` checks
|
|
112
|
+
* nothing.
|
|
113
|
+
*/
|
|
114
|
+
export async function checkOverdueOps(input) {
|
|
115
|
+
const { forge } = input;
|
|
116
|
+
if (!forge.scheduledRuns)
|
|
117
|
+
return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const watch of input.watches) {
|
|
120
|
+
const file = opPipelineFile(input.repoRoot, forge.kind, watch.op, input.member);
|
|
121
|
+
if (!file)
|
|
122
|
+
continue;
|
|
123
|
+
let runs;
|
|
124
|
+
try {
|
|
125
|
+
runs = await forge.scheduledRuns({ op: watch.op, workflow: file.split("/").pop() });
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
input.log(`schedule check: ${forge.kind} did not say when Op ${watch.op} last ran, so it is not checked: ${(err instanceof Error ? err.message : String(err)).split("\n")[0]}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const found = overdueVerdict(watch, forge.kind, runs, runs.last ? undefined : fileAdded(input.repoRoot, file), input.now);
|
|
132
|
+
if (found) {
|
|
133
|
+
input.log(found.message);
|
|
134
|
+
out.push(found);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage-hook runner (#4288): one moment's hooks, with the `targets`,
|
|
3
|
+
* `perRun`, `onFailure` and argv filling a wave job uses. Its own module so
|
|
4
|
+
* a product that runs hooks around its own stages (terragucci) bundles it
|
|
5
|
+
* without the wave loop, the gate or the ledger. `op-waves-run.ts` runs
|
|
6
|
+
* the same hooks inside a wave job.
|
|
7
|
+
*/
|
|
8
|
+
import { type OpWaveHook, type OpWaveHookMoment } from "./op-waves.js";
|
|
9
|
+
/** What one moment's hooks came to, before any hold on another gate is settled. */
|
|
10
|
+
export interface HookListOutcome {
|
|
11
|
+
/** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
|
|
12
|
+
failed?: string;
|
|
13
|
+
/** The `onFailure: approve` hooks that failed. */
|
|
14
|
+
holds: string[];
|
|
15
|
+
outcomes: OpWaveHookOutcome[];
|
|
16
|
+
/** The hook calls that held the wave on another gate. */
|
|
17
|
+
otherGates: OtherGateHold[];
|
|
18
|
+
}
|
|
19
|
+
/** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
|
|
20
|
+
export declare const OP_WAVE_PENDING_RESERVED: readonly ["version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume", "environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy"];
|
|
21
|
+
/** What a hook asks for when it holds a wave on another gate (#3821): it writes this to `CHANT_HOLD` and exits non-zero. */
|
|
22
|
+
export interface OpWaveHoldRequest {
|
|
23
|
+
/** The op the gate is recorded under. Default: the spec's name. */
|
|
24
|
+
op?: string;
|
|
25
|
+
gate: string;
|
|
26
|
+
/** The digest an approval binds. */
|
|
27
|
+
digest: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Fields for the pending fact, as a plans file's `pending`. */
|
|
30
|
+
pending?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/** One hook's run (#3762). */
|
|
33
|
+
export interface OpWaveHookOutcome {
|
|
34
|
+
moment: OpWaveHookMoment;
|
|
35
|
+
name: string;
|
|
36
|
+
/** The run, for a `perRun` hook. */
|
|
37
|
+
target?: string;
|
|
38
|
+
exit: number;
|
|
39
|
+
outcome: "passed" | "failed" | "held" | "skipped";
|
|
40
|
+
/** Why the moment was skipped, for `skipped` (#3811). */
|
|
41
|
+
reason?: string;
|
|
42
|
+
}
|
|
43
|
+
/** One run as a hook sees it (#3804). */
|
|
44
|
+
export interface OpWaveHookRun {
|
|
45
|
+
target: string;
|
|
46
|
+
op: string;
|
|
47
|
+
/** The run's plan file, once planned. */
|
|
48
|
+
plan?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Runs one argv; returns its exit code. `env` is added to the job's environment. Injected so tests run no processes. */
|
|
51
|
+
export type OpWaveExec = (argv: string[], cwd: string, env?: Record<string, string>) => number;
|
|
52
|
+
export declare const spawnExec: OpWaveExec;
|
|
53
|
+
/** Fill one argv template. A placeholder whose value is not given is left as it is. */
|
|
54
|
+
export declare function opWaveArgv(template: readonly string[], values: {
|
|
55
|
+
op: string;
|
|
56
|
+
wave: string;
|
|
57
|
+
target?: string;
|
|
58
|
+
plan?: string;
|
|
59
|
+
runs?: string;
|
|
60
|
+
plans?: string;
|
|
61
|
+
}): string[];
|
|
62
|
+
/** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
|
|
63
|
+
export declare function pendingFields(value: unknown, where: string): Record<string, unknown> | undefined;
|
|
64
|
+
/** A hook call that held the wave on another gate (#3821), kept so it can run again once that gate is approved. */
|
|
65
|
+
export interface OtherGateHold {
|
|
66
|
+
label: string;
|
|
67
|
+
name: string;
|
|
68
|
+
moment: OpWaveHookMoment;
|
|
69
|
+
target?: string;
|
|
70
|
+
argv: string[];
|
|
71
|
+
env: Record<string, string>;
|
|
72
|
+
request: OpWaveHoldRequest;
|
|
73
|
+
}
|
|
74
|
+
/** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
|
|
75
|
+
export declare function readHoldRequest(path: string, log: (line: string) => void, label: string): OpWaveHoldRequest | undefined;
|
|
76
|
+
/** Where hooks run: the wave they belong to and the job's tools. */
|
|
77
|
+
export interface HookPlace {
|
|
78
|
+
/** The spec's name and `op`. */
|
|
79
|
+
name: string;
|
|
80
|
+
op: string;
|
|
81
|
+
waveName: string;
|
|
82
|
+
k: number;
|
|
83
|
+
cwd: string;
|
|
84
|
+
exec: OpWaveExec;
|
|
85
|
+
log: (line: string) => void;
|
|
86
|
+
/** Where a hook writes a hold on another gate (`CHANT_HOLD`). Default: under the wave's directory. */
|
|
87
|
+
holdPath?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run one moment's hooks, in order: each once for the wave, or once per run
|
|
91
|
+
* for a `perRun` hook. A hook that fails stops the rest, unless its
|
|
92
|
+
* `onFailure` is `approve`: then it holds the wave and the hooks after it run.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runHookList(at: HookPlace, moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, runs: readonly OpWaveHookRun[], runsPath: string, extraEnv?: Record<string, string>): HookListOutcome;
|
|
95
|
+
/** The options of {@link runStageHooks} (#4288). */
|
|
96
|
+
export interface RunStageHooksOptions {
|
|
97
|
+
/** What the hooks belong to: `CHANT_OP_WAVES`, and the `{op}` of a hook that runs once. */
|
|
98
|
+
name: string;
|
|
99
|
+
op: string;
|
|
100
|
+
/** `{wave}` and `CHANT_WAVE`. */
|
|
101
|
+
wave: string;
|
|
102
|
+
/** `CHANT_WAVE_NUMBER`. Default 0. */
|
|
103
|
+
k?: number;
|
|
104
|
+
/** The runs a `perRun` hook runs for and `targets` match against. */
|
|
105
|
+
runs: readonly OpWaveHookRun[];
|
|
106
|
+
/** `{runs}` and `CHANT_RUNS`, relative to `cwd`; the caller writes it. */
|
|
107
|
+
runsFile: string;
|
|
108
|
+
/** `CHANT_HOLD`, relative to `cwd`. */
|
|
109
|
+
holdFile: string;
|
|
110
|
+
/** Added to every hook's environment. */
|
|
111
|
+
env?: Record<string, string>;
|
|
112
|
+
cwd?: string;
|
|
113
|
+
exec?: OpWaveExec;
|
|
114
|
+
log?: (line: string) => void;
|
|
115
|
+
}
|
|
116
|
+
/** What {@link runStageHooks} gave. */
|
|
117
|
+
export interface StageHooksRun {
|
|
118
|
+
outcomes: OpWaveHookOutcome[];
|
|
119
|
+
/** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
|
|
120
|
+
failed?: string;
|
|
121
|
+
/** The `onFailure: approve` hooks that failed. A hold on another gate counts as one here. */
|
|
122
|
+
holds: string[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run one moment's hooks outside an Op waves job (#4288): the pull-request
|
|
126
|
+
* jobs, or a product's own stage, with the same `targets`, `perRun`,
|
|
127
|
+
* `onFailure` and argv filling a wave job uses. A hook that holds on another
|
|
128
|
+
* gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
|
|
129
|
+
* caller's.
|
|
130
|
+
*/
|
|
131
|
+
export declare function runStageHooks(moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, options: RunStageHooksOptions): StageHooksRun;
|
|
132
|
+
//# sourceMappingURL=stage-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-hooks.d.ts","sourceRoot":"","sources":["../../src/op/stage-hooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage-hook runner (#4288): one moment's hooks, with the `targets`,
|
|
3
|
+
* `perRun`, `onFailure` and argv filling a wave job uses. Its own module so
|
|
4
|
+
* a product that runs hooks around its own stages (terragucci) bundles it
|
|
5
|
+
* without the wave loop, the gate or the ledger. `op-waves-run.ts` runs
|
|
6
|
+
* the same hooks inside a wave job.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { opWaveHookRuns } from "./op-waves.js";
|
|
12
|
+
/** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
|
|
13
|
+
export const OP_WAVE_PENDING_RESERVED = [
|
|
14
|
+
"version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
|
|
15
|
+
"environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy",
|
|
16
|
+
];
|
|
17
|
+
export const spawnExec = (argv, cwd, env) => {
|
|
18
|
+
const result = spawnSync(argv[0], argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
19
|
+
if (result.error)
|
|
20
|
+
throw result.error;
|
|
21
|
+
return result.status ?? 1;
|
|
22
|
+
};
|
|
23
|
+
/** Fill one argv template. A placeholder whose value is not given is left as it is. */
|
|
24
|
+
export function opWaveArgv(template, values) {
|
|
25
|
+
const pairs = [
|
|
26
|
+
["{op}", values.op],
|
|
27
|
+
["{target}", values.target],
|
|
28
|
+
["{wave}", values.wave],
|
|
29
|
+
["{plans}", values.plans],
|
|
30
|
+
["{plan}", values.plan],
|
|
31
|
+
["{runs}", values.runs],
|
|
32
|
+
];
|
|
33
|
+
return template.map((part) => pairs.reduce((out, [key, value]) => (value === undefined ? out : out.replaceAll(key, value)), part));
|
|
34
|
+
}
|
|
35
|
+
/** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
|
|
36
|
+
export function pendingFields(value, where) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
40
|
+
throw new Error(`${where}: \`pending\` must be an object of fields for the pending fact`);
|
|
41
|
+
const reserved = Object.keys(value).filter((key) => OP_WAVE_PENDING_RESERVED.includes(key));
|
|
42
|
+
if (reserved.length > 0)
|
|
43
|
+
throw new Error(`${where}: \`pending\` sets ${reserved.join(", ")}, which chant writes itself`);
|
|
44
|
+
if ("description" in value && typeof value.description !== "string")
|
|
45
|
+
throw new Error(`${where}: pending.description must be a string`);
|
|
46
|
+
return Object.keys(value).length > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
|
|
49
|
+
export function readHoldRequest(path, log, label) {
|
|
50
|
+
if (!existsSync(path))
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
const r = JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
if (typeof r.gate !== "string" || r.gate === "" || typeof r.digest !== "string" || r.digest === "")
|
|
55
|
+
throw new Error("it names no gate and digest");
|
|
56
|
+
if (r.op !== undefined && (typeof r.op !== "string" || r.op === ""))
|
|
57
|
+
throw new Error("its op is not a name");
|
|
58
|
+
return { gate: r.gate, digest: r.digest, ...(r.op ? { op: r.op } : {}), ...(typeof r.description === "string" ? { description: r.description } : {}), ...(r.pending ? { pending: pendingFields(r.pending, `the hold ${label} wrote`) ?? {} } : {}) };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log(`${label} wrote a hold file that cannot be read (${err instanceof Error ? err.message : String(err)}), so it holds the wave on its set digest`);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run one moment's hooks, in order: each once for the wave, or once per run
|
|
67
|
+
* for a `perRun` hook. A hook that fails stops the rest, unless its
|
|
68
|
+
* `onFailure` is `approve`: then it holds the wave and the hooks after it run.
|
|
69
|
+
*/
|
|
70
|
+
export function runHookList(at, moment, hooks, runs, runsPath, extraEnv = {}) {
|
|
71
|
+
const out = { holds: [], outcomes: [], otherGates: [] };
|
|
72
|
+
if (!hooks || hooks.length === 0)
|
|
73
|
+
return out;
|
|
74
|
+
const { name: specName, op, waveName, k, cwd, exec, log } = at;
|
|
75
|
+
// #3821: a hook that holds the wave on another gate names it in this file.
|
|
76
|
+
const holdPath = at.holdPath ?? `.chant/op-waves/${specName}/wave-${k}/hold.json`;
|
|
77
|
+
const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
|
|
78
|
+
for (const hook of hooks) {
|
|
79
|
+
const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
|
|
80
|
+
// #4288: a hook with `targets` is for the runs they match. Once for the wave, it runs when one matches or no run is known yet.
|
|
81
|
+
const mine = opWaveHookRuns(hook, runs);
|
|
82
|
+
if (hook.targets && (hook.perRun ? mine.length === 0 : mine.length === 0 && runs.length > 0)) {
|
|
83
|
+
log(`${moment} hook ${name}: no run matches its targets (${hook.targets.join(", ")}), so it did not run`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const calls = hook.perRun
|
|
87
|
+
? mine.map((run) => ({
|
|
88
|
+
target: run.target,
|
|
89
|
+
argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
|
|
90
|
+
env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) },
|
|
91
|
+
}))
|
|
92
|
+
: [{ target: undefined, argv: opWaveArgv(hook.command, { op, wave: waveName, runs: runsPath }), env: env }];
|
|
93
|
+
for (const call of calls) {
|
|
94
|
+
const label = `${moment} hook ${name}${call.target ? ` (${call.target})` : ""}`;
|
|
95
|
+
let code;
|
|
96
|
+
rmSync(join(cwd, holdPath), { force: true });
|
|
97
|
+
try {
|
|
98
|
+
code = exec(call.argv, cwd, call.env);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
log(`${label} did not run: ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
code = 127;
|
|
103
|
+
}
|
|
104
|
+
const base = { moment, name, ...(call.target ? { target: call.target } : {}), exit: code };
|
|
105
|
+
if (code === 0) {
|
|
106
|
+
out.outcomes.push({ ...base, outcome: "passed" });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (hook.onFailure === "approve") {
|
|
110
|
+
out.outcomes.push({ ...base, outcome: "held" });
|
|
111
|
+
const request = readHoldRequest(join(cwd, holdPath), log, label);
|
|
112
|
+
if (request) {
|
|
113
|
+
out.otherGates.push({ label, name, moment, ...(call.target ? { target: call.target } : {}), argv: call.argv, env: call.env, request });
|
|
114
|
+
log(`${label} exited ${code} and holds wave ${k} (${waveName}) on gate ${request.op ?? specName} / ${request.gate}, digest ${request.digest}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
out.holds.push(label);
|
|
118
|
+
log(`${label} exited ${code}, so wave ${k} (${waveName}) waits for an approval of its set digest`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
out.outcomes.push({ ...base, outcome: "failed" });
|
|
122
|
+
out.failed = `${label} exited ${code}`;
|
|
123
|
+
log(out.failed);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run one moment's hooks outside an Op waves job (#4288): the pull-request
|
|
131
|
+
* jobs, or a product's own stage, with the same `targets`, `perRun`,
|
|
132
|
+
* `onFailure` and argv filling a wave job uses. A hook that holds on another
|
|
133
|
+
* gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
|
|
134
|
+
* caller's.
|
|
135
|
+
*/
|
|
136
|
+
export function runStageHooks(moment, hooks, options) {
|
|
137
|
+
const cwd = options.cwd ?? process.cwd();
|
|
138
|
+
const log = options.log ?? ((line) => console.error(line));
|
|
139
|
+
const place = { name: options.name, op: options.op, waveName: options.wave, k: options.k ?? 0, cwd, exec: options.exec ?? spawnExec, log, holdPath: options.holdFile };
|
|
140
|
+
const out = runHookList(place, moment, hooks, options.runs, options.runsFile, options.env);
|
|
141
|
+
return { outcomes: out.outcomes, ...(out.failed ? { failed: out.failed } : {}), holds: [...out.holds, ...out.otherGates.map((g) => `${g.label} (on ${g.request.op ?? options.name} / ${g.request.gate})`)] };
|
|
142
|
+
}
|
package/dist/override-gate.d.ts
CHANGED
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
* nothing; `override-gate-bundle.test.ts` holds its import graph to the
|
|
33
33
|
* same bundle. chant's own writers are in `./lifecycle/override-ledger.ts`.
|
|
34
34
|
*/
|
|
35
|
-
import { type GateSigners } from "./
|
|
35
|
+
import { type GateSigners } from "./lifecycle/gate-seal.js";
|
|
36
36
|
import type { GateResolutionRecord, PendingGateRecord } from "./lifecycle/gate-ledger.js";
|
|
37
37
|
import type { GateApprovalSource } from "./op/op-waves.js";
|
|
38
38
|
import type { GateAuditApply } from "./lifecycle/gate-audit.js";
|
|
39
39
|
export type { GateResolutionRecord, PendingGateRecord } from "./lifecycle/gate-ledger.js";
|
|
40
|
-
export type { GateSigners } from "./
|
|
40
|
+
export type { GateSigners } from "./lifecycle/gate-seal.js";
|
|
41
41
|
/** The op every override and denial is recorded under. `./lifecycle/gate-audit.ts` reads this op as overrides by default. */
|
|
42
42
|
export declare const OVERRIDE_OP = "policy-override";
|
|
43
43
|
/** The ledger file on `chant/lifecycle` that holds denials and overrides. */
|