@intentius/chant 0.124.39 → 0.124.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/component-endpoint.d.ts +20 -0
- package/dist/cli/component-endpoint.d.ts.map +1 -0
- package/dist/cli/component-endpoint.js +36 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +26 -4
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +74 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +514 -58
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +16 -2
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +17 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +41 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -2
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +35 -3
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +29 -2
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +2 -0
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/deploy-units.js +5 -0
- package/dist/components/fan-out-run.d.ts +28 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +27 -3
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +72 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +96 -5
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +5 -3
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +39 -16
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +6 -1
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +4 -3
- package/dist/lifecycle/publish-npm.d.ts +2 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +31 -8
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/release-ledger.d.ts +31 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +19 -1
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/status.d.ts +5 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/status.js +10 -5
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/live-endpoint.d.ts +18 -0
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +27 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +62 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +190 -11
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +36 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +82 -0
- package/dist/pr-loop.d.ts +90 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +77 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +239 -239
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +24 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +85 -6
- package/dist/workspace/declaration.d.ts +63 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +147 -0
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +73 -2
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts +3 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +7 -3
- package/dist/workspace/status.schema.json +1 -0
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/component-endpoint.ts +43 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/fan-out.test.ts +10 -0
- package/src/cli/handlers/fan-out.ts +29 -4
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-endpoint.test.ts +169 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +508 -64
- package/src/cli/handlers/promote.ts +25 -2
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.test.ts +2 -1
- package/src/cli/handlers/run.ts +16 -5
- package/src/cli/main.ts +41 -2
- package/src/cli/parse-args.ts +23 -2
- package/src/cli/registry.ts +12 -2
- package/src/components/auto-release-infra.test.ts +131 -0
- package/src/components/auto-release.test.ts +40 -1
- package/src/components/auto-release.ts +50 -4
- package/src/components/cli-support.ts +4 -0
- package/src/components/deploy-units.ts +5 -0
- package/src/components/fan-out-run.ts +39 -2
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +130 -5
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +5 -2
- package/src/components/promote.test.ts +39 -0
- package/src/components/promote.ts +38 -14
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +6 -1
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/publish-declaration.ts +4 -3
- package/src/lifecycle/publish-npm.ts +33 -8
- package/src/lifecycle/publish-targets.test.ts +36 -3
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/release-ledger.ts +42 -1
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/status.test.ts +19 -0
- package/src/lifecycle/status.ts +14 -6
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/live-endpoint.ts +31 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +233 -11
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +114 -0
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +146 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +133 -8
- package/src/workspace/ci-run.ts +92 -7
- package/src/workspace/declaration.schema.json +147 -0
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +134 -2
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +73 -2
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.schema.json +1 -0
- package/src/workspace/status.ts +11 -4
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
package/dist/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";
|
|
@@ -88,15 +90,19 @@ import { findWorkspaceRoot } from "../../project-root.js";
|
|
|
88
90
|
import { resolveChangedUnits } from "./fan-out.js";
|
|
89
91
|
import { coverByMerges, greenOp } from "../../components/green-cover.js";
|
|
90
92
|
import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run.js";
|
|
93
|
+
import { appliedDigestOf } from "../../components/auto-release.js";
|
|
94
|
+
import { withComponentEndpoint } from "../component-endpoint.js";
|
|
91
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";
|
|
92
98
|
/** Where the reports land unless `--output` says. */
|
|
93
99
|
export const PR_REPORT_DIR = ".chant/pr";
|
|
94
100
|
const fail = (message, hint) => {
|
|
95
101
|
console.error(formatError({ message, ...(hint ? { hint } : {}) }));
|
|
96
102
|
return 1;
|
|
97
103
|
};
|
|
98
|
-
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. */
|
|
99
|
-
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) {
|
|
100
106
|
const { args } = ctx;
|
|
101
107
|
const projectPath = resolve(".");
|
|
102
108
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} }));
|
|
@@ -176,8 +182,31 @@ async function prepare(ctx, stage) {
|
|
|
176
182
|
return fail(`--seed-outputs: could not read "${file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
177
183
|
}
|
|
178
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));
|
|
179
205
|
const env = args.env ?? "local";
|
|
180
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));
|
|
181
210
|
return {
|
|
182
211
|
config,
|
|
183
212
|
env,
|
|
@@ -187,12 +216,97 @@ async function prepare(ctx, stage) {
|
|
|
187
216
|
gate: args.gate ?? PR_APPLY_GATE,
|
|
188
217
|
...(forge ? { forge } : {}),
|
|
189
218
|
components: derived.components,
|
|
190
|
-
plan: derived.plan,
|
|
219
|
+
plan: routedAway ? { ...derived.plan, order: [], waves: [], seeds: [], indeterminate: [] } : derived.plan,
|
|
191
220
|
signal: derived.signal,
|
|
192
221
|
registry,
|
|
193
222
|
seededOutputs,
|
|
223
|
+
hooks,
|
|
224
|
+
hookOutcomes: [],
|
|
225
|
+
...(routedAway ? { routedAway } : {}),
|
|
226
|
+
policy,
|
|
194
227
|
};
|
|
195
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
|
+
}
|
|
196
310
|
/**
|
|
197
311
|
* The one `--member` name, checked against the workspace member that owns
|
|
198
312
|
* `projectPath`: a pipeline copied into another member would otherwise
|
|
@@ -221,8 +335,11 @@ async function checkMember(projectPath, names) {
|
|
|
221
335
|
}
|
|
222
336
|
return name;
|
|
223
337
|
}
|
|
224
|
-
/** Plan every member the derivation selects. */
|
|
225
|
-
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) {
|
|
226
343
|
return planPrSet({
|
|
227
344
|
components: p.components,
|
|
228
345
|
plan: p.plan,
|
|
@@ -284,7 +401,7 @@ async function publish(forge, report, note) {
|
|
|
284
401
|
return;
|
|
285
402
|
// A member the change does not reach has nothing to say: one note per
|
|
286
403
|
// member would put a line on every pull request for each member it skips.
|
|
287
|
-
if (report.member && report.status === "nothing")
|
|
404
|
+
if (report.member && report.status === "nothing" && !report.overdue?.length)
|
|
288
405
|
return;
|
|
289
406
|
try {
|
|
290
407
|
await forge.upsertNote(report.pr, prNoteMarker(report.env, report.member), note);
|
|
@@ -306,6 +423,15 @@ function printMembers(report) {
|
|
|
306
423
|
}
|
|
307
424
|
}
|
|
308
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 };
|
|
309
435
|
const note = noteFor(report, p.forge);
|
|
310
436
|
writeOutputs(ctx, report, note, upload);
|
|
311
437
|
if (!ctx.args.noStatus)
|
|
@@ -352,11 +478,90 @@ function openUpload(events, stage, pr, p) {
|
|
|
352
478
|
});
|
|
353
479
|
return events.upload;
|
|
354
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Run `ci.policy` at base over the planned set (#4294) and keep what it said
|
|
483
|
+
* on `p` for the report. A denial is decided against the override ledger:
|
|
484
|
+
* read only at the plan stage, and recorded for an override at the apply.
|
|
485
|
+
*/
|
|
486
|
+
async function judgePolicy(p, at) {
|
|
487
|
+
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 });
|
|
488
|
+
if (!verdict)
|
|
489
|
+
return { held: false };
|
|
490
|
+
const report = prPolicyReport(verdict, p.policy.base);
|
|
491
|
+
p.policyReport = report;
|
|
492
|
+
for (const w of verdict.warnings)
|
|
493
|
+
console.error(formatWarning({ message: `policy warning: ${w}` }));
|
|
494
|
+
if (verdict.result === "error") {
|
|
495
|
+
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}` } };
|
|
496
|
+
}
|
|
497
|
+
const held = verdict.holds.length > 0;
|
|
498
|
+
for (const h of verdict.holds)
|
|
499
|
+
console.error(formatInfo(`policy holds the plan for a person's approval (${h.rule}): ${h.message}`));
|
|
500
|
+
if (verdict.result !== "denied")
|
|
501
|
+
return { held };
|
|
502
|
+
const denied = verdict.denials.map((d) => `${d.rule}: ${d.message}`).join("; ");
|
|
503
|
+
let decided;
|
|
504
|
+
try {
|
|
505
|
+
// The plan stage records nothing, and reads the applied records so a spent override reads as spent.
|
|
506
|
+
const real = gitGateLedgerPort();
|
|
507
|
+
const port = at.stage === "plan" ? { ...readOnlyLedger(real), ...(real.readApplied ? { readApplied: real.readApplied.bind(real) } : {}) } : real;
|
|
508
|
+
decided = await decidePrPolicyOverride({
|
|
509
|
+
port,
|
|
510
|
+
subject: at.op,
|
|
511
|
+
digest: at.set.doc.digest,
|
|
512
|
+
rules: verdict.rules,
|
|
513
|
+
base: p.base,
|
|
514
|
+
cwd: resolve("."),
|
|
515
|
+
sealed: await gateIsSealed(p.gate),
|
|
516
|
+
record: at.stage === "apply",
|
|
517
|
+
description: `${at.op}: plan ${at.set.doc.digest} denied by ${verdict.rules.join(", ")}`,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
522
|
+
if (at.stage === "plan") {
|
|
523
|
+
console.error(formatWarning({ message: `could not read the override ledger, so no override reads as standing: ${why}` }));
|
|
524
|
+
return { held, stop: { refusal: "policy-denied", code: 1, message: `The policy denied this plan: ${denied}.` } };
|
|
525
|
+
}
|
|
526
|
+
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.` } };
|
|
527
|
+
}
|
|
528
|
+
if (decided.status === "overridden") {
|
|
529
|
+
report.override = decided.override;
|
|
530
|
+
console.error(formatInfo(`the policy denial (${verdict.rules.join(", ")}) is overridden by ${decided.override.by} at ${decided.override.at}: ${decided.override.reason}`));
|
|
531
|
+
return { held, override: decided.override };
|
|
532
|
+
}
|
|
533
|
+
report.overrideCommand = decided.command;
|
|
534
|
+
for (const why of decided.refusals)
|
|
535
|
+
console.error(formatInfo(`an override does not count: ${why}`));
|
|
536
|
+
const who = decided.overriders.length ? `${decided.overriders.join(", ")} may` : "nobody listed in .chant/overriders at base may";
|
|
537
|
+
const moved = decided.moved ? ` ${decided.moved.by} overrode an earlier plan or other rules, which counts for nothing here.` : "";
|
|
538
|
+
return {
|
|
539
|
+
held,
|
|
540
|
+
stop: {
|
|
541
|
+
refusal: "policy-denied",
|
|
542
|
+
code: at.stage === "plan" ? 1 : GATED_EXIT_CODE,
|
|
543
|
+
message: `The policy denied this plan: ${denied}.${moved} Nothing was applied. ${who} override it: ${decided.command}`,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/** Spend the override the apply runs under (#4294), before anything applies. The failure's message, or undefined. */
|
|
548
|
+
async function spendPolicy(op, judged, head) {
|
|
549
|
+
if (!judged.override)
|
|
550
|
+
return undefined;
|
|
551
|
+
try {
|
|
552
|
+
await spendPrPolicyOverride(gitGateLedgerPort(), op, judged.override, { commit: head });
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
return `Could not record the override this apply runs under, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`;
|
|
556
|
+
}
|
|
557
|
+
console.error(formatInfo(`applies under the override by ${judged.override.by}, which is now spent`));
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
355
560
|
/** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
356
561
|
export function runComponentsPrPlan(ctx) {
|
|
357
562
|
if (refusesEventsWithJson(ctx.args, "the stage's report"))
|
|
358
563
|
return Promise.resolve(1);
|
|
359
|
-
return withStageEvents("pr-plan", ctx, (events) => prPlan(ctx, events));
|
|
564
|
+
return withStageEvents("pr-plan", ctx, (events) => withComponentEndpoint(ctx, () => prPlan(ctx, events)));
|
|
360
565
|
}
|
|
361
566
|
async function prPlan(ctx, stage) {
|
|
362
567
|
if (ctx.args.pr === undefined) {
|
|
@@ -366,12 +571,15 @@ async function prPlan(ctx, stage) {
|
|
|
366
571
|
if (typeof p === "number")
|
|
367
572
|
return p;
|
|
368
573
|
const op = prOp(ctx.args.pr, p.member);
|
|
574
|
+
placeHooks(ctx, p, "plan", op);
|
|
369
575
|
const upload = openUpload(stage, "plan", ctx.args.pr, p);
|
|
370
576
|
// `--events-json` (#4041): planning, then where the approval stands.
|
|
371
577
|
const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") }, upload?.line);
|
|
372
578
|
stage.stream = events;
|
|
373
579
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
374
580
|
const set = await planAll(p, events?.emit);
|
|
581
|
+
// #4294: the policy at base over the planned change set.
|
|
582
|
+
const judged = p.plan.order.length > 0 && !set.failed ? await judgePolicy(p, { stage: "plan", op, pr: ctx.args.pr, set }) : { held: false };
|
|
375
583
|
let approval = { status: "pending" };
|
|
376
584
|
if (p.plan.order.length > 0) {
|
|
377
585
|
try {
|
|
@@ -383,7 +591,8 @@ async function prPlan(ctx, stage) {
|
|
|
383
591
|
}
|
|
384
592
|
}
|
|
385
593
|
const status = p.plan.order.length === 0 ? "nothing" : set.failed ? "plan-failed" : "planned";
|
|
386
|
-
const
|
|
594
|
+
const overdue = await overdueOps(p.forge, p.member);
|
|
595
|
+
const planned = prReport({
|
|
387
596
|
stage: "plan",
|
|
388
597
|
pr: ctx.args.pr,
|
|
389
598
|
env: p.env,
|
|
@@ -399,6 +608,7 @@ async function prPlan(ctx, stage) {
|
|
|
399
608
|
status,
|
|
400
609
|
...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
|
|
401
610
|
});
|
|
611
|
+
const report = overdue.length > 0 ? { ...planned, overdue } : planned;
|
|
402
612
|
await finish(ctx, p, report, upload);
|
|
403
613
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
404
614
|
events?.settle(settleOf(status), status === "plan-failed" ? "a member failed to plan" : undefined);
|
|
@@ -408,13 +618,49 @@ async function prPlan(ctx, stage) {
|
|
|
408
618
|
console.error(formatInfo(`plan : ${report.digest}`));
|
|
409
619
|
console.error(formatInfo(approval.status === "approved" ? `approved by ${(approval.approvedBy ?? []).join(", ")}` : `approve : ${prApproveCommand(report)}`));
|
|
410
620
|
}
|
|
411
|
-
|
|
621
|
+
if (judged.stop)
|
|
622
|
+
console.error(formatWarning({ message: judged.stop.message }));
|
|
623
|
+
return status === "plan-failed" || judged.stop ? 1 : 0;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The scheduled watch and reconcile Ops that stopped running (#4289,
|
|
627
|
+
* `op/schedule-overdue.ts`), asked of the forge. Without a forge, or when
|
|
628
|
+
* the Ops cannot be read, nothing is checked and the plan goes on. The
|
|
629
|
+
* generated plan holds no forge token (#4295), so there the plan-note job's
|
|
630
|
+
* `pr-post` asks instead, reading the Ops at the target branch's commit.
|
|
631
|
+
*/
|
|
632
|
+
async function overdueOps(forge, member) {
|
|
633
|
+
if (!forge?.scheduledRuns)
|
|
634
|
+
return [];
|
|
635
|
+
try {
|
|
636
|
+
const { discoverOps } = await import("../../op/discover.js");
|
|
637
|
+
const { scheduledWatches, checkOverdueOps } = await import("../../op/schedule-overdue.js");
|
|
638
|
+
const discovered = await discoverOps({ cwd: resolve(".") });
|
|
639
|
+
const watches = scheduledWatches([...discovered.ops.values()].map((d) => d.config));
|
|
640
|
+
if (watches.length === 0)
|
|
641
|
+
return [];
|
|
642
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
|
|
643
|
+
if (top.status !== 0)
|
|
644
|
+
return [];
|
|
645
|
+
return await checkOverdueOps({
|
|
646
|
+
watches,
|
|
647
|
+
forge,
|
|
648
|
+
repoRoot: top.stdout.trim(),
|
|
649
|
+
...(member ? { member } : {}),
|
|
650
|
+
now: new Date(),
|
|
651
|
+
log: (line) => console.error(formatWarning({ message: line })),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
console.error(formatWarning({ message: `could not check the scheduled Ops, so none is reported overdue: ${err instanceof Error ? err.message : String(err)}` }));
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
412
658
|
}
|
|
413
659
|
/** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
414
660
|
export function runComponentsPrApply(ctx) {
|
|
415
661
|
if (refusesEventsWithJson(ctx.args, "the stage's report"))
|
|
416
662
|
return Promise.resolve(1);
|
|
417
|
-
return withStageEvents("pr-apply", ctx, (events) => prApply(ctx, events));
|
|
663
|
+
return withStageEvents("pr-apply", ctx, (events) => withComponentEndpoint(ctx, () => prApply(ctx, events)));
|
|
418
664
|
}
|
|
419
665
|
async function prApply(ctx, stage) {
|
|
420
666
|
// #4168: the review-rules security group turns --require-review on, read from the declaration at base.
|
|
@@ -424,6 +670,9 @@ async function prApply(ctx, stage) {
|
|
|
424
670
|
return fail("the workspace opts into review-rules, which reads the pull request's reviews, so pr-apply needs --forge <kind>");
|
|
425
671
|
if (reviewRules)
|
|
426
672
|
ctx = { ...ctx, args: { ...ctx.args, requireReview: true } };
|
|
673
|
+
// #4293: ci.locks, read at base like the security groups, so the change cannot turn its own locks off.
|
|
674
|
+
const { ciLocksAt } = await import("../../workspace/ci-locks.js");
|
|
675
|
+
const locks = ciLocksAt(process.cwd(), ctx.args.base);
|
|
427
676
|
const { args } = ctx;
|
|
428
677
|
if (args.requireReview && !args.forge)
|
|
429
678
|
return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
|
|
@@ -453,6 +702,7 @@ async function prApply(ctx, stage) {
|
|
|
453
702
|
}
|
|
454
703
|
}
|
|
455
704
|
const op = green ? greenOp(green.commit, p.member) : pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
705
|
+
placeHooks(ctx, p, "apply", op);
|
|
456
706
|
// The run id the release records carry, chosen before the run so that
|
|
457
707
|
// `--events-json` (#4041, #4007) names the run by it too.
|
|
458
708
|
const release = autoReleaseRun(`${op}-${Date.now()}`);
|
|
@@ -460,6 +710,15 @@ async function prApply(ctx, stage) {
|
|
|
460
710
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
461
711
|
stage.stream = events;
|
|
462
712
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
713
|
+
if (locks && p.plan.order.length > 0) {
|
|
714
|
+
const held = await heldLocksOf(p, pr);
|
|
715
|
+
if (typeof held === "number")
|
|
716
|
+
return held;
|
|
717
|
+
if (held) {
|
|
718
|
+
const set = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
719
|
+
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);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
463
722
|
if (args.waveGate)
|
|
464
723
|
return prApplyWaves(ctx, p, { pr, op, release, upload, events, ...(green ? { green } : {}) });
|
|
465
724
|
// `--resume` (#3464): an earlier attempt at this change on this commit
|
|
@@ -519,11 +778,16 @@ async function prApply(ctx, stage) {
|
|
|
519
778
|
await storeChangeSetForGate(ctx, p, set.doc);
|
|
520
779
|
let approval;
|
|
521
780
|
let via = "gate";
|
|
781
|
+
// #4294: the policy at base, before the gate. A resume runs under the decision of the attempt it finishes.
|
|
782
|
+
let judged = { held: false };
|
|
522
783
|
if (resumed) {
|
|
523
784
|
approval = resumed.approval;
|
|
524
785
|
}
|
|
525
786
|
else {
|
|
526
|
-
|
|
787
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
788
|
+
if (judged.stop)
|
|
789
|
+
return stop(prReport({ ...base, approval: { status: "pending" }, status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
790
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
527
791
|
approval = decided.approval;
|
|
528
792
|
if (!decided.satisfied)
|
|
529
793
|
return stop(prReport({ ...base, approval, status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
@@ -537,6 +801,9 @@ async function prApply(ctx, stage) {
|
|
|
537
801
|
emitReviewStop(events, review.message);
|
|
538
802
|
return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
|
|
539
803
|
}
|
|
804
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
805
|
+
if (unspent)
|
|
806
|
+
return stop(prReport({ ...base, approval, status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
540
807
|
// The record is written before the first component runs and again as each
|
|
541
808
|
// one settles, so a run killed partway still knows what applied.
|
|
542
809
|
const prior = resumed ? { completed: resumed.completed, outputs: resumed.outputs } : { completed: [], outputs: {} };
|
|
@@ -567,6 +834,11 @@ async function prApply(ctx, stage) {
|
|
|
567
834
|
});
|
|
568
835
|
};
|
|
569
836
|
recordAttempt();
|
|
837
|
+
// #4288: the before-apply hooks, once the gate and the review let the apply through.
|
|
838
|
+
const toApply = (resumed?.plan ?? p.plan).order.filter((name) => !completedSoFar.has(name));
|
|
839
|
+
const beforeFailed = runStageHook(p, "before-apply", toApply);
|
|
840
|
+
if (beforeFailed)
|
|
841
|
+
return stop(prReport({ ...base, approval, status: "failed", message: beforeFailed }), 1);
|
|
570
842
|
// #3999: a round planned after an upstream applied waits on a gate of its
|
|
571
843
|
// own, under the same identity.gates rule and the same review checks.
|
|
572
844
|
const roundLedger = gitGateLedgerPort();
|
|
@@ -623,11 +895,13 @@ async function prApply(ctx, stage) {
|
|
|
623
895
|
: {}),
|
|
624
896
|
});
|
|
625
897
|
recordAttempt();
|
|
626
|
-
|
|
898
|
+
// #4288: the after-apply hooks, for the components that applied.
|
|
899
|
+
const afterFailed = runStageHook(p, "after-apply", toApply.filter((name) => applied.run.completed.includes(name)), undefined, "the apply counts as failed though its components applied");
|
|
900
|
+
await recordAutoReleasesForRun(applied.run.results, p.env, release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
627
901
|
const report = prReport({
|
|
628
902
|
...base,
|
|
629
903
|
approval,
|
|
630
|
-
status: applied.status,
|
|
904
|
+
status: afterFailed ? "failed" : applied.status,
|
|
631
905
|
members: applied.members,
|
|
632
906
|
...(resumed ? { resumed: resumed.completed } : {}),
|
|
633
907
|
...(applied.rounds ? { rounds: applied.rounds } : {}),
|
|
@@ -636,19 +910,47 @@ async function prApply(ctx, stage) {
|
|
|
636
910
|
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
637
911
|
// a person. A step that refused its plan because the target moved since
|
|
638
912
|
// the approval exits 3, as a moved digest does before anything applies (#3997).
|
|
639
|
-
const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
913
|
+
const exit = afterFailed ? 1 : applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
640
914
|
if (green && applied.status === "applied")
|
|
641
915
|
await recordGreenApplied(p, green, set.doc.digest, approval, via);
|
|
642
916
|
const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
|
|
643
|
-
if (applied.status === "applied")
|
|
917
|
+
if (applied.status === "applied" && !afterFailed)
|
|
644
918
|
console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
645
919
|
return code;
|
|
646
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Under `ci.locks` (#4293): the refusal when another open pull request holds
|
|
923
|
+
* a lock on a component this apply reaches, else null. The targets are the
|
|
924
|
+
* selected components, the names `chant lock` takes. A lock whose pull
|
|
925
|
+
* request the forge says merged or closed reads as released; with no
|
|
926
|
+
* `--forge` every lock counts. A lock file that cannot be read stops the
|
|
927
|
+
* apply, since nothing then says the targets are free.
|
|
928
|
+
*/
|
|
929
|
+
async function heldLocksOf(p, pr) {
|
|
930
|
+
const { heldTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock.js");
|
|
931
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
932
|
+
let held;
|
|
933
|
+
try {
|
|
934
|
+
held = await heldTargetLocks(p.gate, p.plan.order, pr, { ...(isOpen ? { isOpen } : {}) });
|
|
935
|
+
}
|
|
936
|
+
catch (err) {
|
|
937
|
+
return fail(`could not read the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
938
|
+
}
|
|
939
|
+
if (held.length === 0)
|
|
940
|
+
return null;
|
|
941
|
+
const name = (n) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
942
|
+
const holders = [...new Set(held.map((h) => h.pr))].sort((a, b) => a - b);
|
|
943
|
+
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"}.`;
|
|
944
|
+
}
|
|
945
|
+
/** #4274: a component that published nothing is recorded by the plan digest its approval bound. */
|
|
946
|
+
function prAppliedDigests(p, members) {
|
|
947
|
+
return appliedDigestOf({ components: p.components, env: p.env, projectRoot: resolve("."), members });
|
|
948
|
+
}
|
|
647
949
|
/** chant components pr-record --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
648
950
|
export function runComponentsPrRecord(ctx) {
|
|
649
951
|
if (refusesEventsWithJson(ctx.args, "the stage's report"))
|
|
650
952
|
return Promise.resolve(1);
|
|
651
|
-
return withStageEvents("pr-record", ctx, (events) => prRecord(ctx, events));
|
|
953
|
+
return withStageEvents("pr-record", ctx, (events) => withComponentEndpoint(ctx, () => prRecord(ctx, events)));
|
|
652
954
|
}
|
|
653
955
|
/**
|
|
654
956
|
* The record stage (#4168 step 1): what a merge does unless apply on merge
|
|
@@ -675,6 +977,7 @@ async function prRecord(ctx, stage) {
|
|
|
675
977
|
}
|
|
676
978
|
}
|
|
677
979
|
const op = pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
980
|
+
placeHooks(ctx, p, "record", op);
|
|
678
981
|
const upload = openUpload(stage, "record", pr, p);
|
|
679
982
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-record ${op}`, cwd: resolve(".") }, upload?.line);
|
|
680
983
|
stage.stream = events;
|
|
@@ -727,33 +1030,36 @@ async function prRecord(ctx, stage) {
|
|
|
727
1030
|
});
|
|
728
1031
|
const { mergeSourceOf, writeMergeRecord, MERGE_RECORD_SCHEMA_ID, MERGE_RECORD_CONTRACT } = await import("../../lifecycle/merge-record.js");
|
|
729
1032
|
const { pushLifecycleReport } = await import("../../lifecycle/git.js");
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
1033
|
+
// #4292: a member another branch applies has no merge on this one, so no record says it planned nothing here.
|
|
1034
|
+
if (!p.routedAway) {
|
|
1035
|
+
try {
|
|
1036
|
+
const written = await writeMergeRecord({
|
|
1037
|
+
$schema: MERGE_RECORD_SCHEMA_ID,
|
|
1038
|
+
contract: MERGE_RECORD_CONTRACT,
|
|
1039
|
+
commit: p.head,
|
|
1040
|
+
base: p.base,
|
|
1041
|
+
env: p.env,
|
|
1042
|
+
...(p.member ? { member: p.member } : {}),
|
|
1043
|
+
source: mergeSourceOf(resolve("."), p.head, pr),
|
|
1044
|
+
op: pr === null ? null : op,
|
|
1045
|
+
gate: p.gate,
|
|
1046
|
+
digest: nothing ? null : set.doc.digest,
|
|
1047
|
+
plan: nothing ? "nothing" : set.failed ? "plan-failed" : approval.status === "approved" ? "approved" : approval.status === "changed" ? "changed" : "unapproved",
|
|
1048
|
+
...(approval.status === "changed" && approval.approved ? { approvedDigest: approval.approved } : {}),
|
|
1049
|
+
approvedBy: approval.status === "approved" ? (approval.approvedBy ?? []) : [],
|
|
1050
|
+
...(authorApproved && authorApproved.length > 0 ? { authorApproved } : {}),
|
|
1051
|
+
components: p.plan.order,
|
|
1052
|
+
recordedAt: new Date().toISOString(),
|
|
1053
|
+
});
|
|
1054
|
+
if (written.written) {
|
|
1055
|
+
const pushed = await pushLifecycleReport();
|
|
1056
|
+
if (pushed.pushWarning)
|
|
1057
|
+
console.error(formatWarning({ message: pushed.pushWarning }));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
catch (err) {
|
|
1061
|
+
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
753
1062
|
}
|
|
754
|
-
}
|
|
755
|
-
catch (err) {
|
|
756
|
-
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
757
1063
|
}
|
|
758
1064
|
const stop = stopper(ctx, p, events, upload);
|
|
759
1065
|
return stop(report, status === "plan-failed" ? 1 : 0);
|
|
@@ -794,7 +1100,12 @@ async function greenTargetOf(ctx) {
|
|
|
794
1100
|
async function decideApplyGate(p, at) {
|
|
795
1101
|
const { op, pr, set, events, green } = at;
|
|
796
1102
|
let uncovered = [];
|
|
797
|
-
|
|
1103
|
+
// #4294: a plan the policy holds needs an approval of its own digest, so the merges' approvals do not cover it.
|
|
1104
|
+
if (green && at.held) {
|
|
1105
|
+
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`));
|
|
1106
|
+
uncovered = ["the policy holds this plan for an approval of its own digest"];
|
|
1107
|
+
}
|
|
1108
|
+
else if (green) {
|
|
798
1109
|
const cover = coverByMerges(set.doc, green.approved);
|
|
799
1110
|
if (cover.covered) {
|
|
800
1111
|
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`));
|
|
@@ -1175,8 +1486,28 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1175
1486
|
const completed = new Set(carry?.completed ?? []);
|
|
1176
1487
|
const outputsSoFar = { ...(carry?.outputs ?? {}) };
|
|
1177
1488
|
const inputsSoFar = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]]));
|
|
1178
|
-
|
|
1489
|
+
// #4291: what failed in an earlier wave's job stays failed until it applies, so this job blocks its dependents and runs the rest.
|
|
1490
|
+
const failedSoFar = new Set((carry ? attempt?.failed ?? [] : []).filter((name) => !completed.has(name)));
|
|
1179
1491
|
let waves = carry?.record.waves ?? [];
|
|
1492
|
+
const upstreams = new Map(p.components.map((c) => [c.name, c.dependsOn ?? []]));
|
|
1493
|
+
/** Each component's outcome so far, past applied (#4291): failed, waiting, not-applied, or blocked by a failure. */
|
|
1494
|
+
const outcomes = () => {
|
|
1495
|
+
const out = new Map();
|
|
1496
|
+
for (const name of completed)
|
|
1497
|
+
out.set(name, { status: "applied" });
|
|
1498
|
+
for (const name of failedSoFar)
|
|
1499
|
+
out.set(name, { status: "failed" });
|
|
1500
|
+
for (const w of waves) {
|
|
1501
|
+
if (w.status !== "waiting" && w.status !== "plan-failed" && w.status !== "failed")
|
|
1502
|
+
continue;
|
|
1503
|
+
for (const name of w.components)
|
|
1504
|
+
if (!out.has(name))
|
|
1505
|
+
out.set(name, { status: w.status === "waiting" ? "waiting" : "not-applied" });
|
|
1506
|
+
}
|
|
1507
|
+
return out;
|
|
1508
|
+
};
|
|
1509
|
+
/** Components a failure blocked (#4291), each with the component that failed. */
|
|
1510
|
+
const blockedSoFar = new Map();
|
|
1180
1511
|
const after = new Map();
|
|
1181
1512
|
const leftIn = (k) => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
|
|
1182
1513
|
const firstLeft = () => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
|
|
@@ -1191,14 +1522,33 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1191
1522
|
const members = set.members.map((m) => {
|
|
1192
1523
|
const now = after.get(m.member);
|
|
1193
1524
|
if (now)
|
|
1194
|
-
return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
|
|
1195
|
-
|
|
1525
|
+
return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}), ...(now.blockedBy ? { blockedBy: now.blockedBy } : {}) };
|
|
1526
|
+
if (completed.has(m.component))
|
|
1527
|
+
return { ...m, status: "applied" };
|
|
1528
|
+
const by = blockedSoFar.get(m.component);
|
|
1529
|
+
if (by !== undefined)
|
|
1530
|
+
return { ...m, status: "blocked", blockedBy: by };
|
|
1531
|
+
return failedSoFar.has(m.component) ? { ...m, status: "failed" } : m;
|
|
1196
1532
|
});
|
|
1197
1533
|
return prReport({ ...reportBase, set, approval, members, waves, ...input });
|
|
1198
1534
|
};
|
|
1199
1535
|
// `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
|
|
1536
|
+
// #4291: an earlier member that failed, or was blocked by one that failed, counts as settled; it blocks its dependents here.
|
|
1200
1537
|
const left = firstLeft();
|
|
1201
|
-
|
|
1538
|
+
const earlierWaiting = only !== undefined ? waves.find((w) => w.wave < only && w.status === "waiting" && w.components.some((name) => !completed.has(name))) : undefined;
|
|
1539
|
+
if (earlierWaiting) {
|
|
1540
|
+
return stop(report({
|
|
1541
|
+
status: "refused",
|
|
1542
|
+
refusal: "earlier-wave",
|
|
1543
|
+
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.`,
|
|
1544
|
+
}), GATED_EXIT_CODE);
|
|
1545
|
+
}
|
|
1546
|
+
const settled = only !== undefined && left > 0 && left < only ? settleEarlierWaves(p.plan.waves, only, upstreams, outcomes()) : undefined;
|
|
1547
|
+
if (settled)
|
|
1548
|
+
for (const [name, o] of settled.outcomes)
|
|
1549
|
+
if (o.status === "blocked" && o.by)
|
|
1550
|
+
blockedSoFar.set(name, o.by);
|
|
1551
|
+
if (only !== undefined && left > 0 && left < only && (settled?.unsettled.length ?? 0) > 0) {
|
|
1202
1552
|
return stop(report({
|
|
1203
1553
|
status: "refused",
|
|
1204
1554
|
refusal: "earlier-wave",
|
|
@@ -1207,6 +1557,7 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1207
1557
|
}), 1);
|
|
1208
1558
|
}
|
|
1209
1559
|
let planned;
|
|
1560
|
+
let judged = { held: false };
|
|
1210
1561
|
if (carry) {
|
|
1211
1562
|
// What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
|
|
1212
1563
|
p.plan.waves.forEach((names, i) => {
|
|
@@ -1225,7 +1576,11 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1225
1576
|
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);
|
|
1226
1577
|
}
|
|
1227
1578
|
await storeChangeSetForGate(ctx, p, set.doc);
|
|
1228
|
-
|
|
1579
|
+
// #4294: the policy at base judges the whole change set, before the pull request's gate.
|
|
1580
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
1581
|
+
if (judged.stop)
|
|
1582
|
+
return stop(report({ status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
1583
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
1229
1584
|
approval = decided.approval;
|
|
1230
1585
|
if (!decided.satisfied)
|
|
1231
1586
|
return stop(report({ status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
@@ -1241,6 +1596,9 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1241
1596
|
emitReviewStop(events, prReview.message);
|
|
1242
1597
|
return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
|
|
1243
1598
|
}
|
|
1599
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
1600
|
+
if (unspent)
|
|
1601
|
+
return stop(report({ status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
1244
1602
|
const recordAttempt = () => {
|
|
1245
1603
|
if (!resumePath)
|
|
1246
1604
|
return;
|
|
@@ -1270,10 +1628,31 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1270
1628
|
recordAttempt();
|
|
1271
1629
|
const from = only ?? (left || total + 1);
|
|
1272
1630
|
const to = only ?? total;
|
|
1631
|
+
/** The last apply in this job that failed (#4291): its results give the exit code once every wave ran. */
|
|
1632
|
+
let lastFailure;
|
|
1633
|
+
let planFailedIn;
|
|
1273
1634
|
for (let k = from; k <= to; k++) {
|
|
1274
|
-
|
|
1275
|
-
|
|
1635
|
+
// #4291: a component of this wave whose upstream failed, or was blocked by a failure, is blocked; the rest of the wave runs.
|
|
1636
|
+
const before = new Set(p.plan.waves.slice(0, k - 1).flat());
|
|
1637
|
+
const mine = leftIn(k);
|
|
1638
|
+
for (const name of mine) {
|
|
1639
|
+
failedSoFar.delete(name);
|
|
1640
|
+
blockedSoFar.delete(name);
|
|
1641
|
+
}
|
|
1642
|
+
const outcomesNow = outcomes();
|
|
1643
|
+
for (const [name, by] of blockedSoFar)
|
|
1644
|
+
outcomesNow.set(name, { status: "blocked", by });
|
|
1645
|
+
const blockedNow = blockedMembers(mine, upstreams, before, outcomesNow);
|
|
1646
|
+
for (const [name, by] of blockedNow)
|
|
1647
|
+
blockedSoFar.set(name, by);
|
|
1648
|
+
if (blockedNow.size > 0)
|
|
1649
|
+
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." }));
|
|
1650
|
+
const wavePlan = prWavePlan(p.plan, p.components, k, [...completed, ...blockedNow.keys()]);
|
|
1651
|
+
if (wavePlan.order.length === 0) {
|
|
1652
|
+
if (blockedNow.size > 0)
|
|
1653
|
+
recordAttempt();
|
|
1276
1654
|
continue;
|
|
1655
|
+
}
|
|
1277
1656
|
const inThisWave = new Set(wavePlan.order);
|
|
1278
1657
|
// Wave 1 of the apply that just planned every member applies those plans:
|
|
1279
1658
|
// they are the approved set's. Every other wave is planned again, against
|
|
@@ -1290,19 +1669,25 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1290
1669
|
}
|
|
1291
1670
|
else {
|
|
1292
1671
|
const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
|
|
1293
|
-
const fresh = await planPrSet({
|
|
1672
|
+
const fresh = await hookedPlan(p, wavePlan.order, () => planPrSet({
|
|
1294
1673
|
components: p.components,
|
|
1295
1674
|
plan: wavePlan,
|
|
1296
1675
|
registry: p.registry,
|
|
1297
1676
|
env: p.env,
|
|
1298
1677
|
seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
|
|
1299
1678
|
...(events ? { onRunEvent: (body) => events.emit(body) } : {}),
|
|
1300
|
-
});
|
|
1679
|
+
}));
|
|
1301
1680
|
if (fresh.failed) {
|
|
1302
|
-
for (const m of fresh.members)
|
|
1681
|
+
for (const m of fresh.members) {
|
|
1303
1682
|
after.set(m.member, m);
|
|
1683
|
+
if (m.status === "plan-failed")
|
|
1684
|
+
failedSoFar.add(m.component);
|
|
1685
|
+
}
|
|
1304
1686
|
setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
|
|
1305
|
-
|
|
1687
|
+
// #4291: the later waves still run what does not depend on this wave.
|
|
1688
|
+
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 + "." }));
|
|
1689
|
+
planFailedIn ?? (planFailedIn = k);
|
|
1690
|
+
continue;
|
|
1306
1691
|
}
|
|
1307
1692
|
const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
|
|
1308
1693
|
set = fresh;
|
|
@@ -1351,6 +1736,12 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1351
1736
|
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "wave", gate, digest, moved, ...(waveApproval.approvedBy ? { approvedBy: waveApproval.approvedBy } : {}) };
|
|
1352
1737
|
}
|
|
1353
1738
|
}
|
|
1739
|
+
// #4288: the wave's apply-side hooks, around its apply.
|
|
1740
|
+
const waveBefore = runStageHook(p, "before-apply", wavePlan.order, undefined, `nothing in wave ${k} was applied`);
|
|
1741
|
+
if (waveBefore) {
|
|
1742
|
+
setWave({ ...entry, status: "failed" });
|
|
1743
|
+
return stop(report({ status: "failed", message: waveBefore }), 1);
|
|
1744
|
+
}
|
|
1354
1745
|
const applied = await applyPrSet({
|
|
1355
1746
|
components: p.components,
|
|
1356
1747
|
plan: wavePlan,
|
|
@@ -1367,6 +1758,7 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1367
1758
|
onComponentSettled: (settled, outputs) => {
|
|
1368
1759
|
if (settled.status === "ok") {
|
|
1369
1760
|
completed.add(settled.component);
|
|
1761
|
+
failedSoFar.delete(settled.component);
|
|
1370
1762
|
if (inputsNow[settled.component] !== undefined)
|
|
1371
1763
|
inputsSoFar[settled.component] = inputsNow[settled.component];
|
|
1372
1764
|
if (outputs)
|
|
@@ -1386,13 +1778,28 @@ async function prApplyWaves(ctx, p, at) {
|
|
|
1386
1778
|
if (outputs)
|
|
1387
1779
|
outputsSoFar[name] = outputs;
|
|
1388
1780
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1781
|
+
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`);
|
|
1782
|
+
setWave({ ...entry, status: applied.status === "applied" && !waveAfter ? "applied" : "failed" });
|
|
1783
|
+
await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
1391
1784
|
if (applied.status !== "applied") {
|
|
1392
|
-
|
|
1785
|
+
// #4291: a failed member blocks only its dependents, so the later waves still run.
|
|
1786
|
+
lastFailure = { results: applied.run.results, gated: applied.run.status === "gated" };
|
|
1787
|
+
recordAttempt();
|
|
1788
|
+
continue;
|
|
1393
1789
|
}
|
|
1790
|
+
if (waveAfter)
|
|
1791
|
+
return stop(report({ status: "failed", message: waveAfter }), 1);
|
|
1394
1792
|
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(", ")}`));
|
|
1395
1793
|
}
|
|
1794
|
+
// #4291: a failure, or a member it blocked, fails the job once every other member applied.
|
|
1795
|
+
const blockedHere = [...blockedSoFar.keys()].filter((name) => (only === undefined ? true : (p.plan.waves[only - 1] ?? []).includes(name)));
|
|
1796
|
+
if (lastFailure || planFailedIn !== undefined || blockedHere.length > 0) {
|
|
1797
|
+
const blockedLine = blockedSoFar.size > 0 ? ` Blocked: ${blockedWords(blockedSoFar)}.` : "";
|
|
1798
|
+
const message = planFailedIn !== undefined && !lastFailure
|
|
1799
|
+
? `A member of wave ${planFailedIn} failed to plan, so nothing in wave ${planFailedIn} was applied.${blockedLine}`
|
|
1800
|
+
: blockedLine ? blockedLine.trim() : undefined;
|
|
1801
|
+
return stop(report({ status: "failed", ...(message ? { message } : {}) }), lastFailure ? failedRunExitCode(lastFailure.results) : 1, lastFailure?.gated ? "gated" : undefined);
|
|
1802
|
+
}
|
|
1396
1803
|
const done = p.plan.order.every((name) => completed.has(name));
|
|
1397
1804
|
if (green && done)
|
|
1398
1805
|
await recordGreenApplied(p, green, approvedSet.doc.digest, approval, via);
|
|
@@ -1407,3 +1814,52 @@ function writeWaveChangeSet(ctx, wave, doc, upload) {
|
|
|
1407
1814
|
writeFileSync(join(dir, name), body);
|
|
1408
1815
|
upload?.file(name, body);
|
|
1409
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* chant components pr-post --pr <n> --head <sha> --env <env> --forge <kind> [--member <name>] [--output <dir>] [--result <result>] (#4295)
|
|
1819
|
+
*
|
|
1820
|
+
* Post the plan note and the `chant/plan` status from the report a plan job
|
|
1821
|
+
* wrote with `pr-plan --no-status`, in a job that runs none of the change's
|
|
1822
|
+
* code. The pull request, the head, the environment and the member come from
|
|
1823
|
+
* the flags, and a report that names others is refused (`../../pr-post.ts`).
|
|
1824
|
+
* It exits 0 once it has posted, whatever the plan said, so the plan job's
|
|
1825
|
+
* own result stays the one that fails the run.
|
|
1826
|
+
*/
|
|
1827
|
+
export async function runComponentsPrPost(ctx) {
|
|
1828
|
+
const { args } = ctx;
|
|
1829
|
+
if (args.pr === undefined)
|
|
1830
|
+
return fail("pr-post needs the pull or merge request: --pr <n>");
|
|
1831
|
+
if (!args.head || !/^[0-9a-f]{40}$/.test(args.head))
|
|
1832
|
+
return fail("pr-post needs the full commit the plan job checked out: --head <sha>");
|
|
1833
|
+
if (!args.env)
|
|
1834
|
+
return fail("pr-post needs the environment the plan was for: --env <env>");
|
|
1835
|
+
if (!args.forge || !FORGE_KINDS.includes(args.forge))
|
|
1836
|
+
return fail(`pr-post posts to a forge: --forge ${FORGE_KINDS.join("|")}`);
|
|
1837
|
+
if (args.members && args.members.length > 1)
|
|
1838
|
+
return fail(`pr-post takes one --member, not ${args.members.join(", ")}`);
|
|
1839
|
+
let forge;
|
|
1840
|
+
try {
|
|
1841
|
+
forge = forgeFromEnv(args.forge);
|
|
1842
|
+
}
|
|
1843
|
+
catch (err) {
|
|
1844
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
1845
|
+
}
|
|
1846
|
+
const { postPlanReport } = await import("../../pr-post.js");
|
|
1847
|
+
const member = args.members?.[0];
|
|
1848
|
+
const posted = await postPlanReport({
|
|
1849
|
+
forge,
|
|
1850
|
+
dir: resolve(args.output ?? PR_REPORT_DIR),
|
|
1851
|
+
pr: args.pr,
|
|
1852
|
+
head: args.head,
|
|
1853
|
+
env: args.env,
|
|
1854
|
+
...(member ? { member } : {}),
|
|
1855
|
+
...(args.planResult ? { result: args.planResult } : {}),
|
|
1856
|
+
// #4289: the plan job could not ask the forge, so the note job does, from the Ops at the commit it checked out.
|
|
1857
|
+
overdue: () => overdueOps(forge, member),
|
|
1858
|
+
});
|
|
1859
|
+
for (const message of posted.warnings)
|
|
1860
|
+
console.error(formatWarning({ message }));
|
|
1861
|
+
if (posted.refused)
|
|
1862
|
+
console.error(formatWarning({ message: `the plan report was not used: ${posted.refused}` }));
|
|
1863
|
+
console.error(formatInfo(`pr-post: ${prStatusContext("plan", member)} ${posted.state}: ${posted.description}${posted.noted ? "; note posted" : ""}`));
|
|
1864
|
+
return 0;
|
|
1865
|
+
}
|