@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
|
@@ -56,7 +56,8 @@ import { samePlanDigest } from "../../lifecycle/plan-digest";
|
|
|
56
56
|
import { affectedStacks } from "../../lifecycle/affected";
|
|
57
57
|
import { changedFilesBetween, lexiconChangedUnits } from "../../components/changed-files";
|
|
58
58
|
import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
|
|
59
|
-
import { EarlierWaveNotAppliedError, runFanOut } from "../../components/fan-out-run";
|
|
59
|
+
import { EarlierWaveNotAppliedError, EarlierWaveWaitingError, fanOutGateDigest, runFanOut, type FanOutWaveGate } from "../../components/fan-out-run";
|
|
60
|
+
import { parseComponentTargets } from "../../components/capability";
|
|
60
61
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../../components/fan-out-record";
|
|
61
62
|
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
62
63
|
import { appliedDigestOf } from "../../components/auto-release";
|
|
@@ -267,8 +268,30 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
267
268
|
}));
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
// #4300: `--target <component>:<address>`, the addresses a component's steps narrow to.
|
|
272
|
+
let targets: Record<string, string[]> | undefined;
|
|
273
|
+
if (args.targets && args.targets.length > 0) {
|
|
274
|
+
try {
|
|
275
|
+
targets = parseComponentTargets(args.targets, new Set(derived.components.map((c) => c.name)));
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
const running = new Set(derived.plan.order);
|
|
281
|
+
const outside = Object.keys(targets).filter((name) => !running.has(name));
|
|
282
|
+
if (outside.length > 0) {
|
|
283
|
+
console.error(formatError({
|
|
284
|
+
message: `--target names ${outside.join(", ")}, which this fan-out does not run`,
|
|
285
|
+
hint: "A target narrows a component the change selects. Drop the --target, or change what the fan-out selects.",
|
|
286
|
+
}));
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// The fan-out's identity: the derivation's digest, with the targets in it (#4300).
|
|
291
|
+
const runDigest = fanOutGateDigest(derived.plan.digest, targets);
|
|
292
|
+
|
|
270
293
|
const gate = args.gate ? { op: FAN_OUT_GATE_OP, gate: args.gate } : undefined;
|
|
271
|
-
const waveGate = args.waveGate
|
|
294
|
+
const waveGate: FanOutWaveGate | undefined = args.waveGate
|
|
272
295
|
? { op: FAN_OUT_GATE_OP, gate: args.waveGate, ...(args.wave !== undefined ? { only: args.wave } : {}) }
|
|
273
296
|
: undefined;
|
|
274
297
|
|
|
@@ -287,6 +310,7 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
287
310
|
let priorOutputs: Record<string, Record<string, unknown>> = {};
|
|
288
311
|
let priorWaves: WaveRecord[] = [];
|
|
289
312
|
let priorCarried: Record<string, unknown> = {};
|
|
313
|
+
let priorFailed: string[] = [];
|
|
290
314
|
let progress: FanOutProgress | undefined;
|
|
291
315
|
if (resumePath) {
|
|
292
316
|
let attempt: FanOutAttempt | undefined;
|
|
@@ -296,7 +320,7 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
296
320
|
console.error(formatError({ message: `--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}` }));
|
|
297
321
|
return 1;
|
|
298
322
|
}
|
|
299
|
-
if (attempt && !samePlanDigest(attempt.digest,
|
|
323
|
+
if (attempt && !samePlanDigest(attempt.digest, runDigest)) {
|
|
300
324
|
console.error(formatWarning({
|
|
301
325
|
message: `--resume: "${args.resume}" records a different fan-out (${attempt.digest || "no digest"}), so its progress does not apply here`,
|
|
302
326
|
hint: "The derivation changed, which makes this a different change. Everything selected will run.",
|
|
@@ -330,6 +354,12 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
330
354
|
);
|
|
331
355
|
progress = { completed: split.carried };
|
|
332
356
|
priorWaves = attempt.waves ?? [];
|
|
357
|
+
// #4291: what failed stays in the record until it applies, so each later wave's job blocks its dependents and runs the rest.
|
|
358
|
+
priorFailed = attempt.failed.filter((name) => !carried.has(name));
|
|
359
|
+
if (waveGate?.only !== undefined) {
|
|
360
|
+
waveGate.failedEarlier = priorFailed;
|
|
361
|
+
waveGate.waitingEarlier = priorWaves.filter((w) => w.status === "gated").map((w) => ({ wave: w.wave, gate: w.gate, components: w.components }));
|
|
362
|
+
}
|
|
333
363
|
// What steps kept per member (#3459), such as a choudoufu root's wave
|
|
334
364
|
// resume file. Each step decides whether what it kept still applies.
|
|
335
365
|
priorCarried = attempt.carried ?? {};
|
|
@@ -397,14 +427,14 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
397
427
|
// repeated command picks up from there instead of redoing them.
|
|
398
428
|
const completedSoFar = new Set(priorCompleted);
|
|
399
429
|
const inputsSoFar: Record<string, string> = { ...priorInputs };
|
|
400
|
-
const failedSoFar
|
|
430
|
+
const failedSoFar = new Set(priorFailed);
|
|
401
431
|
const outputsSoFar = { ...priorOutputs };
|
|
402
432
|
let wavesSoFar = priorWaves;
|
|
403
433
|
const carriedSoFar: Record<string, unknown> = { ...priorCarried };
|
|
404
434
|
const recordAttempt = (): void => {
|
|
405
435
|
if (!resumePath) return;
|
|
406
436
|
const attempt: FanOutAttempt = {
|
|
407
|
-
digest:
|
|
437
|
+
digest: runDigest,
|
|
408
438
|
completed: [...completedSoFar].sort(),
|
|
409
439
|
failed: [...failedSoFar].sort(),
|
|
410
440
|
outputs: outputsSoFar,
|
|
@@ -426,6 +456,7 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
426
456
|
env,
|
|
427
457
|
releaseIdentity: () => (revision ? { revision } : {}),
|
|
428
458
|
componentOutputs,
|
|
459
|
+
...(targets ? { targets } : {}),
|
|
429
460
|
...(gate ? { gate } : {}),
|
|
430
461
|
...(waveGate ? { waveGate } : {}),
|
|
431
462
|
...(waveGate
|
|
@@ -455,8 +486,9 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
455
486
|
completedSoFar.add(settled.component);
|
|
456
487
|
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
457
488
|
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
489
|
+
failedSoFar.delete(settled.component);
|
|
458
490
|
} else if (settled.status === "fail") {
|
|
459
|
-
failedSoFar.
|
|
491
|
+
failedSoFar.add(settled.component);
|
|
460
492
|
}
|
|
461
493
|
recordAttempt();
|
|
462
494
|
},
|
|
@@ -466,6 +498,11 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
|
|
|
466
498
|
} catch (err) {
|
|
467
499
|
events?.settle("failed", err instanceof Error ? err.message : String(err));
|
|
468
500
|
// `--wave <n>` reached before an earlier wave applied: nothing ran.
|
|
501
|
+
// #4291: an earlier wave waits at its gate, so this one waits too.
|
|
502
|
+
if (err instanceof EarlierWaveWaitingError) {
|
|
503
|
+
console.error(formatInfo(err.message));
|
|
504
|
+
return GATED_EXIT_CODE;
|
|
505
|
+
}
|
|
469
506
|
if (err instanceof EarlierWaveNotAppliedError) {
|
|
470
507
|
console.error(formatError({ message: err.message }));
|
|
471
508
|
return 1;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { appliedGatePath, parseAppliedGateFile, type AppliedGateLine, type FinishedGateRecord } from "../../lifecycle/gate-applied";
|
|
28
28
|
import { listFilesInDir, pushLifecycleStatus, readBlobFromPath, requireLifecycleLedger } from "../../lifecycle/git";
|
|
29
29
|
import { parseGateSubject } from "../../op/gate-point";
|
|
30
|
+
import { quorumRules, ruleNeedsProof, trustQuorumJudge, type GateQuorumJudge } from "../../op/gate-quorum";
|
|
30
31
|
import {
|
|
31
32
|
resolutionAnswering,
|
|
32
33
|
resumeGateRun,
|
|
@@ -91,6 +92,17 @@ export interface ResumeGateOptions {
|
|
|
91
92
|
/** The lines of `_gates/<op>/applied.jsonl` (#3858). Read from `chant/lifecycle` by {@link resumeOpGates} when not given. */
|
|
92
93
|
applied?: readonly AppliedGateLine[];
|
|
93
94
|
finished?: readonly FinishedGateRecord[];
|
|
95
|
+
/** Proves approvers for approval rules that name roles (#4290). Read from the trust policy at base when a fact needs one and none is given. */
|
|
96
|
+
judge?: GateQuorumJudge;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The judge a fact's approval rules are met by (#4290): the trust policy at base, read only when a rule names roles or notAuthor. */
|
|
100
|
+
async function judgeFor(pending: readonly PendingGateRecord[], opts: ResumeGateOptions): Promise<GateQuorumJudge | undefined> {
|
|
101
|
+
if (opts.judge) return opts.judge;
|
|
102
|
+
if (!pending.some((p) => quorumRules(p.approval?.quorum).some(ruleNeedsProof))) return undefined;
|
|
103
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
104
|
+
const { policy } = scopeSource(process.cwd());
|
|
105
|
+
return trustQuorumJudge({ signers: policy, roles: policy.roles });
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/** Every question the workspace at `cwd` lists, by id, the lifecycle ledger's included. Empty when the workspace can't be read. */
|
|
@@ -108,6 +120,7 @@ export async function resumeStanding(
|
|
|
108
120
|
opts: ResumeGateOptions = {},
|
|
109
121
|
pending: readonly PendingGateRecord[] = [standing],
|
|
110
122
|
): Promise<GateResumeReport> {
|
|
123
|
+
const judge = await judgeFor([standing], opts);
|
|
111
124
|
const [candidate] = planGateResumes({
|
|
112
125
|
op: standing.op,
|
|
113
126
|
pending: [standing],
|
|
@@ -115,7 +128,7 @@ export async function resumeStanding(
|
|
|
115
128
|
applied: opts.applied ?? [],
|
|
116
129
|
now: standing.timestamp,
|
|
117
130
|
...(opts.questions ? { questions: opts.questions } : {}),
|
|
118
|
-
answering: (r, s) => resolutionAnswering(r, s, pending),
|
|
131
|
+
answering: (r, s) => resolutionAnswering(r, s, pending, judge),
|
|
119
132
|
});
|
|
120
133
|
return resumeCandidate(candidate!, opts);
|
|
121
134
|
}
|
|
@@ -199,6 +212,7 @@ export async function resumeOpGates(op: string, gate: string | undefined, opts:
|
|
|
199
212
|
const now = opts.now ?? new Date().toISOString();
|
|
200
213
|
// Read the workspace's questions once, and only when a fact waits on one (#3835).
|
|
201
214
|
const questions = opts.questions ?? (ledger.pending.some((p) => p.answer) ? await workspaceQuestions(process.cwd()) : undefined);
|
|
215
|
+
const judge = await judgeFor(ledger.pending, opts);
|
|
202
216
|
const candidates = planGateResumes({
|
|
203
217
|
op,
|
|
204
218
|
pending: ledger.pending,
|
|
@@ -208,7 +222,7 @@ export async function resumeOpGates(op: string, gate: string | undefined, opts:
|
|
|
208
222
|
now,
|
|
209
223
|
...(gate !== undefined ? { gate } : {}),
|
|
210
224
|
...(questions ? { questions } : {}),
|
|
211
|
-
answering: resolutionAnswering,
|
|
225
|
+
answering: (r, s, p) => resolutionAnswering(r, s, p, judge),
|
|
212
226
|
});
|
|
213
227
|
const reports: GateResumeReport[] = [];
|
|
214
228
|
for (const c of candidates) reports.push(await resumeCandidate(c, opts));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4293: `chant lock` and `chant unlock` take, list and release target locks
|
|
3
|
+
* on `chant/lifecycle`, and exit 3 when another pull request holds a target.
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
10
|
+
import { commandRegistry, parseArgs } from "../main";
|
|
11
|
+
import { resolveCommand } from "../registry";
|
|
12
|
+
|
|
13
|
+
const dirs: string[] = [];
|
|
14
|
+
const home = process.cwd();
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
process.chdir(home);
|
|
17
|
+
for (const d of dirs) rmSync(d, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function git(cwd: string, ...args: string[]): string {
|
|
21
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function initRepo(): string {
|
|
25
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-lock-cli-")));
|
|
26
|
+
dirs.push(dir);
|
|
27
|
+
git(dir, "init", "-q", "-b", "main");
|
|
28
|
+
git(dir, "config", "user.email", "test@chant.dev");
|
|
29
|
+
git(dir, "config", "user.name", "Test");
|
|
30
|
+
git(dir, "config", "commit.gpgsign", "false");
|
|
31
|
+
writeFileSync(join(dir, "README.md"), "# t\n");
|
|
32
|
+
git(dir, "add", "README.md");
|
|
33
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function run(argv: string[]): Promise<{ code: number; out: string; err: string }> {
|
|
38
|
+
let out = "";
|
|
39
|
+
let err = "";
|
|
40
|
+
vi.spyOn(console, "log").mockImplementation((s) => void (out += String(s) + "\n"));
|
|
41
|
+
vi.spyOn(console, "error").mockImplementation((s) => void (err += String(s) + "\n"));
|
|
42
|
+
const args = parseArgs(argv);
|
|
43
|
+
const match = resolveCommand(args, commandRegistry);
|
|
44
|
+
expect(match?.def.name).toBe(argv[0]);
|
|
45
|
+
const code = await match!.def.handler({ args, plugins: [], serializers: [] });
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
return { code: code ?? 0, out, err };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let repo: string;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
repo = initRepo();
|
|
53
|
+
process.chdir(repo);
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
process.chdir(home);
|
|
57
|
+
vi.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("chant lock and chant unlock", () => {
|
|
61
|
+
test("lock takes named targets, a second pull request exits 3 naming the holder, unlock frees them", async () => {
|
|
62
|
+
const taken = await run(["lock", "web", "api", "--pr", "1", "--by", "alice", "--json"]);
|
|
63
|
+
expect(taken.code).toBe(0);
|
|
64
|
+
expect(JSON.parse(taken.out)).toMatchObject({ gate: "pr-apply", pr: 1, ok: true, stage: "lock", taken: ["api", "web"] });
|
|
65
|
+
|
|
66
|
+
const held = await run(["lock", "web", "--pr", "2", "--by", "bob"]);
|
|
67
|
+
expect(held.code).toBe(3);
|
|
68
|
+
expect(held.err).toContain("web is locked by pull request 1 (locked with chant lock by alice), so pull request 2 is not locked");
|
|
69
|
+
|
|
70
|
+
const listed = await run(["lock", "--list", "--json"]);
|
|
71
|
+
expect(Object.keys(JSON.parse(listed.out).locks)).toEqual(["api", "web"]);
|
|
72
|
+
|
|
73
|
+
const released = await run(["unlock", "--pr", "1", "--json"]);
|
|
74
|
+
expect(JSON.parse(released.out)).toMatchObject({ released: ["api", "web"] });
|
|
75
|
+
expect((await run(["lock", "web", "--pr", "2", "--by", "bob"])).code).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("--from-report locks the components a pr-plan.json selected, as plan locks, and refuses another pull request's report", async () => {
|
|
79
|
+
const report = join(repo, "pr-plan.json");
|
|
80
|
+
writeFileSync(report, JSON.stringify({ pr: 4, head: "b".repeat(40), selection: { waves: [["db"], ["api", "web"]] } }));
|
|
81
|
+
const taken = await run(["lock", "--from-report", report, "--pr", "4", "--stage", "plan", "--json"]);
|
|
82
|
+
expect(JSON.parse(taken.out)).toMatchObject({ ok: true, stage: "plan", taken: ["api", "db", "web"] });
|
|
83
|
+
const file = JSON.parse(git(repo, "show", "chant/lifecycle:_locks/pr-apply.json"));
|
|
84
|
+
expect(file.locks.db).toMatchObject({ pr: 4, stage: "plan", head: "b".repeat(40) });
|
|
85
|
+
|
|
86
|
+
const wrong = await run(["lock", "--from-report", report, "--pr", "5"]);
|
|
87
|
+
expect(wrong.code).toBe(1);
|
|
88
|
+
expect(wrong.err).toContain("is the report of pull request 4, not 5");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("refuses without --pr or targets, an unknown stage, and a gate that is not a file name", async () => {
|
|
92
|
+
expect((await run(["lock", "web"])).err).toContain("--pr <n>");
|
|
93
|
+
expect((await run(["lock", "--pr", "1"])).err).toContain("needs the targets");
|
|
94
|
+
expect((await run(["lock", "web", "--pr", "1", "--stage", "merge"])).err).toContain("--stage merge is not one of plan, apply, lock");
|
|
95
|
+
expect((await run(["lock", "web", "--pr", "1", "--gate", "../x"])).code).toBe(1);
|
|
96
|
+
expect((await run(["unlock"])).err).toContain("--pr <n>");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant lock` and `chant unlock` (#4293): take, list and release the target
|
|
3
|
+
* locks a pull request holds (../../lifecycle/target-lock.ts).
|
|
4
|
+
*
|
|
5
|
+
* ```
|
|
6
|
+
* chant lock <target>... --pr <n> [--stage plan|apply|lock] [--head <sha>] [--by <who>] [--gate <name>] [--forge <kind>] [--json]
|
|
7
|
+
* chant lock --from-report <pr-plan.json> --pr <n> [--stage plan] [--forge <kind>] [--json]
|
|
8
|
+
* chant lock --list [--gate <name>] [--json]
|
|
9
|
+
* chant unlock --pr <n> [<target>...] [--gate <name>] [--json]
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The targets are names: the components `chant components pr-plan` selects,
|
|
13
|
+
* which are what `pr-apply` checks under `ci.locks`, or any other name a
|
|
14
|
+
* pipeline locks by. `--from-report` reads them from a pull-request report
|
|
15
|
+
* (`selection.waves`), so a lock job that runs the target branch's code can
|
|
16
|
+
* lock what the plan job, which ran the pull request's code, selected.
|
|
17
|
+
*
|
|
18
|
+
* `--forge` reads which holders are still open, so a lock whose pull
|
|
19
|
+
* request merged or closed is taken over, and, with `--head`, posts the
|
|
20
|
+
* `chant/lock` status on that commit. Without it every lock counts as held.
|
|
21
|
+
*
|
|
22
|
+
* Exit 0 when the locks are taken or released, 3 when another open pull
|
|
23
|
+
* request holds a target (nothing is locked), 1 on an error.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { resolve } from "node:path";
|
|
27
|
+
import type { CommandContext } from "../registry";
|
|
28
|
+
import { formatError, formatInfo, formatSuccess, formatWarning } from "../format";
|
|
29
|
+
import { FORGE_KINDS, forgeFromEnv, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
30
|
+
import { PR_APPLY_GATE } from "../../pr-loop";
|
|
31
|
+
import {
|
|
32
|
+
describeHeldTargetLocks,
|
|
33
|
+
readTargetLocks,
|
|
34
|
+
releaseTargetLocks,
|
|
35
|
+
takeTargetLocks,
|
|
36
|
+
targetLockGateProblem,
|
|
37
|
+
targetLockPathAt,
|
|
38
|
+
TARGET_LOCK_STAGES,
|
|
39
|
+
type TargetLockStage,
|
|
40
|
+
} from "../../lifecycle/target-lock";
|
|
41
|
+
|
|
42
|
+
/** The exit for a target another open pull request holds: 3, as a run stopped at a gate exits (`GATED_EXIT_CODE` in ./run.ts). */
|
|
43
|
+
const HELD_EXIT_CODE = 3;
|
|
44
|
+
|
|
45
|
+
/** The status `chant lock --head <sha> --forge <kind>` posts. */
|
|
46
|
+
export const LOCK_STATUS_CONTEXT = "chant/lock";
|
|
47
|
+
|
|
48
|
+
const fail = (message: string, hint?: string): number => {
|
|
49
|
+
console.error(formatError({ message, ...(hint ? { hint } : {}) }));
|
|
50
|
+
return 1;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The targets named after the command word. */
|
|
54
|
+
function targetsOf(ctx: CommandContext): string[] {
|
|
55
|
+
return (ctx.args.positionals ?? []).filter((t) => t !== "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function gateOf(ctx: CommandContext): string | { error: string } {
|
|
59
|
+
const gate = ctx.args.gate ?? PR_APPLY_GATE;
|
|
60
|
+
const problem = targetLockGateProblem(gate);
|
|
61
|
+
return problem ? { error: problem } : gate;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function forgeOf(ctx: CommandContext): PrForge | undefined | { error: string } {
|
|
65
|
+
const kind = ctx.args.forge;
|
|
66
|
+
if (kind === undefined) return undefined;
|
|
67
|
+
if (!(FORGE_KINDS as readonly string[]).includes(kind)) return { error: `--forge ${kind} is not one of ${FORGE_KINDS.join(", ")}` };
|
|
68
|
+
try {
|
|
69
|
+
return forgeFromEnv(kind as ForgeKind);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const prName = (forge: PrForge | undefined) => (n: number) => (forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
76
|
+
|
|
77
|
+
/** The components a `pr-plan.json` selected, and the head it planned, checked against `--pr`. */
|
|
78
|
+
function fromReport(file: string, pr: number): { targets: string[]; head?: string } | { error: string } {
|
|
79
|
+
let doc: { pr?: unknown; head?: unknown; selection?: { waves?: unknown } };
|
|
80
|
+
try {
|
|
81
|
+
doc = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return { error: `--from-report: could not read "${file}": ${err instanceof Error ? err.message : String(err)}` };
|
|
84
|
+
}
|
|
85
|
+
if (doc.pr !== pr) return { error: `--from-report: "${file}" is the report of ${doc.pr === null || doc.pr === undefined ? "no pull request" : `pull request ${String(doc.pr)}`}, not ${pr}` };
|
|
86
|
+
const waves = doc.selection?.waves;
|
|
87
|
+
if (!Array.isArray(waves) || !waves.every((w) => Array.isArray(w) && w.every((n) => typeof n === "string"))) {
|
|
88
|
+
return { error: `--from-report: "${file}" has no selection.waves; pass a pr-plan.json that chant components pr-plan wrote` };
|
|
89
|
+
}
|
|
90
|
+
return { targets: (waves as string[][]).flat(), ...(typeof doc.head === "string" ? { head: doc.head } : {}) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function postStatus(forge: PrForge | undefined, head: string | undefined, state: "success" | "failure", description: string): Promise<void> {
|
|
94
|
+
if (!forge || !head) return;
|
|
95
|
+
try {
|
|
96
|
+
await forge.setStatus(head, { context: LOCK_STATUS_CONTEXT, state, description: description.length > 140 ? `${description.slice(0, 137)}...` : description, ...(forge.runUrl ? { url: forge.runUrl } : {}) });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(formatWarning({ message: `could not post ${LOCK_STATUS_CONTEXT} on ${head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** chant lock */
|
|
103
|
+
export async function runLock(ctx: CommandContext): Promise<number> {
|
|
104
|
+
const { args } = ctx;
|
|
105
|
+
const gate = gateOf(ctx);
|
|
106
|
+
if (typeof gate !== "string") return fail(gate.error);
|
|
107
|
+
|
|
108
|
+
if (args.list) {
|
|
109
|
+
if (targetsOf(ctx).length > 0 || args.pr !== undefined) return fail("--list prints every lock; drop the targets and --pr");
|
|
110
|
+
let file;
|
|
111
|
+
try {
|
|
112
|
+
file = await readTargetLocks(gate);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
115
|
+
}
|
|
116
|
+
const rows = Object.entries(file.locks).sort(([a], [b]) => a.localeCompare(b));
|
|
117
|
+
if (args.json) {
|
|
118
|
+
console.log(JSON.stringify({ gate, path: await targetLockPathAt(gate), locks: Object.fromEntries(rows) }, null, 2));
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
if (rows.length === 0) console.error(formatInfo(`no target locks on ${gate}`));
|
|
122
|
+
for (const [target, l] of rows) console.log(`${target}\tpull request ${l.pr}\t${l.stage}\t${l.by}\t${l.at}`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (args.pr === undefined) return fail("chant lock needs the pull or merge request that holds the locks: --pr <n>", "chant lock --list prints the locks held now.");
|
|
127
|
+
const stage = (args.lockStage ?? "lock") as TargetLockStage;
|
|
128
|
+
if (!(TARGET_LOCK_STAGES as readonly string[]).includes(stage)) return fail(`--stage ${args.lockStage} is not one of ${TARGET_LOCK_STAGES.join(", ")}`);
|
|
129
|
+
|
|
130
|
+
let targets = targetsOf(ctx);
|
|
131
|
+
let head = args.head;
|
|
132
|
+
if (args.fromReport) {
|
|
133
|
+
if (targets.length > 0) return fail("--from-report reads the targets from the report; drop the names");
|
|
134
|
+
const read = fromReport(args.fromReport, args.pr);
|
|
135
|
+
if ("error" in read) return fail(read.error);
|
|
136
|
+
targets = read.targets;
|
|
137
|
+
head ??= read.head;
|
|
138
|
+
} else if (targets.length === 0 && stage !== "plan") {
|
|
139
|
+
return fail("chant lock needs the targets to lock: chant lock <target>... --pr <n>", "Or --from-report <pr-plan.json> to lock what a plan selected.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const forge = forgeOf(ctx);
|
|
143
|
+
if (forge && "error" in forge) return fail(forge.error);
|
|
144
|
+
const isOpen = forge?.pullRequestOpen?.bind(forge);
|
|
145
|
+
const by = args.by ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER ?? "unknown";
|
|
146
|
+
const name = prName(forge);
|
|
147
|
+
|
|
148
|
+
let result;
|
|
149
|
+
try {
|
|
150
|
+
result = await takeTargetLocks(
|
|
151
|
+
gate,
|
|
152
|
+
targets,
|
|
153
|
+
{ pr: args.pr, by, at: new Date().toISOString(), stage, ...(head ? { head } : {}) },
|
|
154
|
+
{ ...(isOpen ? { isOpen } : {}), releaseStalePlan: stage === "plan" },
|
|
155
|
+
);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return fail(`could not take the locks of ${gate}: ${err instanceof Error ? err.message : String(err)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
const holders = [...new Set(result.held.map((h) => h.pr))].sort((a, b) => a - b);
|
|
162
|
+
const text = `${describeHeldTargetLocks(result.held, name)}, so ${name(args.pr)} is not locked`;
|
|
163
|
+
await postStatus(forge, head, "failure", `locked by ${holders.map(name).join(", ")}: ${result.held.map((h) => h.target).join(", ")}`);
|
|
164
|
+
if (args.json) console.log(JSON.stringify({ gate, pr: args.pr, ok: false, held: result.held }, null, 2));
|
|
165
|
+
console.error(formatError({ message: text, hint: `Lock again once ${holders.length === 1 ? "it merges or closes" : "they merge or close"}, or after chant unlock --pr ${holders[0]}.${isOpen ? "" : " Without --forge every lock counts as held."}` }));
|
|
166
|
+
return HELD_EXIT_CODE;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const holds = result.taken.length > 0 ? `holds ${result.taken.join(", ")}` : "reaches no target";
|
|
170
|
+
await postStatus(forge, head, "success", holds);
|
|
171
|
+
if (args.json) {
|
|
172
|
+
console.log(JSON.stringify({ gate, pr: args.pr, ok: true, stage, taken: result.taken, released: result.released, expired: result.expired, pushed: result.pushed }, null, 2));
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
console.error(formatSuccess(`${name(args.pr)} ${holds} on ${gate}`));
|
|
176
|
+
if (result.released.length > 0) console.error(formatInfo(`released ${result.released.join(", ")}, which it no longer reaches`));
|
|
177
|
+
if (result.expired.length > 0) console.error(formatInfo(`took over ${result.expired.join(", ")} from pull requests that merged or closed`));
|
|
178
|
+
if (!result.pushed && (result.taken.length > 0 || result.released.length > 0)) console.error(formatInfo("chant/lifecycle was not pushed: this repository has no remote, or recording is off"));
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** chant unlock */
|
|
183
|
+
export async function runUnlock(ctx: CommandContext): Promise<number> {
|
|
184
|
+
const { args } = ctx;
|
|
185
|
+
const gate = gateOf(ctx);
|
|
186
|
+
if (typeof gate !== "string") return fail(gate.error);
|
|
187
|
+
if (args.pr === undefined) return fail("chant unlock needs the pull or merge request whose locks it releases: --pr <n>");
|
|
188
|
+
const targets = targetsOf(ctx);
|
|
189
|
+
let result;
|
|
190
|
+
try {
|
|
191
|
+
result = await releaseTargetLocks(gate, args.pr, targets.length > 0 ? { targets } : {});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return fail(`could not release the locks of ${gate}: ${err instanceof Error ? err.message : String(err)}`);
|
|
194
|
+
}
|
|
195
|
+
if (args.json) {
|
|
196
|
+
console.log(JSON.stringify({ gate, pr: args.pr, released: result.released, pushed: result.pushed }, null, 2));
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
if (result.released.length === 0) console.error(formatInfo(`pull request ${args.pr} holds no lock${targets.length > 0 ? ` on ${targets.join(", ")}` : ""} on ${gate}`));
|
|
200
|
+
else console.error(formatSuccess(`released the locks pull request ${args.pr} held on ${result.released.join(", ")}`));
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
@@ -1278,7 +1278,9 @@ describe("runApprove — a gate with an approval policy (#2508)", () => {
|
|
|
1278
1278
|
}));
|
|
1279
1279
|
const out = errSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
1280
1280
|
expect(out).toContain("log-only mode, so the decision is recorded and does not change the outcome");
|
|
1281
|
-
|
|
1281
|
+
// #4290: a claimed role is recorded for the policy, and the quorum never counts it.
|
|
1282
|
+
expect(out).toContain("Quorum: 0 of 2 human approval(s) with role maintainer for this plan (none yet)");
|
|
1283
|
+
expect(out).toContain("its roles are read from .chant/trust.json at base");
|
|
1282
1284
|
errSpy.mockRestore();
|
|
1283
1285
|
});
|
|
1284
1286
|
|