@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/cli/handlers/pr.js
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
* all it uses one for; pr-apply still reads the pull request and its reviews
|
|
64
64
|
* through `--forge`.
|
|
65
65
|
*/
|
|
66
|
+
import { spawnSync } from "node:child_process";
|
|
66
67
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
67
68
|
import { join, resolve } from "node:path";
|
|
68
69
|
import { loadChantConfig, resolveAutoReleaseDisabled } from "../../config.js";
|
|
@@ -71,10 +72,11 @@ import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support.j
|
|
|
71
72
|
import { applyPrSet, approvalOf, approversWhoAuthored, approversWithoutReview, decidePrGate, planPrSet, prReport, readOnlyLedger, prWavePlan, resumePrSet, waveNotCovered, } from "../../components/pr-run.js";
|
|
72
73
|
import { composeChangeSet, verifyChangeSetDigest } from "../../change-set.js";
|
|
73
74
|
import { waveGateName } from "../../gated-waves.js";
|
|
75
|
+
import { blockedMembers, blockedWords, settleEarlierWaves } from "../../wave-blocked.js";
|
|
74
76
|
import { readFanOutAttempt, writeFanOutAttempt } from "../../components/fan-out-record.js";
|
|
75
77
|
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs.js";
|
|
76
78
|
import { samePlanDigest } from "../../lifecycle/plan-digest.js";
|
|
77
|
-
import { gitGateLedgerPort } from "../../op/gate.js";
|
|
79
|
+
import { gateIsSealed, gitGateLedgerPort } from "../../op/gate.js";
|
|
78
80
|
import { emitGateStep, gateEventsOf, GATE_PHASE } from "../../run-event-stream.js";
|
|
79
81
|
import { openCliRunEvents, refusesEventsWithJson } from "../run-events.js";
|
|
80
82
|
import { openPrReportUpload, parseReportUrl, reportKey } from "../../pr-report-url.js";
|
|
@@ -91,14 +93,16 @@ import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run
|
|
|
91
93
|
import { appliedDigestOf } from "../../components/auto-release.js";
|
|
92
94
|
import { withComponentEndpoint } from "../component-endpoint.js";
|
|
93
95
|
import { failedRunExitCode } from "../../components/driver.js";
|
|
96
|
+
import { prLoopHooksAt, runPrHooks } from "../../components/pr-hooks.js";
|
|
97
|
+
import { decidePrPolicyOverride, prLoopPolicyAt, prPolicyReport, runPrPolicy, spendPrPolicyOverride } from "../../components/pr-policy.js";
|
|
94
98
|
/** Where the reports land unless `--output` says. */
|
|
95
99
|
export const PR_REPORT_DIR = ".chant/pr";
|
|
96
100
|
const fail = (message, hint) => {
|
|
97
101
|
console.error(formatError({ message, ...(hint ? { hint } : {}) }));
|
|
98
102
|
return 1;
|
|
99
103
|
};
|
|
100
|
-
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. */
|
|
101
|
-
async function prepare(ctx, stage) {
|
|
104
|
+
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. `pr-env up` (#4298) prepares as the plan does. */
|
|
105
|
+
export async function prepare(ctx, stage) {
|
|
102
106
|
const { args } = ctx;
|
|
103
107
|
const projectPath = resolve(".");
|
|
104
108
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} }));
|
|
@@ -178,8 +182,31 @@ async function prepare(ctx, stage) {
|
|
|
178
182
|
return fail(`--seed-outputs: could not read "${file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
179
183
|
}
|
|
180
184
|
}
|
|
185
|
+
// #4288: the stage hooks, read from the declaration at base so the change cannot edit what its own jobs run.
|
|
186
|
+
let hooks;
|
|
187
|
+
try {
|
|
188
|
+
hooks = prLoopHooksAt(projectPath, base);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
192
|
+
}
|
|
193
|
+
if (hooks.note)
|
|
194
|
+
console.error(formatInfo(hooks.note));
|
|
195
|
+
// #4294: the policy, read from the declaration at base like the hooks.
|
|
196
|
+
let policy;
|
|
197
|
+
try {
|
|
198
|
+
policy = prLoopPolicyAt(projectPath, base);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
202
|
+
}
|
|
203
|
+
if (policy.note)
|
|
204
|
+
console.error(formatInfo(policy.note));
|
|
181
205
|
const env = args.env ?? "local";
|
|
182
206
|
const registry = await fanOutRegistry(projectPath, config);
|
|
207
|
+
const routedAway = member ? await routing(ctx, stage, projectPath, member, base, forge) : undefined;
|
|
208
|
+
if (routedAway)
|
|
209
|
+
console.error(formatInfo(routedAway));
|
|
183
210
|
return {
|
|
184
211
|
config,
|
|
185
212
|
env,
|
|
@@ -189,12 +216,97 @@ async function prepare(ctx, stage) {
|
|
|
189
216
|
gate: args.gate ?? PR_APPLY_GATE,
|
|
190
217
|
...(forge ? { forge } : {}),
|
|
191
218
|
components: derived.components,
|
|
192
|
-
plan: derived.plan,
|
|
219
|
+
plan: routedAway ? { ...derived.plan, order: [], waves: [], seeds: [], indeterminate: [] } : derived.plan,
|
|
193
220
|
signal: derived.signal,
|
|
194
221
|
registry,
|
|
195
222
|
seededOutputs,
|
|
223
|
+
hooks,
|
|
224
|
+
hookOutcomes: [],
|
|
225
|
+
...(routedAway ? { routedAway } : {}),
|
|
226
|
+
policy,
|
|
196
227
|
};
|
|
197
228
|
}
|
|
229
|
+
/** Set where `p`'s hooks run, once the stage knows its op (#4288). */
|
|
230
|
+
export function placeHooks(ctx, p, stage, op) {
|
|
231
|
+
p.hookPlace = { op, env: p.env, stage, dir: ctx.args.output ?? PR_REPORT_DIR, cwd: resolve("."), log: (line) => console.error(line) };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Run one moment's hooks for `components` (#4288), keeping their outcomes
|
|
235
|
+
* for the report. Returns the failure as the report's message, or undefined
|
|
236
|
+
* when no hook failed. A hold is logged: the pull request's gate already
|
|
237
|
+
* waits for a person's approval of the digest.
|
|
238
|
+
*/
|
|
239
|
+
export function runStageHook(p, moment, components, planned, what = "nothing was applied") {
|
|
240
|
+
if (!p.hookPlace || components.length === 0)
|
|
241
|
+
return undefined;
|
|
242
|
+
const ran = runPrHooks(p.hooks, moment, p.hookPlace, components, planned ? { members: planned.members, plans: planned.plans } : undefined);
|
|
243
|
+
p.hookOutcomes.push(...ran.outcomes);
|
|
244
|
+
for (const hold of ran.holds)
|
|
245
|
+
console.error(formatInfo(`${hold} held, and the pull request's gate asks for an approval of the digest either way`));
|
|
246
|
+
if (!ran.failed)
|
|
247
|
+
return undefined;
|
|
248
|
+
p.hookFailed = `The ${ran.failed}, so ${what}.`;
|
|
249
|
+
return p.hookFailed;
|
|
250
|
+
}
|
|
251
|
+
const EMPTY_SET = () => ({ doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: true });
|
|
252
|
+
/**
|
|
253
|
+
* Plan with the plan-side hooks around it (#4288): before-observe and
|
|
254
|
+
* before-plan, then the plan, then after-plan. A failed hook gives a failed
|
|
255
|
+
* set, so the stage reports `plan-failed` and nothing applies.
|
|
256
|
+
*/
|
|
257
|
+
async function hookedPlan(p, components, plan) {
|
|
258
|
+
if (runStageHook(p, "before-observe", components, undefined, "nothing was planned or applied"))
|
|
259
|
+
return EMPTY_SET();
|
|
260
|
+
if (runStageHook(p, "before-plan", components, undefined, "nothing was planned or applied"))
|
|
261
|
+
return EMPTY_SET();
|
|
262
|
+
const set = await plan();
|
|
263
|
+
if (set.failed)
|
|
264
|
+
return set;
|
|
265
|
+
if (runStageHook(p, "after-plan", components, set))
|
|
266
|
+
return { ...set, failed: true };
|
|
267
|
+
return set;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Whether `ci.branches` or `ci.labels` (#4292), read from the declaration at
|
|
271
|
+
* base, leave `member` out of this stage: the reason, or undefined when the
|
|
272
|
+
* member runs. The record and apply stages read `ci.branches` against the
|
|
273
|
+
* pushed branch (`--branch`, else the forge's variables); the plan stage
|
|
274
|
+
* reads `ci.labels` against the pull request's labels. A label only narrows
|
|
275
|
+
* what is planned: the record and apply stages never read labels.
|
|
276
|
+
*/
|
|
277
|
+
async function routing(ctx, stage, projectPath, member, base, forge) {
|
|
278
|
+
const { scopeSource } = await import("../../workspace/write-scope.js");
|
|
279
|
+
const { appliesFrom, plansUnderLabels, pushedBranchFromEnv } = await import("../../workspace/ci-branches.js");
|
|
280
|
+
const ci = scopeSource(projectPath, base).declaration?.ci;
|
|
281
|
+
if (stage !== "plan") {
|
|
282
|
+
if (!ci?.branches)
|
|
283
|
+
return undefined;
|
|
284
|
+
const pushed = ctx.args.branch ?? pushedBranchFromEnv();
|
|
285
|
+
if (!pushed)
|
|
286
|
+
return undefined;
|
|
287
|
+
const route = appliesFrom(ci.branches, member, pushed);
|
|
288
|
+
if (!route.applies)
|
|
289
|
+
return route.note;
|
|
290
|
+
console.error(formatInfo(route.note));
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
if (!ci?.labels || ctx.args.pr === undefined || !forge?.labels)
|
|
294
|
+
return undefined;
|
|
295
|
+
let carried;
|
|
296
|
+
try {
|
|
297
|
+
carried = await forge.labels(ctx.args.pr);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
console.error(formatWarning({ message: `could not read the labels of #${ctx.args.pr}, so every member plans: ${err instanceof Error ? err.message : String(err)}` }));
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const filter = plansUnderLabels(ci.labels, member, carried);
|
|
304
|
+
if (!filter.plans)
|
|
305
|
+
return filter.note;
|
|
306
|
+
if (filter.note)
|
|
307
|
+
console.error(formatInfo(filter.note));
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
198
310
|
/**
|
|
199
311
|
* The one `--member` name, checked against the workspace member that owns
|
|
200
312
|
* `projectPath`: a pipeline copied into another member would otherwise
|
|
@@ -223,8 +335,11 @@ async function checkMember(projectPath, names) {
|
|
|
223
335
|
}
|
|
224
336
|
return name;
|
|
225
337
|
}
|
|
226
|
-
/** Plan every member the derivation selects. */
|
|
227
|
-
function planAll(p, onRunEvent) {
|
|
338
|
+
/** Plan every member the derivation selects, between the plan-side hooks (#4288). */
|
|
339
|
+
export function planAll(p, onRunEvent) {
|
|
340
|
+
return hookedPlan(p, p.plan.order, () => planBare(p, onRunEvent));
|
|
341
|
+
}
|
|
342
|
+
export function planBare(p, onRunEvent) {
|
|
228
343
|
return planPrSet({
|
|
229
344
|
components: p.components,
|
|
230
345
|
plan: p.plan,
|
|
@@ -286,7 +401,7 @@ async function publish(forge, report, note) {
|
|
|
286
401
|
return;
|
|
287
402
|
// A member the change does not reach has nothing to say: one note per
|
|
288
403
|
// member would put a line on every pull request for each member it skips.
|
|
289
|
-
if (report.member && report.status === "nothing")
|
|
404
|
+
if (report.member && report.status === "nothing" && !report.overdue?.length)
|
|
290
405
|
return;
|
|
291
406
|
try {
|
|
292
407
|
await forge.upsertNote(report.pr, prNoteMarker(report.env, report.member), note);
|
|
@@ -308,6 +423,17 @@ function printMembers(report) {
|
|
|
308
423
|
}
|
|
309
424
|
}
|
|
310
425
|
async function finish(ctx, p, report, upload) {
|
|
426
|
+
// #4288: the hooks the stage ran, and a failed hook's message in place of the stage's own.
|
|
427
|
+
if (p.hookOutcomes.length > 0)
|
|
428
|
+
report = { ...report, hooks: [...p.hookOutcomes] };
|
|
429
|
+
if (p.hookFailed)
|
|
430
|
+
report = { ...report, message: p.hookFailed };
|
|
431
|
+
if (p.routedAway && report.status === "nothing" && !report.message)
|
|
432
|
+
report = { ...report, message: p.routedAway };
|
|
433
|
+
if (p.policyReport)
|
|
434
|
+
report = { ...report, policy: p.policyReport };
|
|
435
|
+
if (p.beforeMerge)
|
|
436
|
+
report = { ...report, beforeMerge: p.beforeMerge };
|
|
311
437
|
const note = noteFor(report, p.forge);
|
|
312
438
|
writeOutputs(ctx, report, note, upload);
|
|
313
439
|
if (!ctx.args.noStatus)
|
|
@@ -354,6 +480,85 @@ function openUpload(events, stage, pr, p) {
|
|
|
354
480
|
});
|
|
355
481
|
return events.upload;
|
|
356
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Run `ci.policy` at base over the planned set (#4294) and keep what it said
|
|
485
|
+
* on `p` for the report. A denial is decided against the override ledger:
|
|
486
|
+
* read only at the plan stage, and recorded for an override at the apply.
|
|
487
|
+
*/
|
|
488
|
+
async function judgePolicy(p, at) {
|
|
489
|
+
const verdict = runPrPolicy(p.policy, { version: 1, stage: at.stage, op: at.op, gate: p.gate, env: p.env, pr: at.pr, base: p.base, head: p.head, changeSet: at.set.doc });
|
|
490
|
+
if (!verdict)
|
|
491
|
+
return { held: false };
|
|
492
|
+
const report = prPolicyReport(verdict, p.policy.base);
|
|
493
|
+
p.policyReport = report;
|
|
494
|
+
for (const w of verdict.warnings)
|
|
495
|
+
console.error(formatWarning({ message: `policy warning: ${w}` }));
|
|
496
|
+
if (verdict.result === "error") {
|
|
497
|
+
return { held: false, stop: { refusal: "policy-error", code: 1, message: `The policy at ${p.policy.base.slice(0, 12)} could not be checked, so nothing was applied: ${verdict.error}` } };
|
|
498
|
+
}
|
|
499
|
+
const held = verdict.holds.length > 0;
|
|
500
|
+
for (const h of verdict.holds)
|
|
501
|
+
console.error(formatInfo(`policy holds the plan for a person's approval (${h.rule}): ${h.message}`));
|
|
502
|
+
if (verdict.result !== "denied")
|
|
503
|
+
return { held };
|
|
504
|
+
const denied = verdict.denials.map((d) => `${d.rule}: ${d.message}`).join("; ");
|
|
505
|
+
let decided;
|
|
506
|
+
try {
|
|
507
|
+
// The plan stage records nothing, and reads the applied records so a spent override reads as spent.
|
|
508
|
+
const real = gitGateLedgerPort();
|
|
509
|
+
const port = at.stage === "plan" ? { ...readOnlyLedger(real), ...(real.readApplied ? { readApplied: real.readApplied.bind(real) } : {}) } : real;
|
|
510
|
+
decided = await decidePrPolicyOverride({
|
|
511
|
+
port,
|
|
512
|
+
subject: at.op,
|
|
513
|
+
digest: at.set.doc.digest,
|
|
514
|
+
rules: verdict.rules,
|
|
515
|
+
base: p.base,
|
|
516
|
+
cwd: resolve("."),
|
|
517
|
+
sealed: await gateIsSealed(p.gate),
|
|
518
|
+
record: at.stage === "apply",
|
|
519
|
+
description: `${at.op}: plan ${at.set.doc.digest} denied by ${verdict.rules.join(", ")}`,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
524
|
+
if (at.stage === "plan") {
|
|
525
|
+
console.error(formatWarning({ message: `could not read the override ledger, so no override reads as standing: ${why}` }));
|
|
526
|
+
return { held, stop: { refusal: "policy-denied", code: 1, message: `The policy denied this plan: ${denied}.` } };
|
|
527
|
+
}
|
|
528
|
+
return { held, stop: { refusal: "policy-denied", code: GATED_EXIT_CODE, message: `The policy denied this plan (${denied}), and the override ledger could not be read or written: ${why}. Nothing was applied.` } };
|
|
529
|
+
}
|
|
530
|
+
if (decided.status === "overridden") {
|
|
531
|
+
report.override = decided.override;
|
|
532
|
+
console.error(formatInfo(`the policy denial (${verdict.rules.join(", ")}) is overridden by ${decided.override.by} at ${decided.override.at}: ${decided.override.reason}`));
|
|
533
|
+
return { held, override: decided.override };
|
|
534
|
+
}
|
|
535
|
+
report.overrideCommand = decided.command;
|
|
536
|
+
for (const why of decided.refusals)
|
|
537
|
+
console.error(formatInfo(`an override does not count: ${why}`));
|
|
538
|
+
const who = decided.overriders.length ? `${decided.overriders.join(", ")} may` : "nobody listed in .chant/overriders at base may";
|
|
539
|
+
const moved = decided.moved ? ` ${decided.moved.by} overrode an earlier plan or other rules, which counts for nothing here.` : "";
|
|
540
|
+
return {
|
|
541
|
+
held,
|
|
542
|
+
stop: {
|
|
543
|
+
refusal: "policy-denied",
|
|
544
|
+
code: at.stage === "plan" ? 1 : GATED_EXIT_CODE,
|
|
545
|
+
message: `The policy denied this plan: ${denied}.${moved} Nothing was applied. ${who} override it: ${decided.command}`,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/** Spend the override the apply runs under (#4294), before anything applies. The failure's message, or undefined. */
|
|
550
|
+
async function spendPolicy(op, judged, head) {
|
|
551
|
+
if (!judged.override)
|
|
552
|
+
return undefined;
|
|
553
|
+
try {
|
|
554
|
+
await spendPrPolicyOverride(gitGateLedgerPort(), op, judged.override, { commit: head });
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
return `Could not record the override this apply runs under, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`;
|
|
558
|
+
}
|
|
559
|
+
console.error(formatInfo(`applies under the override by ${judged.override.by}, which is now spent`));
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
357
562
|
/** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
358
563
|
export function runComponentsPrPlan(ctx) {
|
|
359
564
|
if (refusesEventsWithJson(ctx.args, "the stage's report"))
|
|
@@ -368,12 +573,15 @@ async function prPlan(ctx, stage) {
|
|
|
368
573
|
if (typeof p === "number")
|
|
369
574
|
return p;
|
|
370
575
|
const op = prOp(ctx.args.pr, p.member);
|
|
576
|
+
placeHooks(ctx, p, "plan", op);
|
|
371
577
|
const upload = openUpload(stage, "plan", ctx.args.pr, p);
|
|
372
578
|
// `--events-json` (#4041): planning, then where the approval stands.
|
|
373
579
|
const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") }, upload?.line);
|
|
374
580
|
stage.stream = events;
|
|
375
581
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
376
582
|
const set = await planAll(p, events?.emit);
|
|
583
|
+
// #4294: the policy at base over the planned change set.
|
|
584
|
+
const judged = p.plan.order.length > 0 && !set.failed ? await judgePolicy(p, { stage: "plan", op, pr: ctx.args.pr, set }) : { held: false };
|
|
377
585
|
let approval = { status: "pending" };
|
|
378
586
|
if (p.plan.order.length > 0) {
|
|
379
587
|
try {
|
|
@@ -385,7 +593,8 @@ async function prPlan(ctx, stage) {
|
|
|
385
593
|
}
|
|
386
594
|
}
|
|
387
595
|
const status = p.plan.order.length === 0 ? "nothing" : set.failed ? "plan-failed" : "planned";
|
|
388
|
-
const
|
|
596
|
+
const overdue = await overdueOps(p.forge, p.member);
|
|
597
|
+
const planned = prReport({
|
|
389
598
|
stage: "plan",
|
|
390
599
|
pr: ctx.args.pr,
|
|
391
600
|
env: p.env,
|
|
@@ -401,6 +610,7 @@ async function prPlan(ctx, stage) {
|
|
|
401
610
|
status,
|
|
402
611
|
...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
|
|
403
612
|
});
|
|
613
|
+
const report = overdue.length > 0 ? { ...planned, overdue } : planned;
|
|
404
614
|
await finish(ctx, p, report, upload);
|
|
405
615
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
406
616
|
events?.settle(settleOf(status), status === "plan-failed" ? "a member failed to plan" : undefined);
|
|
@@ -410,15 +620,61 @@ async function prPlan(ctx, stage) {
|
|
|
410
620
|
console.error(formatInfo(`plan : ${report.digest}`));
|
|
411
621
|
console.error(formatInfo(approval.status === "approved" ? `approved by ${(approval.approvedBy ?? []).join(", ")}` : `approve : ${prApproveCommand(report)}`));
|
|
412
622
|
}
|
|
413
|
-
|
|
623
|
+
if (judged.stop)
|
|
624
|
+
console.error(formatWarning({ message: judged.stop.message }));
|
|
625
|
+
return status === "plan-failed" || judged.stop ? 1 : 0;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* The scheduled watch and reconcile Ops that stopped running (#4289,
|
|
629
|
+
* `op/schedule-overdue.ts`), asked of the forge. Without a forge, or when
|
|
630
|
+
* the Ops cannot be read, nothing is checked and the plan goes on. The
|
|
631
|
+
* generated plan holds no forge token (#4295), so there the plan-note job's
|
|
632
|
+
* `pr-post` asks instead, reading the Ops at the target branch's commit.
|
|
633
|
+
*/
|
|
634
|
+
async function overdueOps(forge, member) {
|
|
635
|
+
if (!forge?.scheduledRuns)
|
|
636
|
+
return [];
|
|
637
|
+
try {
|
|
638
|
+
const { discoverOps } = await import("../../op/discover.js");
|
|
639
|
+
const { scheduledWatches, checkOverdueOps } = await import("../../op/schedule-overdue.js");
|
|
640
|
+
const discovered = await discoverOps({ cwd: resolve(".") });
|
|
641
|
+
const watches = scheduledWatches([...discovered.ops.values()].map((d) => d.config));
|
|
642
|
+
if (watches.length === 0)
|
|
643
|
+
return [];
|
|
644
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
|
|
645
|
+
if (top.status !== 0)
|
|
646
|
+
return [];
|
|
647
|
+
return await checkOverdueOps({
|
|
648
|
+
watches,
|
|
649
|
+
forge,
|
|
650
|
+
repoRoot: top.stdout.trim(),
|
|
651
|
+
...(member ? { member } : {}),
|
|
652
|
+
now: new Date(),
|
|
653
|
+
log: (line) => console.error(formatWarning({ message: line })),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
console.error(formatWarning({ message: `could not check the scheduled Ops, so none is reported overdue: ${err instanceof Error ? err.message : String(err)}` }));
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
414
660
|
}
|
|
415
661
|
/** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
416
662
|
export function runComponentsPrApply(ctx) {
|
|
417
663
|
if (refusesEventsWithJson(ctx.args, "the stage's report"))
|
|
418
664
|
return Promise.resolve(1);
|
|
419
|
-
return withStageEvents("pr-apply", ctx, (events) =>
|
|
665
|
+
return withStageEvents("pr-apply", ctx, async (events) => {
|
|
666
|
+
// #4301: an apply before merge asks the forge its requires checks before any of the head's code loads, the config included.
|
|
667
|
+
let beforeMerge;
|
|
668
|
+
if (ctx.args.beforeMerge) {
|
|
669
|
+
const checked = await beforeMergeChecks(ctx);
|
|
670
|
+
if (typeof checked === "number")
|
|
671
|
+
return checked;
|
|
672
|
+
beforeMerge = checked;
|
|
673
|
+
}
|
|
674
|
+
return withComponentEndpoint(ctx, () => prApply(ctx, events, beforeMerge));
|
|
675
|
+
});
|
|
420
676
|
}
|
|
421
|
-
async function prApply(ctx, stage) {
|
|
677
|
+
async function prApply(ctx, stage, beforeMerge) {
|
|
422
678
|
// #4168: the review-rules security group turns --require-review on, read from the declaration at base.
|
|
423
679
|
const { securityGroupsAt } = await import("../../workspace/security.js");
|
|
424
680
|
const reviewRules = !ctx.args.requireReview && securityGroupsAt(process.cwd(), ctx.args.base).has("review-rules");
|
|
@@ -426,6 +682,9 @@ async function prApply(ctx, stage) {
|
|
|
426
682
|
return fail("the workspace opts into review-rules, which reads the pull request's reviews, so pr-apply needs --forge <kind>");
|
|
427
683
|
if (reviewRules)
|
|
428
684
|
ctx = { ...ctx, args: { ...ctx.args, requireReview: true } };
|
|
685
|
+
// #4293: ci.locks, read at base like the security groups, so the change cannot turn its own locks off.
|
|
686
|
+
const { ciLocksAt } = await import("../../workspace/ci-locks.js");
|
|
687
|
+
const locks = ciLocksAt(process.cwd(), ctx.args.base);
|
|
429
688
|
const { args } = ctx;
|
|
430
689
|
if (args.requireReview && !args.forge)
|
|
431
690
|
return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
|
|
@@ -445,6 +704,8 @@ async function prApply(ctx, stage) {
|
|
|
445
704
|
const p = await prepare(ctx, "apply");
|
|
446
705
|
if (typeof p === "number")
|
|
447
706
|
return p;
|
|
707
|
+
if (beforeMerge)
|
|
708
|
+
p.beforeMerge = beforeMerge;
|
|
448
709
|
let pr = args.pr ?? null;
|
|
449
710
|
if (pr === null && !green && p.forge && p.plan.order.length > 0) {
|
|
450
711
|
try {
|
|
@@ -455,6 +716,7 @@ async function prApply(ctx, stage) {
|
|
|
455
716
|
}
|
|
456
717
|
}
|
|
457
718
|
const op = green ? greenOp(green.commit, p.member) : pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
719
|
+
placeHooks(ctx, p, "apply", op);
|
|
458
720
|
// The run id the release records carry, chosen before the run so that
|
|
459
721
|
// `--events-json` (#4041, #4007) names the run by it too.
|
|
460
722
|
const release = autoReleaseRun(`${op}-${Date.now()}`);
|
|
@@ -462,6 +724,15 @@ async function prApply(ctx, stage) {
|
|
|
462
724
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
463
725
|
stage.stream = events;
|
|
464
726
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
727
|
+
if (locks && p.plan.order.length > 0) {
|
|
728
|
+
const held = await heldLocksOf(p, pr);
|
|
729
|
+
if (typeof held === "number")
|
|
730
|
+
return held;
|
|
731
|
+
if (held) {
|
|
732
|
+
const set = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
733
|
+
return stopper(ctx, p, events, upload)(prReport({ stage: "apply", pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, set, approval: { status: "pending" }, status: "refused", refusal: "locked", message: held }), GATED_EXIT_CODE);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
465
736
|
if (args.waveGate)
|
|
466
737
|
return prApplyWaves(ctx, p, { pr, op, release, upload, events, ...(green ? { green } : {}) });
|
|
467
738
|
// `--resume` (#3464): an earlier attempt at this change on this commit
|
|
@@ -521,11 +792,16 @@ async function prApply(ctx, stage) {
|
|
|
521
792
|
await storeChangeSetForGate(ctx, p, set.doc);
|
|
522
793
|
let approval;
|
|
523
794
|
let via = "gate";
|
|
795
|
+
// #4294: the policy at base, before the gate. A resume runs under the decision of the attempt it finishes.
|
|
796
|
+
let judged = { held: false };
|
|
524
797
|
if (resumed) {
|
|
525
798
|
approval = resumed.approval;
|
|
526
799
|
}
|
|
527
800
|
else {
|
|
528
|
-
|
|
801
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
802
|
+
if (judged.stop)
|
|
803
|
+
return stop(prReport({ ...base, approval: { status: "pending" }, status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
804
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
529
805
|
approval = decided.approval;
|
|
530
806
|
if (!decided.satisfied)
|
|
531
807
|
return stop(prReport({ ...base, approval, status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
@@ -539,6 +815,17 @@ async function prApply(ctx, stage) {
|
|
|
539
815
|
emitReviewStop(events, review.message);
|
|
540
816
|
return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
|
|
541
817
|
}
|
|
818
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
819
|
+
if (unspent)
|
|
820
|
+
return stop(prReport({ ...base, approval, status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
821
|
+
// #4293, #4301: under ci.locks an apply before merge holds its targets until the pull request merges or closes.
|
|
822
|
+
if (locks && beforeMerge && pr !== null) {
|
|
823
|
+
const taken = await takeApplyLocks(p, pr);
|
|
824
|
+
if (typeof taken === "number")
|
|
825
|
+
return taken;
|
|
826
|
+
if (taken)
|
|
827
|
+
return stop(prReport({ ...base, approval, status: "refused", refusal: "locked", message: taken }), GATED_EXIT_CODE);
|
|
828
|
+
}
|
|
542
829
|
// The record is written before the first component runs and again as each
|
|
543
830
|
// one settles, so a run killed partway still knows what applied.
|
|
544
831
|
const prior = resumed ? { completed: resumed.completed, outputs: resumed.outputs } : { completed: [], outputs: {} };
|
|
@@ -569,6 +856,11 @@ async function prApply(ctx, stage) {
|
|
|
569
856
|
});
|
|
570
857
|
};
|
|
571
858
|
recordAttempt();
|
|
859
|
+
// #4288: the before-apply hooks, once the gate and the review let the apply through.
|
|
860
|
+
const toApply = (resumed?.plan ?? p.plan).order.filter((name) => !completedSoFar.has(name));
|
|
861
|
+
const beforeFailed = runStageHook(p, "before-apply", toApply);
|
|
862
|
+
if (beforeFailed)
|
|
863
|
+
return stop(prReport({ ...base, approval, status: "failed", message: beforeFailed }), 1);
|
|
572
864
|
// #3999: a round planned after an upstream applied waits on a gate of its
|
|
573
865
|
// own, under the same identity.gates rule and the same review checks.
|
|
574
866
|
const roundLedger = gitGateLedgerPort();
|
|
@@ -625,11 +917,13 @@ async function prApply(ctx, stage) {
|
|
|
625
917
|
: {}),
|
|
626
918
|
});
|
|
627
919
|
recordAttempt();
|
|
920
|
+
// #4288: the after-apply hooks, for the components that applied.
|
|
921
|
+
const afterFailed = runStageHook(p, "after-apply", toApply.filter((name) => applied.run.completed.includes(name)), undefined, "the apply counts as failed though its components applied");
|
|
628
922
|
await recordAutoReleasesForRun(applied.run.results, p.env, release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
629
923
|
const report = prReport({
|
|
630
924
|
...base,
|
|
631
925
|
approval,
|
|
632
|
-
status: applied.status,
|
|
926
|
+
status: afterFailed ? "failed" : applied.status,
|
|
633
927
|
members: applied.members,
|
|
634
928
|
...(resumed ? { resumed: resumed.completed } : {}),
|
|
635
929
|
...(applied.rounds ? { rounds: applied.rounds } : {}),
|
|
@@ -638,14 +932,126 @@ async function prApply(ctx, stage) {
|
|
|
638
932
|
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
639
933
|
// a person. A step that refused its plan because the target moved since
|
|
640
934
|
// the approval exits 3, as a moved digest does before anything applies (#3997).
|
|
641
|
-
const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
935
|
+
const exit = afterFailed ? 1 : applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
642
936
|
if (green && applied.status === "applied")
|
|
643
937
|
await recordGreenApplied(p, green, set.doc.digest, approval, via);
|
|
644
938
|
const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
|
|
645
|
-
if (applied.status === "applied")
|
|
939
|
+
if (applied.status === "applied" && !afterFailed)
|
|
646
940
|
console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
647
941
|
return code;
|
|
648
942
|
}
|
|
943
|
+
/**
|
|
944
|
+
* Under `ci.locks` (#4293): the refusal when another open pull request holds
|
|
945
|
+
* a lock on a component this apply reaches, else null. The targets are the
|
|
946
|
+
* selected components, the names `chant lock` takes. A lock whose pull
|
|
947
|
+
* request the forge says merged or closed reads as released; with no
|
|
948
|
+
* `--forge` every lock counts. A lock file that cannot be read stops the
|
|
949
|
+
* apply, since nothing then says the targets are free.
|
|
950
|
+
*/
|
|
951
|
+
async function heldLocksOf(p, pr) {
|
|
952
|
+
const { heldTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock.js");
|
|
953
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
954
|
+
let held;
|
|
955
|
+
try {
|
|
956
|
+
held = await heldTargetLocks(p.gate, p.plan.order, pr, { ...(isOpen ? { isOpen } : {}) });
|
|
957
|
+
}
|
|
958
|
+
catch (err) {
|
|
959
|
+
return fail(`could not read the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
960
|
+
}
|
|
961
|
+
if (held.length === 0)
|
|
962
|
+
return null;
|
|
963
|
+
const name = (n) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
964
|
+
const holders = [...new Set(held.map((h) => h.pr))].sort((a, b) => a - b);
|
|
965
|
+
return `${describeHeldTargetLocks(held, name)}, so nothing was applied. Apply again once ${holders.length === 1 ? "it merges or closes" : "they merge or close"}, or after \`chant unlock --pr ${holders[0]}${p.gate === PR_APPLY_GATE ? "" : ` --gate ${p.gate}`}\`${isOpen ? "" : ". Without --forge every lock counts as held"}.`;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* `pr-apply --before-merge` (#4301, ws-106), before any of the head's code
|
|
969
|
+
* loads: the declaration at base must set `ci.workflow: "apply-before-merge"`
|
|
970
|
+
* for the member, and the forge must say the pull request is approved,
|
|
971
|
+
* mergeable, undiverged and green at the commit checked out. Returns the
|
|
972
|
+
* checks for the report, or the exit code once it wrote a refusal.
|
|
973
|
+
*/
|
|
974
|
+
async function beforeMergeChecks(ctx) {
|
|
975
|
+
const { args } = ctx;
|
|
976
|
+
if (args.green)
|
|
977
|
+
return fail("--before-merge applies an open pull request's head and --green a green commit; pass one of them");
|
|
978
|
+
if (args.waveGate)
|
|
979
|
+
return fail("--before-merge applies the pull request's plan in one run, so it takes no --wave-gate");
|
|
980
|
+
if (args.pr === undefined)
|
|
981
|
+
return fail("--before-merge applies an open pull request, so it needs --pr <n>");
|
|
982
|
+
if (!args.forge || !FORGE_KINDS.includes(args.forge))
|
|
983
|
+
return fail("--before-merge asks the forge whether the pull request is ready, so it needs --forge github|gitlab|forgejo");
|
|
984
|
+
if (!args.base)
|
|
985
|
+
return fail("pr-apply needs the commit the change is measured from: --base <ref>", "Before merge, the target branch's commit.");
|
|
986
|
+
const cwd = resolve(".");
|
|
987
|
+
let forge;
|
|
988
|
+
let head;
|
|
989
|
+
let base;
|
|
990
|
+
try {
|
|
991
|
+
forge = forgeFromEnv(args.forge);
|
|
992
|
+
head = await resolveCommit(cwd, args.head ?? "HEAD");
|
|
993
|
+
base = await resolveMergeBase(cwd, args.base, head);
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
997
|
+
}
|
|
998
|
+
const member = args.members?.[0];
|
|
999
|
+
const gate = args.gate ?? PR_APPLY_GATE;
|
|
1000
|
+
const { ciWorkflowAt } = await import("../../workspace/ci-workflow-at.js");
|
|
1001
|
+
const { appliesBeforeMerge } = await import("../../workspace/ci-workflow.js");
|
|
1002
|
+
const { checkPrRequires, unmetRequires } = await import("../../pr-requires.js");
|
|
1003
|
+
const workflow = ciWorkflowAt(cwd, args.base, member);
|
|
1004
|
+
const early = earlyStop(ctx, forge, { pr: args.pr, base, head, gate, ...(member ? { member } : {}) });
|
|
1005
|
+
if (!appliesBeforeMerge(workflow)) {
|
|
1006
|
+
return early({ requires: [] }, "before-merge-off", `The declaration at base sets ci.workflow to ${workflow ?? "nothing"}${member ? ` for ${member}` : ""}, and only apply-before-merge applies a pull request before it merges (ws-106). Nothing was applied.`, 1);
|
|
1007
|
+
}
|
|
1008
|
+
// The apply's own statuses never hold it back: chant ci run's, and the report's.
|
|
1009
|
+
const ignore = [`chant/${gate}`, `chant/${PR_APPLY_GATE}`, prStatusContext("apply", member)];
|
|
1010
|
+
const requires = await checkPrRequires(forge, args.pr, head, { ignore });
|
|
1011
|
+
for (const r of requires)
|
|
1012
|
+
console.error(formatInfo(`${r.name}: ${r.passed ? "passed" : r.pending ? "pending" : "unmet"}, ${r.reason}`));
|
|
1013
|
+
const unmet = unmetRequires(requires);
|
|
1014
|
+
if (unmet) {
|
|
1015
|
+
return early({ requires }, "requires-unmet", `${forge.kind === "gitlab" ? `Merge request !${args.pr}` : `Pull request ${args.pr}`} is not ready to apply before merge: ${unmet}. Nothing was applied.`, GATED_EXIT_CODE);
|
|
1016
|
+
}
|
|
1017
|
+
return { requires };
|
|
1018
|
+
}
|
|
1019
|
+
/** The refusal report of an apply before merge stopped before it planned (#4301): nothing selected, nothing planned. */
|
|
1020
|
+
function earlyStop(ctx, forge, at) {
|
|
1021
|
+
return async (beforeMerge, refusal, message, code) => {
|
|
1022
|
+
const plan = { order: [], waves: [], skipped: [], seeds: [], indeterminate: [] };
|
|
1023
|
+
const signal = { changed: [], indeterminate: [], unclaimed: [] };
|
|
1024
|
+
const p = { env: ctx.args.env ?? "local", base: at.base, head: at.head, gate: at.gate, forge, hookOutcomes: [], beforeMerge, ...(at.member ? { member: at.member } : {}) };
|
|
1025
|
+
const set = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1026
|
+
const report = prReport({ stage: "apply", pr: at.pr, env: p.env, ...(at.member ? { member: at.member } : {}), base: at.base, head: at.head, op: prOp(at.pr, at.member), gate: at.gate, plan, signal, set, approval: { status: "pending" }, status: "refused", refusal, message });
|
|
1027
|
+
return stopper(ctx, p, undefined, undefined)(report, code);
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Take `apply` locks on the targets an apply before merge reaches (#4293,
|
|
1032
|
+
* #4301), held for pull request `pr` until it merges or closes. Returns the
|
|
1033
|
+
* refusal when another open pull request holds one, null once taken, or the
|
|
1034
|
+
* exit code when the locks could not be written.
|
|
1035
|
+
*/
|
|
1036
|
+
async function takeApplyLocks(p, pr) {
|
|
1037
|
+
const { takeTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock.js");
|
|
1038
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
1039
|
+
const by = process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER ?? "unknown";
|
|
1040
|
+
let result;
|
|
1041
|
+
try {
|
|
1042
|
+
result = await takeTargetLocks(p.gate, p.plan.order, { pr, by, at: new Date().toISOString(), stage: "apply", head: p.head }, { ...(isOpen ? { isOpen } : {}) });
|
|
1043
|
+
}
|
|
1044
|
+
catch (err) {
|
|
1045
|
+
return fail(`could not take the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (result.ok) {
|
|
1048
|
+
if (result.taken.length > 0)
|
|
1049
|
+
console.error(formatInfo(`pull request ${pr} holds ${result.taken.join(", ")} on ${p.gate} until it merges or closes`));
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
const name = (n) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
1053
|
+
return `${describeHeldTargetLocks(result.held, name)}, so nothing was applied.`;
|
|
1054
|
+
}
|
|
649
1055
|
/** #4274: a component that published nothing is recorded by the plan digest its approval bound. */
|
|
650
1056
|
function prAppliedDigests(p, members) {
|
|
651
1057
|
return appliedDigestOf({ components: p.components, env: p.env, projectRoot: resolve("."), members });
|
|
@@ -681,6 +1087,7 @@ async function prRecord(ctx, stage) {
|
|
|
681
1087
|
}
|
|
682
1088
|
}
|
|
683
1089
|
const op = pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
1090
|
+
placeHooks(ctx, p, "record", op);
|
|
684
1091
|
const upload = openUpload(stage, "record", pr, p);
|
|
685
1092
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-record ${op}`, cwd: resolve(".") }, upload?.line);
|
|
686
1093
|
stage.stream = events;
|
|
@@ -733,33 +1140,36 @@ async function prRecord(ctx, stage) {
|
|
|
733
1140
|
});
|
|
734
1141
|
const { mergeSourceOf, writeMergeRecord, MERGE_RECORD_SCHEMA_ID, MERGE_RECORD_CONTRACT } = await import("../../lifecycle/merge-record.js");
|
|
735
1142
|
const { pushLifecycleReport } = await import("../../lifecycle/git.js");
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (
|
|
758
|
-
|
|
1143
|
+
// #4292: a member another branch applies has no merge on this one, so no record says it planned nothing here.
|
|
1144
|
+
if (!p.routedAway) {
|
|
1145
|
+
try {
|
|
1146
|
+
const written = await writeMergeRecord({
|
|
1147
|
+
$schema: MERGE_RECORD_SCHEMA_ID,
|
|
1148
|
+
contract: MERGE_RECORD_CONTRACT,
|
|
1149
|
+
commit: p.head,
|
|
1150
|
+
base: p.base,
|
|
1151
|
+
env: p.env,
|
|
1152
|
+
...(p.member ? { member: p.member } : {}),
|
|
1153
|
+
source: mergeSourceOf(resolve("."), p.head, pr),
|
|
1154
|
+
op: pr === null ? null : op,
|
|
1155
|
+
gate: p.gate,
|
|
1156
|
+
digest: nothing ? null : set.doc.digest,
|
|
1157
|
+
plan: nothing ? "nothing" : set.failed ? "plan-failed" : approval.status === "approved" ? "approved" : approval.status === "changed" ? "changed" : "unapproved",
|
|
1158
|
+
...(approval.status === "changed" && approval.approved ? { approvedDigest: approval.approved } : {}),
|
|
1159
|
+
approvedBy: approval.status === "approved" ? (approval.approvedBy ?? []) : [],
|
|
1160
|
+
...(authorApproved && authorApproved.length > 0 ? { authorApproved } : {}),
|
|
1161
|
+
components: p.plan.order,
|
|
1162
|
+
recordedAt: new Date().toISOString(),
|
|
1163
|
+
});
|
|
1164
|
+
if (written.written) {
|
|
1165
|
+
const pushed = await pushLifecycleReport();
|
|
1166
|
+
if (pushed.pushWarning)
|
|
1167
|
+
console.error(formatWarning({ message: pushed.pushWarning }));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
759
1172
|
}
|
|
760
|
-
}
|
|
761
|
-
catch (err) {
|
|
762
|
-
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
763
1173
|
}
|
|
764
1174
|
const stop = stopper(ctx, p, events, upload);
|
|
765
1175
|
return stop(report, status === "plan-failed" ? 1 : 0);
|
|
@@ -800,7 +1210,12 @@ async function greenTargetOf(ctx) {
|
|
|
800
1210
|
async function decideApplyGate(p, at) {
|
|
801
1211
|
const { op, pr, set, events, green } = at;
|
|
802
1212
|
let uncovered = [];
|
|
803
|
-
|
|
1213
|
+
// #4294: a plan the policy holds needs an approval of its own digest, so the merges' approvals do not cover it.
|
|
1214
|
+
if (green && at.held) {
|
|
1215
|
+
console.error(formatInfo(`the policy holds the plan at ${green.commit.slice(0, 12)}, so it needs an approval of its own digest and does not apply under the merges' approvals`));
|
|
1216
|
+
uncovered = ["the policy holds this plan for an approval of its own digest"];
|
|
1217
|
+
}
|
|
1218
|
+
else if (green) {
|
|
804
1219
|
const cover = coverByMerges(set.doc, green.approved);
|
|
805
1220
|
if (cover.covered) {
|
|
806
1221
|
console.error(formatInfo(`the approved plans of ${cover.coveredBy.length} merge${cover.coveredBy.length === 1 ? "" : "s"} cover the plan at ${green.commit.slice(0, 12)}, so it applies under them`));
|
|
@@ -1181,8 +1596,28 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1181
1596
|
const completed = new Set(carry?.completed ?? []);
|
|
1182
1597
|
const outputsSoFar = { ...(carry?.outputs ?? {}) };
|
|
1183
1598
|
const inputsSoFar = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]]));
|
|
1184
|
-
|
|
1599
|
+
// #4291: what failed in an earlier wave's job stays failed until it applies, so this job blocks its dependents and runs the rest.
|
|
1600
|
+
const failedSoFar = new Set((carry ? attempt?.failed ?? [] : []).filter((name) => !completed.has(name)));
|
|
1185
1601
|
let waves = carry?.record.waves ?? [];
|
|
1602
|
+
const upstreams = new Map(p.components.map((c) => [c.name, c.dependsOn ?? []]));
|
|
1603
|
+
/** Each component's outcome so far, past applied (#4291): failed, waiting, not-applied, or blocked by a failure. */
|
|
1604
|
+
const outcomes = () => {
|
|
1605
|
+
const out = new Map();
|
|
1606
|
+
for (const name of completed)
|
|
1607
|
+
out.set(name, { status: "applied" });
|
|
1608
|
+
for (const name of failedSoFar)
|
|
1609
|
+
out.set(name, { status: "failed" });
|
|
1610
|
+
for (const w of waves) {
|
|
1611
|
+
if (w.status !== "waiting" && w.status !== "plan-failed" && w.status !== "failed")
|
|
1612
|
+
continue;
|
|
1613
|
+
for (const name of w.components)
|
|
1614
|
+
if (!out.has(name))
|
|
1615
|
+
out.set(name, { status: w.status === "waiting" ? "waiting" : "not-applied" });
|
|
1616
|
+
}
|
|
1617
|
+
return out;
|
|
1618
|
+
};
|
|
1619
|
+
/** Components a failure blocked (#4291), each with the component that failed. */
|
|
1620
|
+
const blockedSoFar = new Map();
|
|
1186
1621
|
const after = new Map();
|
|
1187
1622
|
const leftIn = (k) => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
|
|
1188
1623
|
const firstLeft = () => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
|
|
@@ -1197,14 +1632,33 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1197
1632
|
const members = set.members.map((m) => {
|
|
1198
1633
|
const now = after.get(m.member);
|
|
1199
1634
|
if (now)
|
|
1200
|
-
return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
|
|
1201
|
-
|
|
1635
|
+
return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}), ...(now.blockedBy ? { blockedBy: now.blockedBy } : {}) };
|
|
1636
|
+
if (completed.has(m.component))
|
|
1637
|
+
return { ...m, status: "applied" };
|
|
1638
|
+
const by = blockedSoFar.get(m.component);
|
|
1639
|
+
if (by !== undefined)
|
|
1640
|
+
return { ...m, status: "blocked", blockedBy: by };
|
|
1641
|
+
return failedSoFar.has(m.component) ? { ...m, status: "failed" } : m;
|
|
1202
1642
|
});
|
|
1203
1643
|
return prReport({ ...reportBase, set, approval, members, waves, ...input });
|
|
1204
1644
|
};
|
|
1205
1645
|
// `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
|
|
1646
|
+
// #4291: an earlier member that failed, or was blocked by one that failed, counts as settled; it blocks its dependents here.
|
|
1206
1647
|
const left = firstLeft();
|
|
1207
|
-
|
|
1648
|
+
const earlierWaiting = only !== undefined ? waves.find((w) => w.wave < only && w.status === "waiting" && w.components.some((name) => !completed.has(name))) : undefined;
|
|
1649
|
+
if (earlierWaiting) {
|
|
1650
|
+
return stop(report({
|
|
1651
|
+
status: "refused",
|
|
1652
|
+
refusal: "earlier-wave",
|
|
1653
|
+
message: `Wave ${only} does not run yet: wave ${earlierWaiting.wave} waits at its gate ${earlierWaiting.gate}. Nothing was applied. Approve that wave and run its job again, then this one.`,
|
|
1654
|
+
}), GATED_EXIT_CODE);
|
|
1655
|
+
}
|
|
1656
|
+
const settled = only !== undefined && left > 0 && left < only ? settleEarlierWaves(p.plan.waves, only, upstreams, outcomes()) : undefined;
|
|
1657
|
+
if (settled)
|
|
1658
|
+
for (const [name, o] of settled.outcomes)
|
|
1659
|
+
if (o.status === "blocked" && o.by)
|
|
1660
|
+
blockedSoFar.set(name, o.by);
|
|
1661
|
+
if (only !== undefined && left > 0 && left < only && (settled?.unsettled.length ?? 0) > 0) {
|
|
1208
1662
|
return stop(report({
|
|
1209
1663
|
status: "refused",
|
|
1210
1664
|
refusal: "earlier-wave",
|
|
@@ -1213,6 +1667,7 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1213
1667
|
}), 1);
|
|
1214
1668
|
}
|
|
1215
1669
|
let planned;
|
|
1670
|
+
let judged = { held: false };
|
|
1216
1671
|
if (carry) {
|
|
1217
1672
|
// What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
|
|
1218
1673
|
p.plan.waves.forEach((names, i) => {
|
|
@@ -1231,7 +1686,11 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1231
1686
|
return stop(report({ status: "plan-failed", message: "A member failed to plan, so there is no plan to compare with the approved one. Nothing was applied." }), 1);
|
|
1232
1687
|
}
|
|
1233
1688
|
await storeChangeSetForGate(ctx, p, set.doc);
|
|
1234
|
-
|
|
1689
|
+
// #4294: the policy at base judges the whole change set, before the pull request's gate.
|
|
1690
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
1691
|
+
if (judged.stop)
|
|
1692
|
+
return stop(report({ status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
1693
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
1235
1694
|
approval = decided.approval;
|
|
1236
1695
|
if (!decided.satisfied)
|
|
1237
1696
|
return stop(report({ status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
@@ -1247,6 +1706,9 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1247
1706
|
emitReviewStop(events, prReview.message);
|
|
1248
1707
|
return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
|
|
1249
1708
|
}
|
|
1709
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
1710
|
+
if (unspent)
|
|
1711
|
+
return stop(report({ status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
1250
1712
|
const recordAttempt = () => {
|
|
1251
1713
|
if (!resumePath)
|
|
1252
1714
|
return;
|
|
@@ -1276,10 +1738,31 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1276
1738
|
recordAttempt();
|
|
1277
1739
|
const from = only ?? (left || total + 1);
|
|
1278
1740
|
const to = only ?? total;
|
|
1741
|
+
/** The last apply in this job that failed (#4291): its results give the exit code once every wave ran. */
|
|
1742
|
+
let lastFailure;
|
|
1743
|
+
let planFailedIn;
|
|
1279
1744
|
for (let k = from; k <= to; k++) {
|
|
1280
|
-
|
|
1281
|
-
|
|
1745
|
+
// #4291: a component of this wave whose upstream failed, or was blocked by a failure, is blocked; the rest of the wave runs.
|
|
1746
|
+
const before = new Set(p.plan.waves.slice(0, k - 1).flat());
|
|
1747
|
+
const mine = leftIn(k);
|
|
1748
|
+
for (const name of mine) {
|
|
1749
|
+
failedSoFar.delete(name);
|
|
1750
|
+
blockedSoFar.delete(name);
|
|
1751
|
+
}
|
|
1752
|
+
const outcomesNow = outcomes();
|
|
1753
|
+
for (const [name, by] of blockedSoFar)
|
|
1754
|
+
outcomesNow.set(name, { status: "blocked", by });
|
|
1755
|
+
const blockedNow = blockedMembers(mine, upstreams, before, outcomesNow);
|
|
1756
|
+
for (const [name, by] of blockedNow)
|
|
1757
|
+
blockedSoFar.set(name, by);
|
|
1758
|
+
if (blockedNow.size > 0)
|
|
1759
|
+
console.error(formatWarning({ message: `wave ${k}: ${blockedWords(blockedNow)}, so not planned or applied`, hint: "A failure blocks only the members that depend on it; the rest of the wave goes on." }));
|
|
1760
|
+
const wavePlan = prWavePlan(p.plan, p.components, k, [...completed, ...blockedNow.keys()]);
|
|
1761
|
+
if (wavePlan.order.length === 0) {
|
|
1762
|
+
if (blockedNow.size > 0)
|
|
1763
|
+
recordAttempt();
|
|
1282
1764
|
continue;
|
|
1765
|
+
}
|
|
1283
1766
|
const inThisWave = new Set(wavePlan.order);
|
|
1284
1767
|
// Wave 1 of the apply that just planned every member applies those plans:
|
|
1285
1768
|
// they are the approved set's. Every other wave is planned again, against
|
|
@@ -1296,19 +1779,25 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1296
1779
|
}
|
|
1297
1780
|
else {
|
|
1298
1781
|
const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
|
|
1299
|
-
const fresh = await planPrSet({
|
|
1782
|
+
const fresh = await hookedPlan(p, wavePlan.order, () => planPrSet({
|
|
1300
1783
|
components: p.components,
|
|
1301
1784
|
plan: wavePlan,
|
|
1302
1785
|
registry: p.registry,
|
|
1303
1786
|
env: p.env,
|
|
1304
1787
|
seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
|
|
1305
1788
|
...(events ? { onRunEvent: (body) => events.emit(body) } : {}),
|
|
1306
|
-
});
|
|
1789
|
+
}));
|
|
1307
1790
|
if (fresh.failed) {
|
|
1308
|
-
for (const m of fresh.members)
|
|
1791
|
+
for (const m of fresh.members) {
|
|
1309
1792
|
after.set(m.member, m);
|
|
1793
|
+
if (m.status === "plan-failed")
|
|
1794
|
+
failedSoFar.add(m.component);
|
|
1795
|
+
}
|
|
1310
1796
|
setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
|
|
1311
|
-
|
|
1797
|
+
// #4291: the later waves still run what does not depend on this wave.
|
|
1798
|
+
console.error(formatWarning({ message: `A member of wave ${k} failed to plan, so nothing in wave ${k} was applied.`, hint: "The waves after it apply what does not depend on wave " + k + "." }));
|
|
1799
|
+
planFailedIn ?? (planFailedIn = k);
|
|
1800
|
+
continue;
|
|
1312
1801
|
}
|
|
1313
1802
|
const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
|
|
1314
1803
|
set = fresh;
|
|
@@ -1357,6 +1846,12 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1357
1846
|
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "wave", gate, digest, moved, ...(waveApproval.approvedBy ? { approvedBy: waveApproval.approvedBy } : {}) };
|
|
1358
1847
|
}
|
|
1359
1848
|
}
|
|
1849
|
+
// #4288: the wave's apply-side hooks, around its apply.
|
|
1850
|
+
const waveBefore = runStageHook(p, "before-apply", wavePlan.order, undefined, `nothing in wave ${k} was applied`);
|
|
1851
|
+
if (waveBefore) {
|
|
1852
|
+
setWave({ ...entry, status: "failed" });
|
|
1853
|
+
return stop(report({ status: "failed", message: waveBefore }), 1);
|
|
1854
|
+
}
|
|
1360
1855
|
const applied = await applyPrSet({
|
|
1361
1856
|
components: p.components,
|
|
1362
1857
|
plan: wavePlan,
|
|
@@ -1373,6 +1868,7 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1373
1868
|
onComponentSettled: (settled, outputs) => {
|
|
1374
1869
|
if (settled.status === "ok") {
|
|
1375
1870
|
completed.add(settled.component);
|
|
1871
|
+
failedSoFar.delete(settled.component);
|
|
1376
1872
|
if (inputsNow[settled.component] !== undefined)
|
|
1377
1873
|
inputsSoFar[settled.component] = inputsNow[settled.component];
|
|
1378
1874
|
if (outputs)
|
|
@@ -1392,13 +1888,28 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1392
1888
|
if (outputs)
|
|
1393
1889
|
outputsSoFar[name] = outputs;
|
|
1394
1890
|
}
|
|
1395
|
-
|
|
1891
|
+
const waveAfter = runStageHook(p, "after-apply", wavePlan.order.filter((name) => applied.run.completed.includes(name)), undefined, `wave ${k} counts as failed though its components applied`);
|
|
1892
|
+
setWave({ ...entry, status: applied.status === "applied" && !waveAfter ? "applied" : "failed" });
|
|
1396
1893
|
await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
1397
1894
|
if (applied.status !== "applied") {
|
|
1398
|
-
|
|
1895
|
+
// #4291: a failed member blocks only its dependents, so the later waves still run.
|
|
1896
|
+
lastFailure = { results: applied.run.results, gated: applied.run.status === "gated" };
|
|
1897
|
+
recordAttempt();
|
|
1898
|
+
continue;
|
|
1399
1899
|
}
|
|
1900
|
+
if (waveAfter)
|
|
1901
|
+
return stop(report({ status: "failed", message: waveAfter }), 1);
|
|
1400
1902
|
console.error(formatSuccess(`applied wave ${k} of ${total} under ${entry.approval === "pull-request" ? `the pull request's approval of ${approvedSet.doc.digest}` : `${entry.gate}, approved for ${entry.digest}`} by ${(entry.approvedBy ?? []).join(", ")}`));
|
|
1401
1903
|
}
|
|
1904
|
+
// #4291: a failure, or a member it blocked, fails the job once every other member applied.
|
|
1905
|
+
const blockedHere = [...blockedSoFar.keys()].filter((name) => (only === undefined ? true : (p.plan.waves[only - 1] ?? []).includes(name)));
|
|
1906
|
+
if (lastFailure || planFailedIn !== undefined || blockedHere.length > 0) {
|
|
1907
|
+
const blockedLine = blockedSoFar.size > 0 ? ` Blocked: ${blockedWords(blockedSoFar)}.` : "";
|
|
1908
|
+
const message = planFailedIn !== undefined && !lastFailure
|
|
1909
|
+
? `A member of wave ${planFailedIn} failed to plan, so nothing in wave ${planFailedIn} was applied.${blockedLine}`
|
|
1910
|
+
: blockedLine ? blockedLine.trim() : undefined;
|
|
1911
|
+
return stop(report({ status: "failed", ...(message ? { message } : {}) }), lastFailure ? failedRunExitCode(lastFailure.results) : 1, lastFailure?.gated ? "gated" : undefined);
|
|
1912
|
+
}
|
|
1402
1913
|
const done = p.plan.order.every((name) => completed.has(name));
|
|
1403
1914
|
if (green && done)
|
|
1404
1915
|
await recordGreenApplied(p, green, approvedSet.doc.digest, approval, via);
|
|
@@ -1413,3 +1924,52 @@ function writeWaveChangeSet(ctx, wave, doc, upload) {
|
|
|
1413
1924
|
writeFileSync(join(dir, name), body);
|
|
1414
1925
|
upload?.file(name, body);
|
|
1415
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* chant components pr-post --pr <n> --head <sha> --env <env> --forge <kind> [--member <name>] [--output <dir>] [--result <result>] (#4295)
|
|
1929
|
+
*
|
|
1930
|
+
* Post the plan note and the `chant/plan` status from the report a plan job
|
|
1931
|
+
* wrote with `pr-plan --no-status`, in a job that runs none of the change's
|
|
1932
|
+
* code. The pull request, the head, the environment and the member come from
|
|
1933
|
+
* the flags, and a report that names others is refused (`../../pr-post.ts`).
|
|
1934
|
+
* It exits 0 once it has posted, whatever the plan said, so the plan job's
|
|
1935
|
+
* own result stays the one that fails the run.
|
|
1936
|
+
*/
|
|
1937
|
+
export async function runComponentsPrPost(ctx) {
|
|
1938
|
+
const { args } = ctx;
|
|
1939
|
+
if (args.pr === undefined)
|
|
1940
|
+
return fail("pr-post needs the pull or merge request: --pr <n>");
|
|
1941
|
+
if (!args.head || !/^[0-9a-f]{40}$/.test(args.head))
|
|
1942
|
+
return fail("pr-post needs the full commit the plan job checked out: --head <sha>");
|
|
1943
|
+
if (!args.env)
|
|
1944
|
+
return fail("pr-post needs the environment the plan was for: --env <env>");
|
|
1945
|
+
if (!args.forge || !FORGE_KINDS.includes(args.forge))
|
|
1946
|
+
return fail(`pr-post posts to a forge: --forge ${FORGE_KINDS.join("|")}`);
|
|
1947
|
+
if (args.members && args.members.length > 1)
|
|
1948
|
+
return fail(`pr-post takes one --member, not ${args.members.join(", ")}`);
|
|
1949
|
+
let forge;
|
|
1950
|
+
try {
|
|
1951
|
+
forge = forgeFromEnv(args.forge);
|
|
1952
|
+
}
|
|
1953
|
+
catch (err) {
|
|
1954
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
1955
|
+
}
|
|
1956
|
+
const { postPlanReport } = await import("../../pr-post.js");
|
|
1957
|
+
const member = args.members?.[0];
|
|
1958
|
+
const posted = await postPlanReport({
|
|
1959
|
+
forge,
|
|
1960
|
+
dir: resolve(args.output ?? PR_REPORT_DIR),
|
|
1961
|
+
pr: args.pr,
|
|
1962
|
+
head: args.head,
|
|
1963
|
+
env: args.env,
|
|
1964
|
+
...(member ? { member } : {}),
|
|
1965
|
+
...(args.planResult ? { result: args.planResult } : {}),
|
|
1966
|
+
// #4289: the plan job could not ask the forge, so the note job does, from the Ops at the commit it checked out.
|
|
1967
|
+
overdue: () => overdueOps(forge, member),
|
|
1968
|
+
});
|
|
1969
|
+
for (const message of posted.warnings)
|
|
1970
|
+
console.error(formatWarning({ message }));
|
|
1971
|
+
if (posted.refused)
|
|
1972
|
+
console.error(formatWarning({ message: `the plan report was not used: ${posted.refused}` }));
|
|
1973
|
+
console.error(formatInfo(`pr-post: ${prStatusContext("plan", member)} ${posted.state}: ${posted.description}${posted.noted ? "; note posted" : ""}`));
|
|
1974
|
+
return 0;
|
|
1975
|
+
}
|