@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/src/cli/handlers/pr.ts
CHANGED
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
* through `--forge`.
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
|
+
import { spawnSync } from "node:child_process";
|
|
67
68
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
68
69
|
import { join, resolve } from "node:path";
|
|
69
70
|
import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
|
|
@@ -85,10 +86,11 @@ import {
|
|
|
85
86
|
} from "../../components/pr-run";
|
|
86
87
|
import { composeChangeSet, verifyChangeSetDigest } from "../../change-set";
|
|
87
88
|
import { waveGateName } from "../../gated-waves";
|
|
89
|
+
import { blockedMembers, blockedWords, settleEarlierWaves, type WaveMemberOutcome } from "../../wave-blocked";
|
|
88
90
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt, type PrApplyRecord } from "../../components/fan-out-record";
|
|
89
91
|
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
90
92
|
import { samePlanDigest } from "../../lifecycle/plan-digest";
|
|
91
|
-
import { gitGateLedgerPort, type GateCheck } from "../../op/gate";
|
|
93
|
+
import { gateIsSealed, gitGateLedgerPort, type GateCheck } from "../../op/gate";
|
|
92
94
|
import { emitGateStep, gateEventsOf, GATE_PHASE, type RunEventSink, type RunEventStream } from "../../run-event-stream";
|
|
93
95
|
import type { RunEventBody, RunEventComponentSettle, RunEventSettleStatus } from "../../run-event";
|
|
94
96
|
import { openCliRunEvents, refusesEventsWithJson } from "../run-events";
|
|
@@ -105,9 +107,11 @@ import {
|
|
|
105
107
|
renderPrNote,
|
|
106
108
|
type PrApproval,
|
|
107
109
|
type PrMember,
|
|
110
|
+
type PrPolicyReport,
|
|
108
111
|
type PrReport,
|
|
109
112
|
type PrWave,
|
|
110
113
|
type PrRound,
|
|
114
|
+
type PrOverdue,
|
|
111
115
|
} from "../../pr-loop";
|
|
112
116
|
import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
113
117
|
import { GITHUB_COMMENT_LIMIT, GITLAB_NOTE_LIMIT } from "../../plan-summary";
|
|
@@ -118,16 +122,22 @@ import { findWorkspaceRoot } from "../../project-root";
|
|
|
118
122
|
import { resolveChangedUnits } from "./fan-out";
|
|
119
123
|
import { coverByMerges, greenOp, type GreenTarget } from "../../components/green-cover";
|
|
120
124
|
import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run";
|
|
125
|
+
import { appliedDigestOf } from "../../components/auto-release";
|
|
121
126
|
import type { CommandContext } from "../registry";
|
|
127
|
+
import { withComponentEndpoint } from "../component-endpoint";
|
|
122
128
|
import type { ComponentChangeSignal, FanOutPlan } from "../../components/fan-out";
|
|
123
129
|
import { failedRunExitCode, type DriverComponent } from "../../components/driver";
|
|
124
130
|
import type { CapabilityRegistry } from "../../components/capability";
|
|
131
|
+
import { prLoopHooksAt, runPrHooks, type PrHookPlace, type PrLoopHooks } from "../../components/pr-hooks";
|
|
132
|
+
import { decidePrPolicyOverride, prLoopPolicyAt, prPolicyReport, runPrPolicy, spendPrPolicyOverride, type PrLoopPolicy } from "../../components/pr-policy";
|
|
133
|
+
import type { OpWaveHookMoment } from "../../op/op-waves";
|
|
134
|
+
import type { PrHookOutcome } from "../../pr-loop";
|
|
125
135
|
|
|
126
136
|
|
|
127
137
|
/** Where the reports land unless `--output` says. */
|
|
128
138
|
export const PR_REPORT_DIR = ".chant/pr";
|
|
129
139
|
|
|
130
|
-
interface Prepared {
|
|
140
|
+
export interface Prepared {
|
|
131
141
|
config: ChantConfig;
|
|
132
142
|
env: string;
|
|
133
143
|
/** The workspace member, with `--member` (#3465). */
|
|
@@ -146,6 +156,23 @@ interface Prepared {
|
|
|
146
156
|
signal: ComponentChangeSignal;
|
|
147
157
|
registry: CapabilityRegistry;
|
|
148
158
|
seededOutputs: Record<string, Record<string, unknown>>;
|
|
159
|
+
/** `ci.hooks` from the declaration at base (#4288). */
|
|
160
|
+
hooks: PrLoopHooks;
|
|
161
|
+
/** Every hook the stage ran so far, for the report's `hooks`. */
|
|
162
|
+
hookOutcomes: PrHookOutcome[];
|
|
163
|
+
/** Set when a hook failed: the report's message. */
|
|
164
|
+
hookFailed?: string;
|
|
165
|
+
/** Where the stage's hooks run, once its op is known. */
|
|
166
|
+
hookPlace?: PrHookPlace;
|
|
167
|
+
/**
|
|
168
|
+
* Set when `ci.branches` or `ci.labels` (#4292) leave the member out of this
|
|
169
|
+
* stage: why. The plan is then empty, so the stage reports `nothing`.
|
|
170
|
+
*/
|
|
171
|
+
routedAway?: string;
|
|
172
|
+
/** `ci.policy` from the declaration at base (#4294). */
|
|
173
|
+
policy: PrLoopPolicy;
|
|
174
|
+
/** What the policy said about the plan, for the report's `policy`. */
|
|
175
|
+
policyReport?: PrPolicyReport;
|
|
149
176
|
}
|
|
150
177
|
|
|
151
178
|
const fail = (message: string, hint?: string): number => {
|
|
@@ -153,8 +180,8 @@ const fail = (message: string, hint?: string): number => {
|
|
|
153
180
|
return 1;
|
|
154
181
|
};
|
|
155
182
|
|
|
156
|
-
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. */
|
|
157
|
-
async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"): Promise<Prepared | number> {
|
|
183
|
+
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. `pr-env up` (#4298) prepares as the plan does. */
|
|
184
|
+
export async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"): Promise<Prepared | number> {
|
|
158
185
|
const { args } = ctx;
|
|
159
186
|
const projectPath = resolve(".");
|
|
160
187
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
@@ -234,8 +261,27 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"):
|
|
|
234
261
|
}
|
|
235
262
|
}
|
|
236
263
|
|
|
264
|
+
// #4288: the stage hooks, read from the declaration at base so the change cannot edit what its own jobs run.
|
|
265
|
+
let hooks: PrLoopHooks;
|
|
266
|
+
try {
|
|
267
|
+
hooks = prLoopHooksAt(projectPath, base);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
270
|
+
}
|
|
271
|
+
if (hooks.note) console.error(formatInfo(hooks.note));
|
|
272
|
+
// #4294: the policy, read from the declaration at base like the hooks.
|
|
273
|
+
let policy: PrLoopPolicy;
|
|
274
|
+
try {
|
|
275
|
+
policy = prLoopPolicyAt(projectPath, base);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
278
|
+
}
|
|
279
|
+
if (policy.note) console.error(formatInfo(policy.note));
|
|
280
|
+
|
|
237
281
|
const env = args.env ?? "local";
|
|
238
282
|
const registry = await fanOutRegistry(projectPath, config);
|
|
283
|
+
const routedAway = member ? await routing(ctx, stage, projectPath, member, base, forge) : undefined;
|
|
284
|
+
if (routedAway) console.error(formatInfo(routedAway));
|
|
239
285
|
return {
|
|
240
286
|
config,
|
|
241
287
|
env,
|
|
@@ -245,13 +291,89 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"):
|
|
|
245
291
|
gate: args.gate ?? PR_APPLY_GATE,
|
|
246
292
|
...(forge ? { forge } : {}),
|
|
247
293
|
components: derived.components,
|
|
248
|
-
plan: derived.plan,
|
|
294
|
+
plan: routedAway ? { ...derived.plan, order: [], waves: [], seeds: [], indeterminate: [] } : derived.plan,
|
|
249
295
|
signal: derived.signal,
|
|
250
296
|
registry,
|
|
251
297
|
seededOutputs,
|
|
298
|
+
hooks,
|
|
299
|
+
hookOutcomes: [],
|
|
300
|
+
...(routedAway ? { routedAway } : {}),
|
|
301
|
+
policy,
|
|
252
302
|
};
|
|
253
303
|
}
|
|
254
304
|
|
|
305
|
+
/** Set where `p`'s hooks run, once the stage knows its op (#4288). */
|
|
306
|
+
export function placeHooks(ctx: CommandContext, p: Prepared, stage: PrHookPlace["stage"], op: string): void {
|
|
307
|
+
p.hookPlace = { op, env: p.env, stage, dir: ctx.args.output ?? PR_REPORT_DIR, cwd: resolve("."), log: (line) => console.error(line) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Run one moment's hooks for `components` (#4288), keeping their outcomes
|
|
312
|
+
* for the report. Returns the failure as the report's message, or undefined
|
|
313
|
+
* when no hook failed. A hold is logged: the pull request's gate already
|
|
314
|
+
* waits for a person's approval of the digest.
|
|
315
|
+
*/
|
|
316
|
+
export function runStageHook(p: Prepared, moment: OpWaveHookMoment, components: readonly string[], planned?: PrPlanSet, what = "nothing was applied"): string | undefined {
|
|
317
|
+
if (!p.hookPlace || components.length === 0) return undefined;
|
|
318
|
+
const ran = runPrHooks(p.hooks, moment, p.hookPlace, components, planned ? { members: planned.members, plans: planned.plans } : undefined);
|
|
319
|
+
p.hookOutcomes.push(...ran.outcomes);
|
|
320
|
+
for (const hold of ran.holds) console.error(formatInfo(`${hold} held, and the pull request's gate asks for an approval of the digest either way`));
|
|
321
|
+
if (!ran.failed) return undefined;
|
|
322
|
+
p.hookFailed = `The ${ran.failed}, so ${what}.`;
|
|
323
|
+
return p.hookFailed;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const EMPTY_SET = (): PrPlanSet => ({ doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: true });
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Plan with the plan-side hooks around it (#4288): before-observe and
|
|
330
|
+
* before-plan, then the plan, then after-plan. A failed hook gives a failed
|
|
331
|
+
* set, so the stage reports `plan-failed` and nothing applies.
|
|
332
|
+
*/
|
|
333
|
+
async function hookedPlan(p: Prepared, components: readonly string[], plan: () => Promise<PrPlanSet>): Promise<PrPlanSet> {
|
|
334
|
+
if (runStageHook(p, "before-observe", components, undefined, "nothing was planned or applied")) return EMPTY_SET();
|
|
335
|
+
if (runStageHook(p, "before-plan", components, undefined, "nothing was planned or applied")) return EMPTY_SET();
|
|
336
|
+
const set = await plan();
|
|
337
|
+
if (set.failed) return set;
|
|
338
|
+
if (runStageHook(p, "after-plan", components, set)) return { ...set, failed: true };
|
|
339
|
+
return set;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Whether `ci.branches` or `ci.labels` (#4292), read from the declaration at
|
|
344
|
+
* base, leave `member` out of this stage: the reason, or undefined when the
|
|
345
|
+
* member runs. The record and apply stages read `ci.branches` against the
|
|
346
|
+
* pushed branch (`--branch`, else the forge's variables); the plan stage
|
|
347
|
+
* reads `ci.labels` against the pull request's labels. A label only narrows
|
|
348
|
+
* what is planned: the record and apply stages never read labels.
|
|
349
|
+
*/
|
|
350
|
+
async function routing(ctx: CommandContext, stage: "plan" | "apply" | "record", projectPath: string, member: string, base: string, forge: PrForge | undefined): Promise<string | undefined> {
|
|
351
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
352
|
+
const { appliesFrom, plansUnderLabels, pushedBranchFromEnv } = await import("../../workspace/ci-branches");
|
|
353
|
+
const ci = scopeSource(projectPath, base).declaration?.ci;
|
|
354
|
+
if (stage !== "plan") {
|
|
355
|
+
if (!ci?.branches) return undefined;
|
|
356
|
+
const pushed = ctx.args.branch ?? pushedBranchFromEnv();
|
|
357
|
+
if (!pushed) return undefined;
|
|
358
|
+
const route = appliesFrom(ci.branches, member, pushed);
|
|
359
|
+
if (!route.applies) return route.note;
|
|
360
|
+
console.error(formatInfo(route.note));
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
if (!ci?.labels || ctx.args.pr === undefined || !forge?.labels) return undefined;
|
|
364
|
+
let carried: string[];
|
|
365
|
+
try {
|
|
366
|
+
carried = await forge.labels(ctx.args.pr);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.error(formatWarning({ message: `could not read the labels of #${ctx.args.pr}, so every member plans: ${err instanceof Error ? err.message : String(err)}` }));
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
const filter = plansUnderLabels(ci.labels, member, carried);
|
|
372
|
+
if (!filter.plans) return filter.note;
|
|
373
|
+
if (filter.note) console.error(formatInfo(filter.note));
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
255
377
|
/**
|
|
256
378
|
* The one `--member` name, checked against the workspace member that owns
|
|
257
379
|
* `projectPath`: a pipeline copied into another member would otherwise
|
|
@@ -278,8 +400,12 @@ async function checkMember(projectPath: string, names: string[]): Promise<string
|
|
|
278
400
|
return name;
|
|
279
401
|
}
|
|
280
402
|
|
|
281
|
-
/** Plan every member the derivation selects. */
|
|
282
|
-
function planAll(p: Prepared, onRunEvent?: RunEventSink): Promise<PrPlanSet> {
|
|
403
|
+
/** Plan every member the derivation selects, between the plan-side hooks (#4288). */
|
|
404
|
+
export function planAll(p: Prepared, onRunEvent?: RunEventSink): Promise<PrPlanSet> {
|
|
405
|
+
return hookedPlan(p, p.plan.order, () => planBare(p, onRunEvent));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function planBare(p: Prepared, onRunEvent?: RunEventSink): Promise<PrPlanSet> {
|
|
283
409
|
return planPrSet({
|
|
284
410
|
components: p.components,
|
|
285
411
|
plan: p.plan,
|
|
@@ -341,7 +467,7 @@ async function publish(forge: PrForge | undefined, report: PrReport, note: strin
|
|
|
341
467
|
if (report.pr === null) return;
|
|
342
468
|
// A member the change does not reach has nothing to say: one note per
|
|
343
469
|
// member would put a line on every pull request for each member it skips.
|
|
344
|
-
if (report.member && report.status === "nothing") return;
|
|
470
|
+
if (report.member && report.status === "nothing" && !report.overdue?.length) return;
|
|
345
471
|
try {
|
|
346
472
|
await forge.upsertNote(report.pr, prNoteMarker(report.env, report.member), note);
|
|
347
473
|
} catch (err) {
|
|
@@ -364,6 +490,11 @@ function printMembers(report: PrReport): void {
|
|
|
364
490
|
}
|
|
365
491
|
|
|
366
492
|
async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload?: PrReportUpload): Promise<void> {
|
|
493
|
+
// #4288: the hooks the stage ran, and a failed hook's message in place of the stage's own.
|
|
494
|
+
if (p.hookOutcomes.length > 0) report = { ...report, hooks: [...p.hookOutcomes] };
|
|
495
|
+
if (p.hookFailed) report = { ...report, message: p.hookFailed };
|
|
496
|
+
if (p.routedAway && report.status === "nothing" && !report.message) report = { ...report, message: p.routedAway };
|
|
497
|
+
if (p.policyReport) report = { ...report, policy: p.policyReport };
|
|
367
498
|
const note = noteFor(report, p.forge);
|
|
368
499
|
writeOutputs(ctx, report, note, upload);
|
|
369
500
|
if (!ctx.args.noStatus) await publish(p.forge, report, note);
|
|
@@ -418,10 +549,90 @@ function openUpload(events: StageEvents, stage: "plan" | "apply" | "record", pr:
|
|
|
418
549
|
return events.upload;
|
|
419
550
|
}
|
|
420
551
|
|
|
552
|
+
/** What the policy step decided (#4294): a stop, or whether the plan is held and the override it applies under. */
|
|
553
|
+
interface PolicyJudged {
|
|
554
|
+
stop?: { refusal: "policy-denied" | "policy-error"; message: string; code: number };
|
|
555
|
+
held: boolean;
|
|
556
|
+
override?: NonNullable<PrPolicyReport["override"]>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Run `ci.policy` at base over the planned set (#4294) and keep what it said
|
|
561
|
+
* on `p` for the report. A denial is decided against the override ledger:
|
|
562
|
+
* read only at the plan stage, and recorded for an override at the apply.
|
|
563
|
+
*/
|
|
564
|
+
async function judgePolicy(p: Prepared, at: { stage: "plan" | "apply"; op: string; pr: number | null; set: PrPlanSet }): Promise<PolicyJudged> {
|
|
565
|
+
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 });
|
|
566
|
+
if (!verdict) return { held: false };
|
|
567
|
+
const report = prPolicyReport(verdict, p.policy.base);
|
|
568
|
+
p.policyReport = report;
|
|
569
|
+
for (const w of verdict.warnings) console.error(formatWarning({ message: `policy warning: ${w}` }));
|
|
570
|
+
if (verdict.result === "error") {
|
|
571
|
+
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}` } };
|
|
572
|
+
}
|
|
573
|
+
const held = verdict.holds.length > 0;
|
|
574
|
+
for (const h of verdict.holds) console.error(formatInfo(`policy holds the plan for a person's approval (${h.rule}): ${h.message}`));
|
|
575
|
+
if (verdict.result !== "denied") return { held };
|
|
576
|
+
const denied = verdict.denials.map((d) => `${d.rule}: ${d.message}`).join("; ");
|
|
577
|
+
let decided: Awaited<ReturnType<typeof decidePrPolicyOverride>>;
|
|
578
|
+
try {
|
|
579
|
+
// The plan stage records nothing, and reads the applied records so a spent override reads as spent.
|
|
580
|
+
const real = gitGateLedgerPort();
|
|
581
|
+
const port = at.stage === "plan" ? { ...readOnlyLedger(real), ...(real.readApplied ? { readApplied: real.readApplied.bind(real) } : {}) } : real;
|
|
582
|
+
decided = await decidePrPolicyOverride({
|
|
583
|
+
port,
|
|
584
|
+
subject: at.op,
|
|
585
|
+
digest: at.set.doc.digest,
|
|
586
|
+
rules: verdict.rules,
|
|
587
|
+
base: p.base,
|
|
588
|
+
cwd: resolve("."),
|
|
589
|
+
sealed: await gateIsSealed(p.gate),
|
|
590
|
+
record: at.stage === "apply",
|
|
591
|
+
description: `${at.op}: plan ${at.set.doc.digest} denied by ${verdict.rules.join(", ")}`,
|
|
592
|
+
});
|
|
593
|
+
} catch (err) {
|
|
594
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
595
|
+
if (at.stage === "plan") {
|
|
596
|
+
console.error(formatWarning({ message: `could not read the override ledger, so no override reads as standing: ${why}` }));
|
|
597
|
+
return { held, stop: { refusal: "policy-denied", code: 1, message: `The policy denied this plan: ${denied}.` } };
|
|
598
|
+
}
|
|
599
|
+
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.` } };
|
|
600
|
+
}
|
|
601
|
+
if (decided.status === "overridden") {
|
|
602
|
+
report.override = decided.override;
|
|
603
|
+
console.error(formatInfo(`the policy denial (${verdict.rules.join(", ")}) is overridden by ${decided.override.by} at ${decided.override.at}: ${decided.override.reason}`));
|
|
604
|
+
return { held, override: decided.override };
|
|
605
|
+
}
|
|
606
|
+
report.overrideCommand = decided.command;
|
|
607
|
+
for (const why of decided.refusals) console.error(formatInfo(`an override does not count: ${why}`));
|
|
608
|
+
const who = decided.overriders.length ? `${decided.overriders.join(", ")} may` : "nobody listed in .chant/overriders at base may";
|
|
609
|
+
const moved = decided.moved ? ` ${decided.moved.by} overrode an earlier plan or other rules, which counts for nothing here.` : "";
|
|
610
|
+
return {
|
|
611
|
+
held,
|
|
612
|
+
stop: {
|
|
613
|
+
refusal: "policy-denied",
|
|
614
|
+
code: at.stage === "plan" ? 1 : GATED_EXIT_CODE,
|
|
615
|
+
message: `The policy denied this plan: ${denied}.${moved} Nothing was applied. ${who} override it: ${decided.command}`,
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Spend the override the apply runs under (#4294), before anything applies. The failure's message, or undefined. */
|
|
621
|
+
async function spendPolicy(op: string, judged: PolicyJudged, head: string): Promise<string | undefined> {
|
|
622
|
+
if (!judged.override) return undefined;
|
|
623
|
+
try {
|
|
624
|
+
await spendPrPolicyOverride(gitGateLedgerPort(), op, judged.override, { commit: head });
|
|
625
|
+
} catch (err) {
|
|
626
|
+
return `Could not record the override this apply runs under, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`;
|
|
627
|
+
}
|
|
628
|
+
console.error(formatInfo(`applies under the override by ${judged.override.by}, which is now spent`));
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
|
|
421
632
|
/** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
422
633
|
export function runComponentsPrPlan(ctx: CommandContext): Promise<number> {
|
|
423
634
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
424
|
-
return withStageEvents("pr-plan", ctx, (events) => prPlan(ctx, events));
|
|
635
|
+
return withStageEvents("pr-plan", ctx, (events) => withComponentEndpoint(ctx, () => prPlan(ctx, events)));
|
|
425
636
|
}
|
|
426
637
|
|
|
427
638
|
async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
@@ -431,12 +642,15 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
431
642
|
const p = await prepare(ctx, "plan");
|
|
432
643
|
if (typeof p === "number") return p;
|
|
433
644
|
const op = prOp(ctx.args.pr, p.member);
|
|
645
|
+
placeHooks(ctx, p, "plan", op);
|
|
434
646
|
const upload = openUpload(stage, "plan", ctx.args.pr, p);
|
|
435
647
|
// `--events-json` (#4041): planning, then where the approval stands.
|
|
436
648
|
const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") }, upload?.line);
|
|
437
649
|
stage.stream = events;
|
|
438
650
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
439
651
|
const set = await planAll(p, events?.emit);
|
|
652
|
+
// #4294: the policy at base over the planned change set.
|
|
653
|
+
const judged = p.plan.order.length > 0 && !set.failed ? await judgePolicy(p, { stage: "plan", op, pr: ctx.args.pr, set }) : { held: false };
|
|
440
654
|
|
|
441
655
|
let approval: PrApproval = { status: "pending" };
|
|
442
656
|
if (p.plan.order.length > 0) {
|
|
@@ -450,7 +664,8 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
450
664
|
}
|
|
451
665
|
}
|
|
452
666
|
const status = p.plan.order.length === 0 ? "nothing" : set.failed ? "plan-failed" : "planned";
|
|
453
|
-
const
|
|
667
|
+
const overdue = await overdueOps(p.forge, p.member);
|
|
668
|
+
const planned = prReport({
|
|
454
669
|
stage: "plan",
|
|
455
670
|
pr: ctx.args.pr,
|
|
456
671
|
env: p.env,
|
|
@@ -466,6 +681,7 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
466
681
|
status,
|
|
467
682
|
...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
|
|
468
683
|
});
|
|
684
|
+
const report: PrReport = overdue.length > 0 ? { ...planned, overdue } : planned;
|
|
469
685
|
await finish(ctx, p, report, upload);
|
|
470
686
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
471
687
|
events?.settle(settleOf(status), status === "plan-failed" ? "a member failed to plan" : undefined);
|
|
@@ -476,13 +692,45 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
476
692
|
console.error(formatInfo(`plan : ${report.digest}`));
|
|
477
693
|
console.error(formatInfo(approval.status === "approved" ? `approved by ${(approval.approvedBy ?? []).join(", ")}` : `approve : ${prApproveCommand(report)}`));
|
|
478
694
|
}
|
|
479
|
-
|
|
695
|
+
if (judged.stop) console.error(formatWarning({ message: judged.stop.message }));
|
|
696
|
+
return status === "plan-failed" || judged.stop ? 1 : 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The scheduled watch and reconcile Ops that stopped running (#4289,
|
|
701
|
+
* `op/schedule-overdue.ts`), asked of the forge. Without a forge, or when
|
|
702
|
+
* the Ops cannot be read, nothing is checked and the plan goes on. The
|
|
703
|
+
* generated plan holds no forge token (#4295), so there the plan-note job's
|
|
704
|
+
* `pr-post` asks instead, reading the Ops at the target branch's commit.
|
|
705
|
+
*/
|
|
706
|
+
async function overdueOps(forge: PrForge | undefined, member: string | undefined): Promise<PrOverdue[]> {
|
|
707
|
+
if (!forge?.scheduledRuns) return [];
|
|
708
|
+
try {
|
|
709
|
+
const { discoverOps } = await import("../../op/discover");
|
|
710
|
+
const { scheduledWatches, checkOverdueOps } = await import("../../op/schedule-overdue");
|
|
711
|
+
const discovered = await discoverOps({ cwd: resolve(".") });
|
|
712
|
+
const watches = scheduledWatches([...discovered.ops.values()].map((d) => d.config));
|
|
713
|
+
if (watches.length === 0) return [];
|
|
714
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
|
|
715
|
+
if (top.status !== 0) return [];
|
|
716
|
+
return await checkOverdueOps({
|
|
717
|
+
watches,
|
|
718
|
+
forge,
|
|
719
|
+
repoRoot: top.stdout.trim(),
|
|
720
|
+
...(member ? { member } : {}),
|
|
721
|
+
now: new Date(),
|
|
722
|
+
log: (line) => console.error(formatWarning({ message: line })),
|
|
723
|
+
});
|
|
724
|
+
} catch (err) {
|
|
725
|
+
console.error(formatWarning({ message: `could not check the scheduled Ops, so none is reported overdue: ${err instanceof Error ? err.message : String(err)}` }));
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
480
728
|
}
|
|
481
729
|
|
|
482
730
|
/** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
483
731
|
export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
|
|
484
732
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
485
|
-
return withStageEvents("pr-apply", ctx, (events) => prApply(ctx, events));
|
|
733
|
+
return withStageEvents("pr-apply", ctx, (events) => withComponentEndpoint(ctx, () => prApply(ctx, events)));
|
|
486
734
|
}
|
|
487
735
|
|
|
488
736
|
async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
@@ -491,6 +739,9 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
491
739
|
const reviewRules = !ctx.args.requireReview && securityGroupsAt(process.cwd(), ctx.args.base).has("review-rules");
|
|
492
740
|
if (reviewRules && !ctx.args.forge) return fail("the workspace opts into review-rules, which reads the pull request's reviews, so pr-apply needs --forge <kind>");
|
|
493
741
|
if (reviewRules) ctx = { ...ctx, args: { ...ctx.args, requireReview: true } };
|
|
742
|
+
// #4293: ci.locks, read at base like the security groups, so the change cannot turn its own locks off.
|
|
743
|
+
const { ciLocksAt } = await import("../../workspace/ci-locks");
|
|
744
|
+
const locks = ciLocksAt(process.cwd(), ctx.args.base);
|
|
494
745
|
const { args } = ctx;
|
|
495
746
|
if (args.requireReview && !args.forge) return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
|
|
496
747
|
if (args.wave !== undefined && !args.waveGate) return fail("--wave runs one wave of a --wave-gate apply, so it needs --wave-gate <name>");
|
|
@@ -515,6 +766,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
515
766
|
}
|
|
516
767
|
}
|
|
517
768
|
const op = green ? greenOp(green.commit, p.member) : pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
769
|
+
placeHooks(ctx, p, "apply", op);
|
|
518
770
|
// The run id the release records carry, chosen before the run so that
|
|
519
771
|
// `--events-json` (#4041, #4007) names the run by it too.
|
|
520
772
|
const release = autoReleaseRun(`${op}-${Date.now()}`);
|
|
@@ -522,6 +774,17 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
522
774
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
523
775
|
stage.stream = events;
|
|
524
776
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
777
|
+
if (locks && p.plan.order.length > 0) {
|
|
778
|
+
const held = await heldLocksOf(p, pr);
|
|
779
|
+
if (typeof held === "number") return held;
|
|
780
|
+
if (held) {
|
|
781
|
+
const set: PrPlanSet = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
782
|
+
return stopper(ctx, p, events, upload)(
|
|
783
|
+
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 }),
|
|
784
|
+
GATED_EXIT_CODE,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
525
788
|
if (args.waveGate) return prApplyWaves(ctx, p, { pr, op, release, upload, events, ...(green ? { green } : {}) });
|
|
526
789
|
|
|
527
790
|
// `--resume` (#3464): an earlier attempt at this change on this commit
|
|
@@ -591,10 +854,14 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
591
854
|
|
|
592
855
|
let approval: PrApproval;
|
|
593
856
|
let via: "merges" | "gate" = "gate";
|
|
857
|
+
// #4294: the policy at base, before the gate. A resume runs under the decision of the attempt it finishes.
|
|
858
|
+
let judged: PolicyJudged = { held: false };
|
|
594
859
|
if (resumed) {
|
|
595
860
|
approval = resumed.approval;
|
|
596
861
|
} else {
|
|
597
|
-
|
|
862
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
863
|
+
if (judged.stop) return stop(prReport({ ...base, approval: { status: "pending" }, status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
864
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
598
865
|
approval = decided.approval;
|
|
599
866
|
if (!decided.satisfied) return stop(prReport({ ...base, approval, status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
600
867
|
via = decided.via;
|
|
@@ -607,6 +874,8 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
607
874
|
emitReviewStop(events, review.message);
|
|
608
875
|
return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
|
|
609
876
|
}
|
|
877
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
878
|
+
if (unspent) return stop(prReport({ ...base, approval, status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
610
879
|
|
|
611
880
|
// The record is written before the first component runs and again as each
|
|
612
881
|
// one settles, so a run killed partway still knows what applied.
|
|
@@ -640,6 +909,11 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
640
909
|
};
|
|
641
910
|
recordAttempt();
|
|
642
911
|
|
|
912
|
+
// #4288: the before-apply hooks, once the gate and the review let the apply through.
|
|
913
|
+
const toApply = (resumed?.plan ?? p.plan).order.filter((name) => !completedSoFar.has(name));
|
|
914
|
+
const beforeFailed = runStageHook(p, "before-apply", toApply);
|
|
915
|
+
if (beforeFailed) return stop(prReport({ ...base, approval, status: "failed", message: beforeFailed }), 1);
|
|
916
|
+
|
|
643
917
|
// #3999: a round planned after an upstream applied waits on a gate of its
|
|
644
918
|
// own, under the same identity.gates rule and the same review checks.
|
|
645
919
|
const roundLedger = gitGateLedgerPort();
|
|
@@ -695,11 +969,13 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
695
969
|
: {}),
|
|
696
970
|
});
|
|
697
971
|
recordAttempt();
|
|
698
|
-
|
|
972
|
+
// #4288: the after-apply hooks, for the components that applied.
|
|
973
|
+
const afterFailed = runStageHook(p, "after-apply", toApply.filter((name) => applied.run.completed.includes(name)), undefined, "the apply counts as failed though its components applied");
|
|
974
|
+
await recordAutoReleasesForRun(applied.run.results, p.env, release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
699
975
|
const report = prReport({
|
|
700
976
|
...base,
|
|
701
977
|
approval,
|
|
702
|
-
status: applied.status,
|
|
978
|
+
status: afterFailed ? "failed" : applied.status,
|
|
703
979
|
members: applied.members,
|
|
704
980
|
...(resumed ? { resumed: resumed.completed } : {}),
|
|
705
981
|
...(applied.rounds ? { rounds: applied.rounds } : {}),
|
|
@@ -708,17 +984,45 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
708
984
|
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
709
985
|
// a person. A step that refused its plan because the target moved since
|
|
710
986
|
// the approval exits 3, as a moved digest does before anything applies (#3997).
|
|
711
|
-
const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
987
|
+
const exit = afterFailed ? 1 : applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
712
988
|
if (green && applied.status === "applied") await recordGreenApplied(p, green, set.doc.digest, approval, via);
|
|
713
989
|
const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
|
|
714
|
-
if (applied.status === "applied") console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
990
|
+
if (applied.status === "applied" && !afterFailed) console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
715
991
|
return code;
|
|
716
992
|
}
|
|
717
993
|
|
|
994
|
+
/**
|
|
995
|
+
* Under `ci.locks` (#4293): the refusal when another open pull request holds
|
|
996
|
+
* a lock on a component this apply reaches, else null. The targets are the
|
|
997
|
+
* selected components, the names `chant lock` takes. A lock whose pull
|
|
998
|
+
* request the forge says merged or closed reads as released; with no
|
|
999
|
+
* `--forge` every lock counts. A lock file that cannot be read stops the
|
|
1000
|
+
* apply, since nothing then says the targets are free.
|
|
1001
|
+
*/
|
|
1002
|
+
async function heldLocksOf(p: Prepared, pr: number | null): Promise<string | null | number> {
|
|
1003
|
+
const { heldTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock");
|
|
1004
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
1005
|
+
let held;
|
|
1006
|
+
try {
|
|
1007
|
+
held = await heldTargetLocks(p.gate, p.plan.order, pr, { ...(isOpen ? { isOpen } : {}) });
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
return fail(`could not read the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (held.length === 0) return null;
|
|
1012
|
+
const name = (n: number) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
1013
|
+
const holders = [...new Set(held.map((h) => h.pr))].sort((a, b) => a - b);
|
|
1014
|
+
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"}.`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** #4274: a component that published nothing is recorded by the plan digest its approval bound. */
|
|
1018
|
+
function prAppliedDigests(p: Prepared, members: PrMember[]) {
|
|
1019
|
+
return appliedDigestOf({ components: p.components, env: p.env, projectRoot: resolve("."), members });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
718
1022
|
/** chant components pr-record --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
719
1023
|
export function runComponentsPrRecord(ctx: CommandContext): Promise<number> {
|
|
720
1024
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
721
|
-
return withStageEvents("pr-record", ctx, (events) => prRecord(ctx, events));
|
|
1025
|
+
return withStageEvents("pr-record", ctx, (events) => withComponentEndpoint(ctx, () => prRecord(ctx, events)));
|
|
722
1026
|
}
|
|
723
1027
|
|
|
724
1028
|
/**
|
|
@@ -745,6 +1049,7 @@ async function prRecord(ctx: CommandContext, stage: StageEvents): Promise<number
|
|
|
745
1049
|
}
|
|
746
1050
|
}
|
|
747
1051
|
const op = pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
1052
|
+
placeHooks(ctx, p, "record", op);
|
|
748
1053
|
const upload = openUpload(stage, "record", pr, p);
|
|
749
1054
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-record ${op}`, cwd: resolve(".") }, upload?.line);
|
|
750
1055
|
stage.stream = events;
|
|
@@ -801,31 +1106,34 @@ async function prRecord(ctx: CommandContext, stage: StageEvents): Promise<number
|
|
|
801
1106
|
|
|
802
1107
|
const { mergeSourceOf, writeMergeRecord, MERGE_RECORD_SCHEMA_ID, MERGE_RECORD_CONTRACT } = await import("../../lifecycle/merge-record");
|
|
803
1108
|
const { pushLifecycleReport } = await import("../../lifecycle/git");
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (
|
|
1109
|
+
// #4292: a member another branch applies has no merge on this one, so no record says it planned nothing here.
|
|
1110
|
+
if (!p.routedAway) {
|
|
1111
|
+
try {
|
|
1112
|
+
const written = await writeMergeRecord({
|
|
1113
|
+
$schema: MERGE_RECORD_SCHEMA_ID,
|
|
1114
|
+
contract: MERGE_RECORD_CONTRACT,
|
|
1115
|
+
commit: p.head,
|
|
1116
|
+
base: p.base,
|
|
1117
|
+
env: p.env,
|
|
1118
|
+
...(p.member ? { member: p.member } : {}),
|
|
1119
|
+
source: mergeSourceOf(resolve("."), p.head, pr),
|
|
1120
|
+
op: pr === null ? null : op,
|
|
1121
|
+
gate: p.gate,
|
|
1122
|
+
digest: nothing ? null : set.doc.digest,
|
|
1123
|
+
plan: nothing ? "nothing" : set.failed ? "plan-failed" : approval.status === "approved" ? "approved" : approval.status === "changed" ? "changed" : "unapproved",
|
|
1124
|
+
...(approval.status === "changed" && approval.approved ? { approvedDigest: approval.approved } : {}),
|
|
1125
|
+
approvedBy: approval.status === "approved" ? (approval.approvedBy ?? []) : [],
|
|
1126
|
+
...(authorApproved && authorApproved.length > 0 ? { authorApproved } : {}),
|
|
1127
|
+
components: p.plan.order,
|
|
1128
|
+
recordedAt: new Date().toISOString(),
|
|
1129
|
+
});
|
|
1130
|
+
if (written.written) {
|
|
1131
|
+
const pushed = await pushLifecycleReport();
|
|
1132
|
+
if (pushed.pushWarning) console.error(formatWarning({ message: pushed.pushWarning }));
|
|
1133
|
+
}
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
826
1136
|
}
|
|
827
|
-
} catch (err) {
|
|
828
|
-
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
829
1137
|
}
|
|
830
1138
|
|
|
831
1139
|
const stop = stopper(ctx, p, events, upload);
|
|
@@ -867,11 +1175,15 @@ async function greenTargetOf(ctx: CommandContext): Promise<GreenTarget | number>
|
|
|
867
1175
|
*/
|
|
868
1176
|
async function decideApplyGate(
|
|
869
1177
|
p: Prepared,
|
|
870
|
-
at: { op: string; pr: number | null; set: PrPlanSet; events?: RunEventStream; green?: GreenTarget },
|
|
1178
|
+
at: { op: string; pr: number | null; set: PrPlanSet; events?: RunEventStream; green?: GreenTarget; held?: boolean },
|
|
871
1179
|
): Promise<{ approval: PrApproval; satisfied: true; via: "merges" | "gate" } | { approval: PrApproval; satisfied: false; refusal: "plan-changed" | "not-approved"; message: string }> {
|
|
872
1180
|
const { op, pr, set, events, green } = at;
|
|
873
1181
|
let uncovered: string[] = [];
|
|
874
|
-
|
|
1182
|
+
// #4294: a plan the policy holds needs an approval of its own digest, so the merges' approvals do not cover it.
|
|
1183
|
+
if (green && at.held) {
|
|
1184
|
+
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`));
|
|
1185
|
+
uncovered = ["the policy holds this plan for an approval of its own digest"];
|
|
1186
|
+
} else if (green) {
|
|
875
1187
|
const cover = coverByMerges(set.doc, green.approved);
|
|
876
1188
|
if (cover.covered) {
|
|
877
1189
|
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`));
|
|
@@ -1270,8 +1582,23 @@ async function prApplyWaves(
|
|
|
1270
1582
|
const completed = new Set(carry?.completed ?? []);
|
|
1271
1583
|
const outputsSoFar: Record<string, Record<string, unknown>> = { ...(carry?.outputs ?? {}) };
|
|
1272
1584
|
const inputsSoFar: Record<string, string> = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]!]));
|
|
1273
|
-
|
|
1585
|
+
// #4291: what failed in an earlier wave's job stays failed until it applies, so this job blocks its dependents and runs the rest.
|
|
1586
|
+
const failedSoFar = new Set<string>((carry ? attempt?.failed ?? [] : []).filter((name) => !completed.has(name)));
|
|
1274
1587
|
let waves: PrWave[] = carry?.record.waves ?? [];
|
|
1588
|
+
const upstreams = new Map(p.components.map((c) => [c.name, c.dependsOn ?? []]));
|
|
1589
|
+
/** Each component's outcome so far, past applied (#4291): failed, waiting, not-applied, or blocked by a failure. */
|
|
1590
|
+
const outcomes = (): Map<string, WaveMemberOutcome> => {
|
|
1591
|
+
const out = new Map<string, WaveMemberOutcome>();
|
|
1592
|
+
for (const name of completed) out.set(name, { status: "applied" });
|
|
1593
|
+
for (const name of failedSoFar) out.set(name, { status: "failed" });
|
|
1594
|
+
for (const w of waves) {
|
|
1595
|
+
if (w.status !== "waiting" && w.status !== "plan-failed" && w.status !== "failed") continue;
|
|
1596
|
+
for (const name of w.components) if (!out.has(name)) out.set(name, { status: w.status === "waiting" ? "waiting" : "not-applied" });
|
|
1597
|
+
}
|
|
1598
|
+
return out;
|
|
1599
|
+
};
|
|
1600
|
+
/** Components a failure blocked (#4291), each with the component that failed. */
|
|
1601
|
+
const blockedSoFar = new Map<string, string>();
|
|
1275
1602
|
const after = new Map<string, PrMember>();
|
|
1276
1603
|
const leftIn = (k: number): string[] => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
|
|
1277
1604
|
const firstLeft = (): number => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
|
|
@@ -1286,15 +1613,32 @@ async function prApplyWaves(
|
|
|
1286
1613
|
const set = approved ?? { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1287
1614
|
const members = set.members.map((m): PrMember => {
|
|
1288
1615
|
const now = after.get(m.member);
|
|
1289
|
-
if (now) return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
|
|
1290
|
-
|
|
1616
|
+
if (now) return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}), ...(now.blockedBy ? { blockedBy: now.blockedBy } : {}) };
|
|
1617
|
+
if (completed.has(m.component)) return { ...m, status: "applied" };
|
|
1618
|
+
const by = blockedSoFar.get(m.component);
|
|
1619
|
+
if (by !== undefined) return { ...m, status: "blocked", blockedBy: by };
|
|
1620
|
+
return failedSoFar.has(m.component) ? { ...m, status: "failed" } : m;
|
|
1291
1621
|
});
|
|
1292
1622
|
return prReport({ ...reportBase, set, approval, members, waves, ...input });
|
|
1293
1623
|
};
|
|
1294
1624
|
|
|
1295
1625
|
// `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
|
|
1626
|
+
// #4291: an earlier member that failed, or was blocked by one that failed, counts as settled; it blocks its dependents here.
|
|
1296
1627
|
const left = firstLeft();
|
|
1297
|
-
|
|
1628
|
+
const earlierWaiting = only !== undefined ? waves.find((w) => w.wave < only && w.status === "waiting" && w.components.some((name) => !completed.has(name))) : undefined;
|
|
1629
|
+
if (earlierWaiting) {
|
|
1630
|
+
return stop(
|
|
1631
|
+
report({
|
|
1632
|
+
status: "refused",
|
|
1633
|
+
refusal: "earlier-wave",
|
|
1634
|
+
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.`,
|
|
1635
|
+
}),
|
|
1636
|
+
GATED_EXIT_CODE,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
const settled = only !== undefined && left > 0 && left < only ? settleEarlierWaves(p.plan.waves, only, upstreams, outcomes()) : undefined;
|
|
1640
|
+
if (settled) for (const [name, o] of settled.outcomes) if (o.status === "blocked" && o.by) blockedSoFar.set(name, o.by);
|
|
1641
|
+
if (only !== undefined && left > 0 && left < only && (settled?.unsettled.length ?? 0) > 0) {
|
|
1298
1642
|
return stop(
|
|
1299
1643
|
report({
|
|
1300
1644
|
status: "refused",
|
|
@@ -1308,6 +1652,7 @@ async function prApplyWaves(
|
|
|
1308
1652
|
}
|
|
1309
1653
|
|
|
1310
1654
|
let planned: PrPlanSet | undefined;
|
|
1655
|
+
let judged: PolicyJudged = { held: false };
|
|
1311
1656
|
if (carry) {
|
|
1312
1657
|
// What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
|
|
1313
1658
|
p.plan.waves.forEach((names, i) => {
|
|
@@ -1325,7 +1670,10 @@ async function prApplyWaves(
|
|
|
1325
1670
|
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);
|
|
1326
1671
|
}
|
|
1327
1672
|
await storeChangeSetForGate(ctx, p, set.doc);
|
|
1328
|
-
|
|
1673
|
+
// #4294: the policy at base judges the whole change set, before the pull request's gate.
|
|
1674
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
1675
|
+
if (judged.stop) return stop(report({ status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
1676
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
1329
1677
|
approval = decided.approval;
|
|
1330
1678
|
if (!decided.satisfied) return stop(report({ status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
1331
1679
|
via = decided.via;
|
|
@@ -1340,6 +1688,8 @@ async function prApplyWaves(
|
|
|
1340
1688
|
emitReviewStop(events, prReview.message);
|
|
1341
1689
|
return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
|
|
1342
1690
|
}
|
|
1691
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
1692
|
+
if (unspent) return stop(report({ status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
1343
1693
|
|
|
1344
1694
|
const recordAttempt = (): void => {
|
|
1345
1695
|
if (!resumePath) return;
|
|
@@ -1370,9 +1720,27 @@ async function prApplyWaves(
|
|
|
1370
1720
|
|
|
1371
1721
|
const from = only ?? (left || total + 1);
|
|
1372
1722
|
const to = only ?? total;
|
|
1723
|
+
/** The last apply in this job that failed (#4291): its results give the exit code once every wave ran. */
|
|
1724
|
+
let lastFailure: { results: Parameters<typeof failedRunExitCode>[0]; gated: boolean } | undefined;
|
|
1725
|
+
let planFailedIn: number | undefined;
|
|
1373
1726
|
for (let k = from; k <= to; k++) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1727
|
+
// #4291: a component of this wave whose upstream failed, or was blocked by a failure, is blocked; the rest of the wave runs.
|
|
1728
|
+
const before = new Set(p.plan.waves.slice(0, k - 1).flat());
|
|
1729
|
+
const mine = leftIn(k);
|
|
1730
|
+
for (const name of mine) {
|
|
1731
|
+
failedSoFar.delete(name);
|
|
1732
|
+
blockedSoFar.delete(name);
|
|
1733
|
+
}
|
|
1734
|
+
const outcomesNow = outcomes();
|
|
1735
|
+
for (const [name, by] of blockedSoFar) outcomesNow.set(name, { status: "blocked", by });
|
|
1736
|
+
const blockedNow = blockedMembers(mine, upstreams, before, outcomesNow);
|
|
1737
|
+
for (const [name, by] of blockedNow) blockedSoFar.set(name, by);
|
|
1738
|
+
if (blockedNow.size > 0) 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." }));
|
|
1739
|
+
const wavePlan = prWavePlan(p.plan, p.components, k, [...completed, ...blockedNow.keys()]);
|
|
1740
|
+
if (wavePlan.order.length === 0) {
|
|
1741
|
+
if (blockedNow.size > 0) recordAttempt();
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1376
1744
|
const inThisWave = new Set(wavePlan.order);
|
|
1377
1745
|
|
|
1378
1746
|
// Wave 1 of the apply that just planned every member applies those plans:
|
|
@@ -1389,18 +1757,26 @@ async function prApplyWaves(
|
|
|
1389
1757
|
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
|
|
1390
1758
|
} else {
|
|
1391
1759
|
const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
|
|
1392
|
-
const fresh = await
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1760
|
+
const fresh = await hookedPlan(p, wavePlan.order, () =>
|
|
1761
|
+
planPrSet({
|
|
1762
|
+
components: p.components,
|
|
1763
|
+
plan: wavePlan,
|
|
1764
|
+
registry: p.registry,
|
|
1765
|
+
env: p.env,
|
|
1766
|
+
seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
|
|
1767
|
+
...(events ? { onRunEvent: (body: RunEventBody) => events.emit(body) } : {}),
|
|
1768
|
+
}),
|
|
1769
|
+
);
|
|
1400
1770
|
if (fresh.failed) {
|
|
1401
|
-
for (const m of fresh.members)
|
|
1771
|
+
for (const m of fresh.members) {
|
|
1772
|
+
after.set(m.member, m);
|
|
1773
|
+
if (m.status === "plan-failed") failedSoFar.add(m.component);
|
|
1774
|
+
}
|
|
1402
1775
|
setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
|
|
1403
|
-
|
|
1776
|
+
// #4291: the later waves still run what does not depend on this wave.
|
|
1777
|
+
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 + "." }));
|
|
1778
|
+
planFailedIn ??= k;
|
|
1779
|
+
continue;
|
|
1404
1780
|
}
|
|
1405
1781
|
const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
|
|
1406
1782
|
set = fresh;
|
|
@@ -1455,6 +1831,12 @@ async function prApplyWaves(
|
|
|
1455
1831
|
}
|
|
1456
1832
|
}
|
|
1457
1833
|
|
|
1834
|
+
// #4288: the wave's apply-side hooks, around its apply.
|
|
1835
|
+
const waveBefore = runStageHook(p, "before-apply", wavePlan.order, undefined, `nothing in wave ${k} was applied`);
|
|
1836
|
+
if (waveBefore) {
|
|
1837
|
+
setWave({ ...entry, status: "failed" });
|
|
1838
|
+
return stop(report({ status: "failed", message: waveBefore }), 1);
|
|
1839
|
+
}
|
|
1458
1840
|
const applied = await applyPrSet({
|
|
1459
1841
|
components: p.components,
|
|
1460
1842
|
plan: wavePlan,
|
|
@@ -1470,6 +1852,7 @@ async function prApplyWaves(
|
|
|
1470
1852
|
onComponentSettled: (settled, outputs) => {
|
|
1471
1853
|
if (settled.status === "ok") {
|
|
1472
1854
|
completed.add(settled.component);
|
|
1855
|
+
failedSoFar.delete(settled.component);
|
|
1473
1856
|
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
1474
1857
|
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
1475
1858
|
} else if (settled.status === "fail") {
|
|
@@ -1484,14 +1867,33 @@ async function prApplyWaves(
|
|
|
1484
1867
|
const outputs = applied.run.componentOutputs[name];
|
|
1485
1868
|
if (outputs) outputsSoFar[name] = outputs;
|
|
1486
1869
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1870
|
+
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`);
|
|
1871
|
+
setWave({ ...entry, status: applied.status === "applied" && !waveAfter ? "applied" : "failed" });
|
|
1872
|
+
await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
1489
1873
|
if (applied.status !== "applied") {
|
|
1490
|
-
|
|
1874
|
+
// #4291: a failed member blocks only its dependents, so the later waves still run.
|
|
1875
|
+
lastFailure = { results: applied.run.results, gated: applied.run.status === "gated" };
|
|
1876
|
+
recordAttempt();
|
|
1877
|
+
continue;
|
|
1491
1878
|
}
|
|
1879
|
+
if (waveAfter) return stop(report({ status: "failed", message: waveAfter }), 1);
|
|
1492
1880
|
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(", ")}`));
|
|
1493
1881
|
}
|
|
1494
1882
|
|
|
1883
|
+
// #4291: a failure, or a member it blocked, fails the job once every other member applied.
|
|
1884
|
+
const blockedHere = [...blockedSoFar.keys()].filter((name) => (only === undefined ? true : (p.plan.waves[only - 1] ?? []).includes(name)));
|
|
1885
|
+
if (lastFailure || planFailedIn !== undefined || blockedHere.length > 0) {
|
|
1886
|
+
const blockedLine = blockedSoFar.size > 0 ? ` Blocked: ${blockedWords(blockedSoFar)}.` : "";
|
|
1887
|
+
const message = planFailedIn !== undefined && !lastFailure
|
|
1888
|
+
? `A member of wave ${planFailedIn} failed to plan, so nothing in wave ${planFailedIn} was applied.${blockedLine}`
|
|
1889
|
+
: blockedLine ? blockedLine.trim() : undefined;
|
|
1890
|
+
return stop(
|
|
1891
|
+
report({ status: "failed", ...(message ? { message } : {}) }),
|
|
1892
|
+
lastFailure ? failedRunExitCode(lastFailure.results) : 1,
|
|
1893
|
+
lastFailure?.gated ? "gated" : undefined,
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1495
1897
|
const done = p.plan.order.every((name) => completed.has(name));
|
|
1496
1898
|
if (green && done) await recordGreenApplied(p, green, approvedSet.doc.digest, approval, via);
|
|
1497
1899
|
return stop(report({ status: done ? "applied" : "wave-applied" }), 0);
|
|
@@ -1506,3 +1908,45 @@ function writeWaveChangeSet(ctx: CommandContext, wave: number, doc: PrPlanSet["d
|
|
|
1506
1908
|
writeFileSync(join(dir, name), body);
|
|
1507
1909
|
upload?.file(name, body);
|
|
1508
1910
|
}
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* chant components pr-post --pr <n> --head <sha> --env <env> --forge <kind> [--member <name>] [--output <dir>] [--result <result>] (#4295)
|
|
1914
|
+
*
|
|
1915
|
+
* Post the plan note and the `chant/plan` status from the report a plan job
|
|
1916
|
+
* wrote with `pr-plan --no-status`, in a job that runs none of the change's
|
|
1917
|
+
* code. The pull request, the head, the environment and the member come from
|
|
1918
|
+
* the flags, and a report that names others is refused (`../../pr-post.ts`).
|
|
1919
|
+
* It exits 0 once it has posted, whatever the plan said, so the plan job's
|
|
1920
|
+
* own result stays the one that fails the run.
|
|
1921
|
+
*/
|
|
1922
|
+
export async function runComponentsPrPost(ctx: CommandContext): Promise<number> {
|
|
1923
|
+
const { args } = ctx;
|
|
1924
|
+
if (args.pr === undefined) return fail("pr-post needs the pull or merge request: --pr <n>");
|
|
1925
|
+
if (!args.head || !/^[0-9a-f]{40}$/.test(args.head)) return fail("pr-post needs the full commit the plan job checked out: --head <sha>");
|
|
1926
|
+
if (!args.env) return fail("pr-post needs the environment the plan was for: --env <env>");
|
|
1927
|
+
if (!args.forge || !(FORGE_KINDS as readonly string[]).includes(args.forge)) return fail(`pr-post posts to a forge: --forge ${FORGE_KINDS.join("|")}`);
|
|
1928
|
+
if (args.members && args.members.length > 1) return fail(`pr-post takes one --member, not ${args.members.join(", ")}`);
|
|
1929
|
+
let forge: PrForge;
|
|
1930
|
+
try {
|
|
1931
|
+
forge = forgeFromEnv(args.forge as ForgeKind);
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
1934
|
+
}
|
|
1935
|
+
const { postPlanReport } = await import("../../pr-post");
|
|
1936
|
+
const member = args.members?.[0];
|
|
1937
|
+
const posted = await postPlanReport({
|
|
1938
|
+
forge,
|
|
1939
|
+
dir: resolve(args.output ?? PR_REPORT_DIR),
|
|
1940
|
+
pr: args.pr,
|
|
1941
|
+
head: args.head,
|
|
1942
|
+
env: args.env,
|
|
1943
|
+
...(member ? { member } : {}),
|
|
1944
|
+
...(args.planResult ? { result: args.planResult } : {}),
|
|
1945
|
+
// #4289: the plan job could not ask the forge, so the note job does, from the Ops at the commit it checked out.
|
|
1946
|
+
overdue: () => overdueOps(forge, member),
|
|
1947
|
+
});
|
|
1948
|
+
for (const message of posted.warnings) console.error(formatWarning({ message }));
|
|
1949
|
+
if (posted.refused) console.error(formatWarning({ message: `the plan report was not used: ${posted.refused}` }));
|
|
1950
|
+
console.error(formatInfo(`pr-post: ${prStatusContext("plan", member)} ${posted.state}: ${posted.description}${posted.noted ? "; note posted" : ""}`));
|
|
1951
|
+
return 0;
|
|
1952
|
+
}
|