@intentius/chant 0.124.40 → 0.124.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +42 -5
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +76 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +615 -55
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +49 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +46 -2
- package/dist/cli/registry.d.ts +31 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +37 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +44 -4
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +92 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +150 -21
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +8 -3
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +28 -43
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +170 -120
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/op/stage-hooks.d.ts +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +64 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +331 -44
- package/dist/pr-loop.d.ts +107 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +84 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/pr-requires.d.ts +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +243 -243
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +66 -4
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +69 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +149 -2
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +94 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +4 -1
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +18 -1
- package/src/cli/handlers/fan-out.ts +43 -6
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-before-merge.test.ts +276 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +605 -61
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/parse-args.ts +34 -2
- package/src/cli/registry.ts +31 -2
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +58 -3
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/gate-plan.ts +3 -0
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +194 -21
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +14 -2
- package/src/components/wave-plan.ts +38 -3
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +197 -183
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/op/stage-hooks.ts +251 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +347 -35
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +170 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/pr-requires.test.ts +184 -0
- package/src/pr-requires.ts +79 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-init.ts +5 -4
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +107 -8
- package/src/workspace/ci-run.ts +74 -5
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +149 -2
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +140 -5
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +94 -3
- package/src/workspace/reason-codes.ts +1 -1
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
package/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";
|
|
@@ -104,10 +106,13 @@ import {
|
|
|
104
106
|
prStatusState,
|
|
105
107
|
renderPrNote,
|
|
106
108
|
type PrApproval,
|
|
109
|
+
type PrBeforeMerge,
|
|
107
110
|
type PrMember,
|
|
111
|
+
type PrPolicyReport,
|
|
108
112
|
type PrReport,
|
|
109
113
|
type PrWave,
|
|
110
114
|
type PrRound,
|
|
115
|
+
type PrOverdue,
|
|
111
116
|
} from "../../pr-loop";
|
|
112
117
|
import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
113
118
|
import { GITHUB_COMMENT_LIMIT, GITLAB_NOTE_LIMIT } from "../../plan-summary";
|
|
@@ -124,12 +129,16 @@ import { withComponentEndpoint } from "../component-endpoint";
|
|
|
124
129
|
import type { ComponentChangeSignal, FanOutPlan } from "../../components/fan-out";
|
|
125
130
|
import { failedRunExitCode, type DriverComponent } from "../../components/driver";
|
|
126
131
|
import type { CapabilityRegistry } from "../../components/capability";
|
|
132
|
+
import { prLoopHooksAt, runPrHooks, type PrHookPlace, type PrLoopHooks } from "../../components/pr-hooks";
|
|
133
|
+
import { decidePrPolicyOverride, prLoopPolicyAt, prPolicyReport, runPrPolicy, spendPrPolicyOverride, type PrLoopPolicy } from "../../components/pr-policy";
|
|
134
|
+
import type { OpWaveHookMoment } from "../../op/op-waves";
|
|
135
|
+
import type { PrHookOutcome } from "../../pr-loop";
|
|
127
136
|
|
|
128
137
|
|
|
129
138
|
/** Where the reports land unless `--output` says. */
|
|
130
139
|
export const PR_REPORT_DIR = ".chant/pr";
|
|
131
140
|
|
|
132
|
-
interface Prepared {
|
|
141
|
+
export interface Prepared {
|
|
133
142
|
config: ChantConfig;
|
|
134
143
|
env: string;
|
|
135
144
|
/** The workspace member, with `--member` (#3465). */
|
|
@@ -148,6 +157,25 @@ interface Prepared {
|
|
|
148
157
|
signal: ComponentChangeSignal;
|
|
149
158
|
registry: CapabilityRegistry;
|
|
150
159
|
seededOutputs: Record<string, Record<string, unknown>>;
|
|
160
|
+
/** `ci.hooks` from the declaration at base (#4288). */
|
|
161
|
+
hooks: PrLoopHooks;
|
|
162
|
+
/** Every hook the stage ran so far, for the report's `hooks`. */
|
|
163
|
+
hookOutcomes: PrHookOutcome[];
|
|
164
|
+
/** Set when a hook failed: the report's message. */
|
|
165
|
+
hookFailed?: string;
|
|
166
|
+
/** Where the stage's hooks run, once its op is known. */
|
|
167
|
+
hookPlace?: PrHookPlace;
|
|
168
|
+
/**
|
|
169
|
+
* Set when `ci.branches` or `ci.labels` (#4292) leave the member out of this
|
|
170
|
+
* stage: why. The plan is then empty, so the stage reports `nothing`.
|
|
171
|
+
*/
|
|
172
|
+
routedAway?: string;
|
|
173
|
+
/** `ci.policy` from the declaration at base (#4294). */
|
|
174
|
+
policy: PrLoopPolicy;
|
|
175
|
+
/** What the policy said about the plan, for the report's `policy`. */
|
|
176
|
+
policyReport?: PrPolicyReport;
|
|
177
|
+
/** `pr-apply --before-merge` (#4301): the requires checks, for the report's `beforeMerge`. */
|
|
178
|
+
beforeMerge?: PrBeforeMerge;
|
|
151
179
|
}
|
|
152
180
|
|
|
153
181
|
const fail = (message: string, hint?: string): number => {
|
|
@@ -155,8 +183,8 @@ const fail = (message: string, hint?: string): number => {
|
|
|
155
183
|
return 1;
|
|
156
184
|
};
|
|
157
185
|
|
|
158
|
-
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. */
|
|
159
|
-
async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"): Promise<Prepared | number> {
|
|
186
|
+
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. `pr-env up` (#4298) prepares as the plan does. */
|
|
187
|
+
export async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"): Promise<Prepared | number> {
|
|
160
188
|
const { args } = ctx;
|
|
161
189
|
const projectPath = resolve(".");
|
|
162
190
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
@@ -236,8 +264,27 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"):
|
|
|
236
264
|
}
|
|
237
265
|
}
|
|
238
266
|
|
|
267
|
+
// #4288: the stage hooks, read from the declaration at base so the change cannot edit what its own jobs run.
|
|
268
|
+
let hooks: PrLoopHooks;
|
|
269
|
+
try {
|
|
270
|
+
hooks = prLoopHooksAt(projectPath, base);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
273
|
+
}
|
|
274
|
+
if (hooks.note) console.error(formatInfo(hooks.note));
|
|
275
|
+
// #4294: the policy, read from the declaration at base like the hooks.
|
|
276
|
+
let policy: PrLoopPolicy;
|
|
277
|
+
try {
|
|
278
|
+
policy = prLoopPolicyAt(projectPath, base);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
281
|
+
}
|
|
282
|
+
if (policy.note) console.error(formatInfo(policy.note));
|
|
283
|
+
|
|
239
284
|
const env = args.env ?? "local";
|
|
240
285
|
const registry = await fanOutRegistry(projectPath, config);
|
|
286
|
+
const routedAway = member ? await routing(ctx, stage, projectPath, member, base, forge) : undefined;
|
|
287
|
+
if (routedAway) console.error(formatInfo(routedAway));
|
|
241
288
|
return {
|
|
242
289
|
config,
|
|
243
290
|
env,
|
|
@@ -247,13 +294,89 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"):
|
|
|
247
294
|
gate: args.gate ?? PR_APPLY_GATE,
|
|
248
295
|
...(forge ? { forge } : {}),
|
|
249
296
|
components: derived.components,
|
|
250
|
-
plan: derived.plan,
|
|
297
|
+
plan: routedAway ? { ...derived.plan, order: [], waves: [], seeds: [], indeterminate: [] } : derived.plan,
|
|
251
298
|
signal: derived.signal,
|
|
252
299
|
registry,
|
|
253
300
|
seededOutputs,
|
|
301
|
+
hooks,
|
|
302
|
+
hookOutcomes: [],
|
|
303
|
+
...(routedAway ? { routedAway } : {}),
|
|
304
|
+
policy,
|
|
254
305
|
};
|
|
255
306
|
}
|
|
256
307
|
|
|
308
|
+
/** Set where `p`'s hooks run, once the stage knows its op (#4288). */
|
|
309
|
+
export function placeHooks(ctx: CommandContext, p: Prepared, stage: PrHookPlace["stage"], op: string): void {
|
|
310
|
+
p.hookPlace = { op, env: p.env, stage, dir: ctx.args.output ?? PR_REPORT_DIR, cwd: resolve("."), log: (line) => console.error(line) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Run one moment's hooks for `components` (#4288), keeping their outcomes
|
|
315
|
+
* for the report. Returns the failure as the report's message, or undefined
|
|
316
|
+
* when no hook failed. A hold is logged: the pull request's gate already
|
|
317
|
+
* waits for a person's approval of the digest.
|
|
318
|
+
*/
|
|
319
|
+
export function runStageHook(p: Prepared, moment: OpWaveHookMoment, components: readonly string[], planned?: PrPlanSet, what = "nothing was applied"): string | undefined {
|
|
320
|
+
if (!p.hookPlace || components.length === 0) return undefined;
|
|
321
|
+
const ran = runPrHooks(p.hooks, moment, p.hookPlace, components, planned ? { members: planned.members, plans: planned.plans } : undefined);
|
|
322
|
+
p.hookOutcomes.push(...ran.outcomes);
|
|
323
|
+
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`));
|
|
324
|
+
if (!ran.failed) return undefined;
|
|
325
|
+
p.hookFailed = `The ${ran.failed}, so ${what}.`;
|
|
326
|
+
return p.hookFailed;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const EMPTY_SET = (): PrPlanSet => ({ doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: true });
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Plan with the plan-side hooks around it (#4288): before-observe and
|
|
333
|
+
* before-plan, then the plan, then after-plan. A failed hook gives a failed
|
|
334
|
+
* set, so the stage reports `plan-failed` and nothing applies.
|
|
335
|
+
*/
|
|
336
|
+
async function hookedPlan(p: Prepared, components: readonly string[], plan: () => Promise<PrPlanSet>): Promise<PrPlanSet> {
|
|
337
|
+
if (runStageHook(p, "before-observe", components, undefined, "nothing was planned or applied")) return EMPTY_SET();
|
|
338
|
+
if (runStageHook(p, "before-plan", components, undefined, "nothing was planned or applied")) return EMPTY_SET();
|
|
339
|
+
const set = await plan();
|
|
340
|
+
if (set.failed) return set;
|
|
341
|
+
if (runStageHook(p, "after-plan", components, set)) return { ...set, failed: true };
|
|
342
|
+
return set;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether `ci.branches` or `ci.labels` (#4292), read from the declaration at
|
|
347
|
+
* base, leave `member` out of this stage: the reason, or undefined when the
|
|
348
|
+
* member runs. The record and apply stages read `ci.branches` against the
|
|
349
|
+
* pushed branch (`--branch`, else the forge's variables); the plan stage
|
|
350
|
+
* reads `ci.labels` against the pull request's labels. A label only narrows
|
|
351
|
+
* what is planned: the record and apply stages never read labels.
|
|
352
|
+
*/
|
|
353
|
+
async function routing(ctx: CommandContext, stage: "plan" | "apply" | "record", projectPath: string, member: string, base: string, forge: PrForge | undefined): Promise<string | undefined> {
|
|
354
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
355
|
+
const { appliesFrom, plansUnderLabels, pushedBranchFromEnv } = await import("../../workspace/ci-branches");
|
|
356
|
+
const ci = scopeSource(projectPath, base).declaration?.ci;
|
|
357
|
+
if (stage !== "plan") {
|
|
358
|
+
if (!ci?.branches) return undefined;
|
|
359
|
+
const pushed = ctx.args.branch ?? pushedBranchFromEnv();
|
|
360
|
+
if (!pushed) return undefined;
|
|
361
|
+
const route = appliesFrom(ci.branches, member, pushed);
|
|
362
|
+
if (!route.applies) return route.note;
|
|
363
|
+
console.error(formatInfo(route.note));
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
if (!ci?.labels || ctx.args.pr === undefined || !forge?.labels) return undefined;
|
|
367
|
+
let carried: string[];
|
|
368
|
+
try {
|
|
369
|
+
carried = await forge.labels(ctx.args.pr);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
console.error(formatWarning({ message: `could not read the labels of #${ctx.args.pr}, so every member plans: ${err instanceof Error ? err.message : String(err)}` }));
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
const filter = plansUnderLabels(ci.labels, member, carried);
|
|
375
|
+
if (!filter.plans) return filter.note;
|
|
376
|
+
if (filter.note) console.error(formatInfo(filter.note));
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
257
380
|
/**
|
|
258
381
|
* The one `--member` name, checked against the workspace member that owns
|
|
259
382
|
* `projectPath`: a pipeline copied into another member would otherwise
|
|
@@ -280,8 +403,12 @@ async function checkMember(projectPath: string, names: string[]): Promise<string
|
|
|
280
403
|
return name;
|
|
281
404
|
}
|
|
282
405
|
|
|
283
|
-
/** Plan every member the derivation selects. */
|
|
284
|
-
function planAll(p: Prepared, onRunEvent?: RunEventSink): Promise<PrPlanSet> {
|
|
406
|
+
/** Plan every member the derivation selects, between the plan-side hooks (#4288). */
|
|
407
|
+
export function planAll(p: Prepared, onRunEvent?: RunEventSink): Promise<PrPlanSet> {
|
|
408
|
+
return hookedPlan(p, p.plan.order, () => planBare(p, onRunEvent));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function planBare(p: Prepared, onRunEvent?: RunEventSink): Promise<PrPlanSet> {
|
|
285
412
|
return planPrSet({
|
|
286
413
|
components: p.components,
|
|
287
414
|
plan: p.plan,
|
|
@@ -343,7 +470,7 @@ async function publish(forge: PrForge | undefined, report: PrReport, note: strin
|
|
|
343
470
|
if (report.pr === null) return;
|
|
344
471
|
// A member the change does not reach has nothing to say: one note per
|
|
345
472
|
// member would put a line on every pull request for each member it skips.
|
|
346
|
-
if (report.member && report.status === "nothing") return;
|
|
473
|
+
if (report.member && report.status === "nothing" && !report.overdue?.length) return;
|
|
347
474
|
try {
|
|
348
475
|
await forge.upsertNote(report.pr, prNoteMarker(report.env, report.member), note);
|
|
349
476
|
} catch (err) {
|
|
@@ -366,6 +493,12 @@ function printMembers(report: PrReport): void {
|
|
|
366
493
|
}
|
|
367
494
|
|
|
368
495
|
async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload?: PrReportUpload): Promise<void> {
|
|
496
|
+
// #4288: the hooks the stage ran, and a failed hook's message in place of the stage's own.
|
|
497
|
+
if (p.hookOutcomes.length > 0) report = { ...report, hooks: [...p.hookOutcomes] };
|
|
498
|
+
if (p.hookFailed) report = { ...report, message: p.hookFailed };
|
|
499
|
+
if (p.routedAway && report.status === "nothing" && !report.message) report = { ...report, message: p.routedAway };
|
|
500
|
+
if (p.policyReport) report = { ...report, policy: p.policyReport };
|
|
501
|
+
if (p.beforeMerge) report = { ...report, beforeMerge: p.beforeMerge };
|
|
369
502
|
const note = noteFor(report, p.forge);
|
|
370
503
|
writeOutputs(ctx, report, note, upload);
|
|
371
504
|
if (!ctx.args.noStatus) await publish(p.forge, report, note);
|
|
@@ -420,6 +553,86 @@ function openUpload(events: StageEvents, stage: "plan" | "apply" | "record", pr:
|
|
|
420
553
|
return events.upload;
|
|
421
554
|
}
|
|
422
555
|
|
|
556
|
+
/** What the policy step decided (#4294): a stop, or whether the plan is held and the override it applies under. */
|
|
557
|
+
interface PolicyJudged {
|
|
558
|
+
stop?: { refusal: "policy-denied" | "policy-error"; message: string; code: number };
|
|
559
|
+
held: boolean;
|
|
560
|
+
override?: NonNullable<PrPolicyReport["override"]>;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Run `ci.policy` at base over the planned set (#4294) and keep what it said
|
|
565
|
+
* on `p` for the report. A denial is decided against the override ledger:
|
|
566
|
+
* read only at the plan stage, and recorded for an override at the apply.
|
|
567
|
+
*/
|
|
568
|
+
async function judgePolicy(p: Prepared, at: { stage: "plan" | "apply"; op: string; pr: number | null; set: PrPlanSet }): Promise<PolicyJudged> {
|
|
569
|
+
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 });
|
|
570
|
+
if (!verdict) return { held: false };
|
|
571
|
+
const report = prPolicyReport(verdict, p.policy.base);
|
|
572
|
+
p.policyReport = report;
|
|
573
|
+
for (const w of verdict.warnings) console.error(formatWarning({ message: `policy warning: ${w}` }));
|
|
574
|
+
if (verdict.result === "error") {
|
|
575
|
+
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}` } };
|
|
576
|
+
}
|
|
577
|
+
const held = verdict.holds.length > 0;
|
|
578
|
+
for (const h of verdict.holds) console.error(formatInfo(`policy holds the plan for a person's approval (${h.rule}): ${h.message}`));
|
|
579
|
+
if (verdict.result !== "denied") return { held };
|
|
580
|
+
const denied = verdict.denials.map((d) => `${d.rule}: ${d.message}`).join("; ");
|
|
581
|
+
let decided: Awaited<ReturnType<typeof decidePrPolicyOverride>>;
|
|
582
|
+
try {
|
|
583
|
+
// The plan stage records nothing, and reads the applied records so a spent override reads as spent.
|
|
584
|
+
const real = gitGateLedgerPort();
|
|
585
|
+
const port = at.stage === "plan" ? { ...readOnlyLedger(real), ...(real.readApplied ? { readApplied: real.readApplied.bind(real) } : {}) } : real;
|
|
586
|
+
decided = await decidePrPolicyOverride({
|
|
587
|
+
port,
|
|
588
|
+
subject: at.op,
|
|
589
|
+
digest: at.set.doc.digest,
|
|
590
|
+
rules: verdict.rules,
|
|
591
|
+
base: p.base,
|
|
592
|
+
cwd: resolve("."),
|
|
593
|
+
sealed: await gateIsSealed(p.gate),
|
|
594
|
+
record: at.stage === "apply",
|
|
595
|
+
description: `${at.op}: plan ${at.set.doc.digest} denied by ${verdict.rules.join(", ")}`,
|
|
596
|
+
});
|
|
597
|
+
} catch (err) {
|
|
598
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
599
|
+
if (at.stage === "plan") {
|
|
600
|
+
console.error(formatWarning({ message: `could not read the override ledger, so no override reads as standing: ${why}` }));
|
|
601
|
+
return { held, stop: { refusal: "policy-denied", code: 1, message: `The policy denied this plan: ${denied}.` } };
|
|
602
|
+
}
|
|
603
|
+
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.` } };
|
|
604
|
+
}
|
|
605
|
+
if (decided.status === "overridden") {
|
|
606
|
+
report.override = decided.override;
|
|
607
|
+
console.error(formatInfo(`the policy denial (${verdict.rules.join(", ")}) is overridden by ${decided.override.by} at ${decided.override.at}: ${decided.override.reason}`));
|
|
608
|
+
return { held, override: decided.override };
|
|
609
|
+
}
|
|
610
|
+
report.overrideCommand = decided.command;
|
|
611
|
+
for (const why of decided.refusals) console.error(formatInfo(`an override does not count: ${why}`));
|
|
612
|
+
const who = decided.overriders.length ? `${decided.overriders.join(", ")} may` : "nobody listed in .chant/overriders at base may";
|
|
613
|
+
const moved = decided.moved ? ` ${decided.moved.by} overrode an earlier plan or other rules, which counts for nothing here.` : "";
|
|
614
|
+
return {
|
|
615
|
+
held,
|
|
616
|
+
stop: {
|
|
617
|
+
refusal: "policy-denied",
|
|
618
|
+
code: at.stage === "plan" ? 1 : GATED_EXIT_CODE,
|
|
619
|
+
message: `The policy denied this plan: ${denied}.${moved} Nothing was applied. ${who} override it: ${decided.command}`,
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Spend the override the apply runs under (#4294), before anything applies. The failure's message, or undefined. */
|
|
625
|
+
async function spendPolicy(op: string, judged: PolicyJudged, head: string): Promise<string | undefined> {
|
|
626
|
+
if (!judged.override) return undefined;
|
|
627
|
+
try {
|
|
628
|
+
await spendPrPolicyOverride(gitGateLedgerPort(), op, judged.override, { commit: head });
|
|
629
|
+
} catch (err) {
|
|
630
|
+
return `Could not record the override this apply runs under, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`;
|
|
631
|
+
}
|
|
632
|
+
console.error(formatInfo(`applies under the override by ${judged.override.by}, which is now spent`));
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
|
|
423
636
|
/** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
424
637
|
export function runComponentsPrPlan(ctx: CommandContext): Promise<number> {
|
|
425
638
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
@@ -433,12 +646,15 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
433
646
|
const p = await prepare(ctx, "plan");
|
|
434
647
|
if (typeof p === "number") return p;
|
|
435
648
|
const op = prOp(ctx.args.pr, p.member);
|
|
649
|
+
placeHooks(ctx, p, "plan", op);
|
|
436
650
|
const upload = openUpload(stage, "plan", ctx.args.pr, p);
|
|
437
651
|
// `--events-json` (#4041): planning, then where the approval stands.
|
|
438
652
|
const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") }, upload?.line);
|
|
439
653
|
stage.stream = events;
|
|
440
654
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
441
655
|
const set = await planAll(p, events?.emit);
|
|
656
|
+
// #4294: the policy at base over the planned change set.
|
|
657
|
+
const judged = p.plan.order.length > 0 && !set.failed ? await judgePolicy(p, { stage: "plan", op, pr: ctx.args.pr, set }) : { held: false };
|
|
442
658
|
|
|
443
659
|
let approval: PrApproval = { status: "pending" };
|
|
444
660
|
if (p.plan.order.length > 0) {
|
|
@@ -452,7 +668,8 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
452
668
|
}
|
|
453
669
|
}
|
|
454
670
|
const status = p.plan.order.length === 0 ? "nothing" : set.failed ? "plan-failed" : "planned";
|
|
455
|
-
const
|
|
671
|
+
const overdue = await overdueOps(p.forge, p.member);
|
|
672
|
+
const planned = prReport({
|
|
456
673
|
stage: "plan",
|
|
457
674
|
pr: ctx.args.pr,
|
|
458
675
|
env: p.env,
|
|
@@ -468,6 +685,7 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
468
685
|
status,
|
|
469
686
|
...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
|
|
470
687
|
});
|
|
688
|
+
const report: PrReport = overdue.length > 0 ? { ...planned, overdue } : planned;
|
|
471
689
|
await finish(ctx, p, report, upload);
|
|
472
690
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
473
691
|
events?.settle(settleOf(status), status === "plan-failed" ? "a member failed to plan" : undefined);
|
|
@@ -478,21 +696,65 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
478
696
|
console.error(formatInfo(`plan : ${report.digest}`));
|
|
479
697
|
console.error(formatInfo(approval.status === "approved" ? `approved by ${(approval.approvedBy ?? []).join(", ")}` : `approve : ${prApproveCommand(report)}`));
|
|
480
698
|
}
|
|
481
|
-
|
|
699
|
+
if (judged.stop) console.error(formatWarning({ message: judged.stop.message }));
|
|
700
|
+
return status === "plan-failed" || judged.stop ? 1 : 0;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The scheduled watch and reconcile Ops that stopped running (#4289,
|
|
705
|
+
* `op/schedule-overdue.ts`), asked of the forge. Without a forge, or when
|
|
706
|
+
* the Ops cannot be read, nothing is checked and the plan goes on. The
|
|
707
|
+
* generated plan holds no forge token (#4295), so there the plan-note job's
|
|
708
|
+
* `pr-post` asks instead, reading the Ops at the target branch's commit.
|
|
709
|
+
*/
|
|
710
|
+
async function overdueOps(forge: PrForge | undefined, member: string | undefined): Promise<PrOverdue[]> {
|
|
711
|
+
if (!forge?.scheduledRuns) return [];
|
|
712
|
+
try {
|
|
713
|
+
const { discoverOps } = await import("../../op/discover");
|
|
714
|
+
const { scheduledWatches, checkOverdueOps } = await import("../../op/schedule-overdue");
|
|
715
|
+
const discovered = await discoverOps({ cwd: resolve(".") });
|
|
716
|
+
const watches = scheduledWatches([...discovered.ops.values()].map((d) => d.config));
|
|
717
|
+
if (watches.length === 0) return [];
|
|
718
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
|
|
719
|
+
if (top.status !== 0) return [];
|
|
720
|
+
return await checkOverdueOps({
|
|
721
|
+
watches,
|
|
722
|
+
forge,
|
|
723
|
+
repoRoot: top.stdout.trim(),
|
|
724
|
+
...(member ? { member } : {}),
|
|
725
|
+
now: new Date(),
|
|
726
|
+
log: (line) => console.error(formatWarning({ message: line })),
|
|
727
|
+
});
|
|
728
|
+
} catch (err) {
|
|
729
|
+
console.error(formatWarning({ message: `could not check the scheduled Ops, so none is reported overdue: ${err instanceof Error ? err.message : String(err)}` }));
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
482
732
|
}
|
|
483
733
|
|
|
484
734
|
/** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
485
735
|
export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
|
|
486
736
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
487
|
-
return withStageEvents("pr-apply", ctx, (events) =>
|
|
737
|
+
return withStageEvents("pr-apply", ctx, async (events) => {
|
|
738
|
+
// #4301: an apply before merge asks the forge its requires checks before any of the head's code loads, the config included.
|
|
739
|
+
let beforeMerge: PrBeforeMerge | undefined;
|
|
740
|
+
if (ctx.args.beforeMerge) {
|
|
741
|
+
const checked = await beforeMergeChecks(ctx);
|
|
742
|
+
if (typeof checked === "number") return checked;
|
|
743
|
+
beforeMerge = checked;
|
|
744
|
+
}
|
|
745
|
+
return withComponentEndpoint(ctx, () => prApply(ctx, events, beforeMerge));
|
|
746
|
+
});
|
|
488
747
|
}
|
|
489
748
|
|
|
490
|
-
async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
749
|
+
async function prApply(ctx: CommandContext, stage: StageEvents, beforeMerge?: PrBeforeMerge): Promise<number> {
|
|
491
750
|
// #4168: the review-rules security group turns --require-review on, read from the declaration at base.
|
|
492
751
|
const { securityGroupsAt } = await import("../../workspace/security");
|
|
493
752
|
const reviewRules = !ctx.args.requireReview && securityGroupsAt(process.cwd(), ctx.args.base).has("review-rules");
|
|
494
753
|
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>");
|
|
495
754
|
if (reviewRules) ctx = { ...ctx, args: { ...ctx.args, requireReview: true } };
|
|
755
|
+
// #4293: ci.locks, read at base like the security groups, so the change cannot turn its own locks off.
|
|
756
|
+
const { ciLocksAt } = await import("../../workspace/ci-locks");
|
|
757
|
+
const locks = ciLocksAt(process.cwd(), ctx.args.base);
|
|
496
758
|
const { args } = ctx;
|
|
497
759
|
if (args.requireReview && !args.forge) return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
|
|
498
760
|
if (args.wave !== undefined && !args.waveGate) return fail("--wave runs one wave of a --wave-gate apply, so it needs --wave-gate <name>");
|
|
@@ -507,6 +769,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
507
769
|
}
|
|
508
770
|
const p = await prepare(ctx, "apply");
|
|
509
771
|
if (typeof p === "number") return p;
|
|
772
|
+
if (beforeMerge) p.beforeMerge = beforeMerge;
|
|
510
773
|
|
|
511
774
|
let pr: number | null = args.pr ?? null;
|
|
512
775
|
if (pr === null && !green && p.forge && p.plan.order.length > 0) {
|
|
@@ -517,6 +780,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
517
780
|
}
|
|
518
781
|
}
|
|
519
782
|
const op = green ? greenOp(green.commit, p.member) : pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
783
|
+
placeHooks(ctx, p, "apply", op);
|
|
520
784
|
// The run id the release records carry, chosen before the run so that
|
|
521
785
|
// `--events-json` (#4041, #4007) names the run by it too.
|
|
522
786
|
const release = autoReleaseRun(`${op}-${Date.now()}`);
|
|
@@ -524,6 +788,17 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
524
788
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
525
789
|
stage.stream = events;
|
|
526
790
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
791
|
+
if (locks && p.plan.order.length > 0) {
|
|
792
|
+
const held = await heldLocksOf(p, pr);
|
|
793
|
+
if (typeof held === "number") return held;
|
|
794
|
+
if (held) {
|
|
795
|
+
const set: PrPlanSet = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
796
|
+
return stopper(ctx, p, events, upload)(
|
|
797
|
+
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 }),
|
|
798
|
+
GATED_EXIT_CODE,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
527
802
|
if (args.waveGate) return prApplyWaves(ctx, p, { pr, op, release, upload, events, ...(green ? { green } : {}) });
|
|
528
803
|
|
|
529
804
|
// `--resume` (#3464): an earlier attempt at this change on this commit
|
|
@@ -593,10 +868,14 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
593
868
|
|
|
594
869
|
let approval: PrApproval;
|
|
595
870
|
let via: "merges" | "gate" = "gate";
|
|
871
|
+
// #4294: the policy at base, before the gate. A resume runs under the decision of the attempt it finishes.
|
|
872
|
+
let judged: PolicyJudged = { held: false };
|
|
596
873
|
if (resumed) {
|
|
597
874
|
approval = resumed.approval;
|
|
598
875
|
} else {
|
|
599
|
-
|
|
876
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
877
|
+
if (judged.stop) return stop(prReport({ ...base, approval: { status: "pending" }, status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
878
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
600
879
|
approval = decided.approval;
|
|
601
880
|
if (!decided.satisfied) return stop(prReport({ ...base, approval, status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
602
881
|
via = decided.via;
|
|
@@ -609,6 +888,14 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
609
888
|
emitReviewStop(events, review.message);
|
|
610
889
|
return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
|
|
611
890
|
}
|
|
891
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
892
|
+
if (unspent) return stop(prReport({ ...base, approval, status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
893
|
+
// #4293, #4301: under ci.locks an apply before merge holds its targets until the pull request merges or closes.
|
|
894
|
+
if (locks && beforeMerge && pr !== null) {
|
|
895
|
+
const taken = await takeApplyLocks(p, pr);
|
|
896
|
+
if (typeof taken === "number") return taken;
|
|
897
|
+
if (taken) return stop(prReport({ ...base, approval, status: "refused", refusal: "locked", message: taken }), GATED_EXIT_CODE);
|
|
898
|
+
}
|
|
612
899
|
|
|
613
900
|
// The record is written before the first component runs and again as each
|
|
614
901
|
// one settles, so a run killed partway still knows what applied.
|
|
@@ -642,6 +929,11 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
642
929
|
};
|
|
643
930
|
recordAttempt();
|
|
644
931
|
|
|
932
|
+
// #4288: the before-apply hooks, once the gate and the review let the apply through.
|
|
933
|
+
const toApply = (resumed?.plan ?? p.plan).order.filter((name) => !completedSoFar.has(name));
|
|
934
|
+
const beforeFailed = runStageHook(p, "before-apply", toApply);
|
|
935
|
+
if (beforeFailed) return stop(prReport({ ...base, approval, status: "failed", message: beforeFailed }), 1);
|
|
936
|
+
|
|
645
937
|
// #3999: a round planned after an upstream applied waits on a gate of its
|
|
646
938
|
// own, under the same identity.gates rule and the same review checks.
|
|
647
939
|
const roundLedger = gitGateLedgerPort();
|
|
@@ -697,11 +989,13 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
697
989
|
: {}),
|
|
698
990
|
});
|
|
699
991
|
recordAttempt();
|
|
992
|
+
// #4288: the after-apply hooks, for the components that applied.
|
|
993
|
+
const afterFailed = runStageHook(p, "after-apply", toApply.filter((name) => applied.run.completed.includes(name)), undefined, "the apply counts as failed though its components applied");
|
|
700
994
|
await recordAutoReleasesForRun(applied.run.results, p.env, release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
701
995
|
const report = prReport({
|
|
702
996
|
...base,
|
|
703
997
|
approval,
|
|
704
|
-
status: applied.status,
|
|
998
|
+
status: afterFailed ? "failed" : applied.status,
|
|
705
999
|
members: applied.members,
|
|
706
1000
|
...(resumed ? { resumed: resumed.completed } : {}),
|
|
707
1001
|
...(applied.rounds ? { rounds: applied.rounds } : {}),
|
|
@@ -710,13 +1004,123 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
710
1004
|
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
711
1005
|
// a person. A step that refused its plan because the target moved since
|
|
712
1006
|
// the approval exits 3, as a moved digest does before anything applies (#3997).
|
|
713
|
-
const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
1007
|
+
const exit = afterFailed ? 1 : applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
714
1008
|
if (green && applied.status === "applied") await recordGreenApplied(p, green, set.doc.digest, approval, via);
|
|
715
1009
|
const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
|
|
716
|
-
if (applied.status === "applied") console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
1010
|
+
if (applied.status === "applied" && !afterFailed) console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
717
1011
|
return code;
|
|
718
1012
|
}
|
|
719
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* Under `ci.locks` (#4293): the refusal when another open pull request holds
|
|
1016
|
+
* a lock on a component this apply reaches, else null. The targets are the
|
|
1017
|
+
* selected components, the names `chant lock` takes. A lock whose pull
|
|
1018
|
+
* request the forge says merged or closed reads as released; with no
|
|
1019
|
+
* `--forge` every lock counts. A lock file that cannot be read stops the
|
|
1020
|
+
* apply, since nothing then says the targets are free.
|
|
1021
|
+
*/
|
|
1022
|
+
async function heldLocksOf(p: Prepared, pr: number | null): Promise<string | null | number> {
|
|
1023
|
+
const { heldTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock");
|
|
1024
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
1025
|
+
let held;
|
|
1026
|
+
try {
|
|
1027
|
+
held = await heldTargetLocks(p.gate, p.plan.order, pr, { ...(isOpen ? { isOpen } : {}) });
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
return fail(`could not read the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (held.length === 0) return null;
|
|
1032
|
+
const name = (n: number) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
1033
|
+
const holders = [...new Set(held.map((h) => h.pr))].sort((a, b) => a - b);
|
|
1034
|
+
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"}.`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* `pr-apply --before-merge` (#4301, ws-106), before any of the head's code
|
|
1039
|
+
* loads: the declaration at base must set `ci.workflow: "apply-before-merge"`
|
|
1040
|
+
* for the member, and the forge must say the pull request is approved,
|
|
1041
|
+
* mergeable, undiverged and green at the commit checked out. Returns the
|
|
1042
|
+
* checks for the report, or the exit code once it wrote a refusal.
|
|
1043
|
+
*/
|
|
1044
|
+
async function beforeMergeChecks(ctx: CommandContext): Promise<PrBeforeMerge | number> {
|
|
1045
|
+
const { args } = ctx;
|
|
1046
|
+
if (args.green) return fail("--before-merge applies an open pull request's head and --green a green commit; pass one of them");
|
|
1047
|
+
if (args.waveGate) return fail("--before-merge applies the pull request's plan in one run, so it takes no --wave-gate");
|
|
1048
|
+
if (args.pr === undefined) return fail("--before-merge applies an open pull request, so it needs --pr <n>");
|
|
1049
|
+
if (!args.forge || !(FORGE_KINDS as readonly string[]).includes(args.forge)) return fail("--before-merge asks the forge whether the pull request is ready, so it needs --forge github|gitlab|forgejo");
|
|
1050
|
+
if (!args.base) return fail("pr-apply needs the commit the change is measured from: --base <ref>", "Before merge, the target branch's commit.");
|
|
1051
|
+
const cwd = resolve(".");
|
|
1052
|
+
let forge: PrForge;
|
|
1053
|
+
let head: string;
|
|
1054
|
+
let base: string;
|
|
1055
|
+
try {
|
|
1056
|
+
forge = forgeFromEnv(args.forge as ForgeKind);
|
|
1057
|
+
head = await resolveCommit(cwd, args.head ?? "HEAD");
|
|
1058
|
+
base = await resolveMergeBase(cwd, args.base, head);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
1061
|
+
}
|
|
1062
|
+
const member = args.members?.[0];
|
|
1063
|
+
const gate = args.gate ?? PR_APPLY_GATE;
|
|
1064
|
+
const { ciWorkflowAt } = await import("../../workspace/ci-workflow-at");
|
|
1065
|
+
const { appliesBeforeMerge } = await import("../../workspace/ci-workflow");
|
|
1066
|
+
const { checkPrRequires, unmetRequires } = await import("../../pr-requires");
|
|
1067
|
+
const workflow = ciWorkflowAt(cwd, args.base, member);
|
|
1068
|
+
const early = earlyStop(ctx, forge, { pr: args.pr, base, head, gate, ...(member ? { member } : {}) });
|
|
1069
|
+
if (!appliesBeforeMerge(workflow)) {
|
|
1070
|
+
return early(
|
|
1071
|
+
{ requires: [] },
|
|
1072
|
+
"before-merge-off",
|
|
1073
|
+
`The declaration at base sets ci.workflow to ${workflow ?? "nothing"}${member ? ` for ${member}` : ""}, and only apply-before-merge applies a pull request before it merges (ws-106). Nothing was applied.`,
|
|
1074
|
+
1,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
// The apply's own statuses never hold it back: chant ci run's, and the report's.
|
|
1078
|
+
const ignore = [`chant/${gate}`, `chant/${PR_APPLY_GATE}`, prStatusContext("apply", member)];
|
|
1079
|
+
const requires = await checkPrRequires(forge, args.pr, head, { ignore });
|
|
1080
|
+
for (const r of requires) console.error(formatInfo(`${r.name}: ${r.passed ? "passed" : r.pending ? "pending" : "unmet"}, ${r.reason}`));
|
|
1081
|
+
const unmet = unmetRequires(requires);
|
|
1082
|
+
if (unmet) {
|
|
1083
|
+
return early({ requires }, "requires-unmet", `${forge.kind === "gitlab" ? `Merge request !${args.pr}` : `Pull request ${args.pr}`} is not ready to apply before merge: ${unmet}. Nothing was applied.`, GATED_EXIT_CODE);
|
|
1084
|
+
}
|
|
1085
|
+
return { requires };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** The refusal report of an apply before merge stopped before it planned (#4301): nothing selected, nothing planned. */
|
|
1089
|
+
function earlyStop(ctx: CommandContext, forge: PrForge, at: { pr: number; base: string; head: string; gate: string; member?: string }) {
|
|
1090
|
+
return async (beforeMerge: PrBeforeMerge, refusal: "requires-unmet" | "before-merge-off", message: string, code: number): Promise<number> => {
|
|
1091
|
+
const plan = { order: [], waves: [], skipped: [], seeds: [], indeterminate: [] } as unknown as FanOutPlan;
|
|
1092
|
+
const signal = { changed: [], indeterminate: [], unclaimed: [] } as unknown as ComponentChangeSignal;
|
|
1093
|
+
const p = { env: ctx.args.env ?? "local", base: at.base, head: at.head, gate: at.gate, forge, hookOutcomes: [], beforeMerge, ...(at.member ? { member: at.member } : {}) } as unknown as Prepared;
|
|
1094
|
+
const set: PrPlanSet = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1095
|
+
const report = prReport({ stage: "apply", pr: at.pr, env: p.env, ...(at.member ? { member: at.member } : {}), base: at.base, head: at.head, op: prOp(at.pr, at.member), gate: at.gate, plan, signal, set, approval: { status: "pending" }, status: "refused", refusal, message });
|
|
1096
|
+
return stopper(ctx, p, undefined, undefined)(report, code);
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Take `apply` locks on the targets an apply before merge reaches (#4293,
|
|
1102
|
+
* #4301), held for pull request `pr` until it merges or closes. Returns the
|
|
1103
|
+
* refusal when another open pull request holds one, null once taken, or the
|
|
1104
|
+
* exit code when the locks could not be written.
|
|
1105
|
+
*/
|
|
1106
|
+
async function takeApplyLocks(p: Prepared, pr: number): Promise<string | null | number> {
|
|
1107
|
+
const { takeTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock");
|
|
1108
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
1109
|
+
const by = process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER ?? "unknown";
|
|
1110
|
+
let result;
|
|
1111
|
+
try {
|
|
1112
|
+
result = await takeTargetLocks(p.gate, p.plan.order, { pr, by, at: new Date().toISOString(), stage: "apply", head: p.head }, { ...(isOpen ? { isOpen } : {}) });
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
return fail(`could not take the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
1115
|
+
}
|
|
1116
|
+
if (result.ok) {
|
|
1117
|
+
if (result.taken.length > 0) console.error(formatInfo(`pull request ${pr} holds ${result.taken.join(", ")} on ${p.gate} until it merges or closes`));
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
const name = (n: number) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
1121
|
+
return `${describeHeldTargetLocks(result.held, name)}, so nothing was applied.`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
720
1124
|
/** #4274: a component that published nothing is recorded by the plan digest its approval bound. */
|
|
721
1125
|
function prAppliedDigests(p: Prepared, members: PrMember[]) {
|
|
722
1126
|
return appliedDigestOf({ components: p.components, env: p.env, projectRoot: resolve("."), members });
|
|
@@ -752,6 +1156,7 @@ async function prRecord(ctx: CommandContext, stage: StageEvents): Promise<number
|
|
|
752
1156
|
}
|
|
753
1157
|
}
|
|
754
1158
|
const op = pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
1159
|
+
placeHooks(ctx, p, "record", op);
|
|
755
1160
|
const upload = openUpload(stage, "record", pr, p);
|
|
756
1161
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-record ${op}`, cwd: resolve(".") }, upload?.line);
|
|
757
1162
|
stage.stream = events;
|
|
@@ -808,31 +1213,34 @@ async function prRecord(ctx: CommandContext, stage: StageEvents): Promise<number
|
|
|
808
1213
|
|
|
809
1214
|
const { mergeSourceOf, writeMergeRecord, MERGE_RECORD_SCHEMA_ID, MERGE_RECORD_CONTRACT } = await import("../../lifecycle/merge-record");
|
|
810
1215
|
const { pushLifecycleReport } = await import("../../lifecycle/git");
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (
|
|
1216
|
+
// #4292: a member another branch applies has no merge on this one, so no record says it planned nothing here.
|
|
1217
|
+
if (!p.routedAway) {
|
|
1218
|
+
try {
|
|
1219
|
+
const written = await writeMergeRecord({
|
|
1220
|
+
$schema: MERGE_RECORD_SCHEMA_ID,
|
|
1221
|
+
contract: MERGE_RECORD_CONTRACT,
|
|
1222
|
+
commit: p.head,
|
|
1223
|
+
base: p.base,
|
|
1224
|
+
env: p.env,
|
|
1225
|
+
...(p.member ? { member: p.member } : {}),
|
|
1226
|
+
source: mergeSourceOf(resolve("."), p.head, pr),
|
|
1227
|
+
op: pr === null ? null : op,
|
|
1228
|
+
gate: p.gate,
|
|
1229
|
+
digest: nothing ? null : set.doc.digest,
|
|
1230
|
+
plan: nothing ? "nothing" : set.failed ? "plan-failed" : approval.status === "approved" ? "approved" : approval.status === "changed" ? "changed" : "unapproved",
|
|
1231
|
+
...(approval.status === "changed" && approval.approved ? { approvedDigest: approval.approved } : {}),
|
|
1232
|
+
approvedBy: approval.status === "approved" ? (approval.approvedBy ?? []) : [],
|
|
1233
|
+
...(authorApproved && authorApproved.length > 0 ? { authorApproved } : {}),
|
|
1234
|
+
components: p.plan.order,
|
|
1235
|
+
recordedAt: new Date().toISOString(),
|
|
1236
|
+
});
|
|
1237
|
+
if (written.written) {
|
|
1238
|
+
const pushed = await pushLifecycleReport();
|
|
1239
|
+
if (pushed.pushWarning) console.error(formatWarning({ message: pushed.pushWarning }));
|
|
1240
|
+
}
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
833
1243
|
}
|
|
834
|
-
} catch (err) {
|
|
835
|
-
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
836
1244
|
}
|
|
837
1245
|
|
|
838
1246
|
const stop = stopper(ctx, p, events, upload);
|
|
@@ -874,11 +1282,15 @@ async function greenTargetOf(ctx: CommandContext): Promise<GreenTarget | number>
|
|
|
874
1282
|
*/
|
|
875
1283
|
async function decideApplyGate(
|
|
876
1284
|
p: Prepared,
|
|
877
|
-
at: { op: string; pr: number | null; set: PrPlanSet; events?: RunEventStream; green?: GreenTarget },
|
|
1285
|
+
at: { op: string; pr: number | null; set: PrPlanSet; events?: RunEventStream; green?: GreenTarget; held?: boolean },
|
|
878
1286
|
): Promise<{ approval: PrApproval; satisfied: true; via: "merges" | "gate" } | { approval: PrApproval; satisfied: false; refusal: "plan-changed" | "not-approved"; message: string }> {
|
|
879
1287
|
const { op, pr, set, events, green } = at;
|
|
880
1288
|
let uncovered: string[] = [];
|
|
881
|
-
|
|
1289
|
+
// #4294: a plan the policy holds needs an approval of its own digest, so the merges' approvals do not cover it.
|
|
1290
|
+
if (green && at.held) {
|
|
1291
|
+
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`));
|
|
1292
|
+
uncovered = ["the policy holds this plan for an approval of its own digest"];
|
|
1293
|
+
} else if (green) {
|
|
882
1294
|
const cover = coverByMerges(set.doc, green.approved);
|
|
883
1295
|
if (cover.covered) {
|
|
884
1296
|
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`));
|
|
@@ -1277,8 +1689,23 @@ async function prApplyWaves(
|
|
|
1277
1689
|
const completed = new Set(carry?.completed ?? []);
|
|
1278
1690
|
const outputsSoFar: Record<string, Record<string, unknown>> = { ...(carry?.outputs ?? {}) };
|
|
1279
1691
|
const inputsSoFar: Record<string, string> = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]!]));
|
|
1280
|
-
|
|
1692
|
+
// #4291: what failed in an earlier wave's job stays failed until it applies, so this job blocks its dependents and runs the rest.
|
|
1693
|
+
const failedSoFar = new Set<string>((carry ? attempt?.failed ?? [] : []).filter((name) => !completed.has(name)));
|
|
1281
1694
|
let waves: PrWave[] = carry?.record.waves ?? [];
|
|
1695
|
+
const upstreams = new Map(p.components.map((c) => [c.name, c.dependsOn ?? []]));
|
|
1696
|
+
/** Each component's outcome so far, past applied (#4291): failed, waiting, not-applied, or blocked by a failure. */
|
|
1697
|
+
const outcomes = (): Map<string, WaveMemberOutcome> => {
|
|
1698
|
+
const out = new Map<string, WaveMemberOutcome>();
|
|
1699
|
+
for (const name of completed) out.set(name, { status: "applied" });
|
|
1700
|
+
for (const name of failedSoFar) out.set(name, { status: "failed" });
|
|
1701
|
+
for (const w of waves) {
|
|
1702
|
+
if (w.status !== "waiting" && w.status !== "plan-failed" && w.status !== "failed") continue;
|
|
1703
|
+
for (const name of w.components) if (!out.has(name)) out.set(name, { status: w.status === "waiting" ? "waiting" : "not-applied" });
|
|
1704
|
+
}
|
|
1705
|
+
return out;
|
|
1706
|
+
};
|
|
1707
|
+
/** Components a failure blocked (#4291), each with the component that failed. */
|
|
1708
|
+
const blockedSoFar = new Map<string, string>();
|
|
1282
1709
|
const after = new Map<string, PrMember>();
|
|
1283
1710
|
const leftIn = (k: number): string[] => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
|
|
1284
1711
|
const firstLeft = (): number => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
|
|
@@ -1293,15 +1720,32 @@ async function prApplyWaves(
|
|
|
1293
1720
|
const set = approved ?? { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1294
1721
|
const members = set.members.map((m): PrMember => {
|
|
1295
1722
|
const now = after.get(m.member);
|
|
1296
|
-
if (now) return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
|
|
1297
|
-
|
|
1723
|
+
if (now) return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}), ...(now.blockedBy ? { blockedBy: now.blockedBy } : {}) };
|
|
1724
|
+
if (completed.has(m.component)) return { ...m, status: "applied" };
|
|
1725
|
+
const by = blockedSoFar.get(m.component);
|
|
1726
|
+
if (by !== undefined) return { ...m, status: "blocked", blockedBy: by };
|
|
1727
|
+
return failedSoFar.has(m.component) ? { ...m, status: "failed" } : m;
|
|
1298
1728
|
});
|
|
1299
1729
|
return prReport({ ...reportBase, set, approval, members, waves, ...input });
|
|
1300
1730
|
};
|
|
1301
1731
|
|
|
1302
1732
|
// `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
|
|
1733
|
+
// #4291: an earlier member that failed, or was blocked by one that failed, counts as settled; it blocks its dependents here.
|
|
1303
1734
|
const left = firstLeft();
|
|
1304
|
-
|
|
1735
|
+
const earlierWaiting = only !== undefined ? waves.find((w) => w.wave < only && w.status === "waiting" && w.components.some((name) => !completed.has(name))) : undefined;
|
|
1736
|
+
if (earlierWaiting) {
|
|
1737
|
+
return stop(
|
|
1738
|
+
report({
|
|
1739
|
+
status: "refused",
|
|
1740
|
+
refusal: "earlier-wave",
|
|
1741
|
+
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.`,
|
|
1742
|
+
}),
|
|
1743
|
+
GATED_EXIT_CODE,
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
const settled = only !== undefined && left > 0 && left < only ? settleEarlierWaves(p.plan.waves, only, upstreams, outcomes()) : undefined;
|
|
1747
|
+
if (settled) for (const [name, o] of settled.outcomes) if (o.status === "blocked" && o.by) blockedSoFar.set(name, o.by);
|
|
1748
|
+
if (only !== undefined && left > 0 && left < only && (settled?.unsettled.length ?? 0) > 0) {
|
|
1305
1749
|
return stop(
|
|
1306
1750
|
report({
|
|
1307
1751
|
status: "refused",
|
|
@@ -1315,6 +1759,7 @@ async function prApplyWaves(
|
|
|
1315
1759
|
}
|
|
1316
1760
|
|
|
1317
1761
|
let planned: PrPlanSet | undefined;
|
|
1762
|
+
let judged: PolicyJudged = { held: false };
|
|
1318
1763
|
if (carry) {
|
|
1319
1764
|
// What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
|
|
1320
1765
|
p.plan.waves.forEach((names, i) => {
|
|
@@ -1332,7 +1777,10 @@ async function prApplyWaves(
|
|
|
1332
1777
|
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);
|
|
1333
1778
|
}
|
|
1334
1779
|
await storeChangeSetForGate(ctx, p, set.doc);
|
|
1335
|
-
|
|
1780
|
+
// #4294: the policy at base judges the whole change set, before the pull request's gate.
|
|
1781
|
+
judged = await judgePolicy(p, { stage: "apply", op, pr, set });
|
|
1782
|
+
if (judged.stop) return stop(report({ status: "refused", refusal: judged.stop.refusal, message: judged.stop.message }), judged.stop.code);
|
|
1783
|
+
const decided = await decideApplyGate(p, { op, pr, set, events, held: judged.held, ...(green ? { green } : {}) });
|
|
1336
1784
|
approval = decided.approval;
|
|
1337
1785
|
if (!decided.satisfied) return stop(report({ status: "refused", refusal: decided.refusal, message: decided.message }), GATED_EXIT_CODE);
|
|
1338
1786
|
via = decided.via;
|
|
@@ -1347,6 +1795,8 @@ async function prApplyWaves(
|
|
|
1347
1795
|
emitReviewStop(events, prReview.message);
|
|
1348
1796
|
return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
|
|
1349
1797
|
}
|
|
1798
|
+
const unspent = await spendPolicy(op, judged, p.head);
|
|
1799
|
+
if (unspent) return stop(report({ status: "refused", refusal: "policy-denied", message: unspent }), 1);
|
|
1350
1800
|
|
|
1351
1801
|
const recordAttempt = (): void => {
|
|
1352
1802
|
if (!resumePath) return;
|
|
@@ -1377,9 +1827,27 @@ async function prApplyWaves(
|
|
|
1377
1827
|
|
|
1378
1828
|
const from = only ?? (left || total + 1);
|
|
1379
1829
|
const to = only ?? total;
|
|
1830
|
+
/** The last apply in this job that failed (#4291): its results give the exit code once every wave ran. */
|
|
1831
|
+
let lastFailure: { results: Parameters<typeof failedRunExitCode>[0]; gated: boolean } | undefined;
|
|
1832
|
+
let planFailedIn: number | undefined;
|
|
1380
1833
|
for (let k = from; k <= to; k++) {
|
|
1381
|
-
|
|
1382
|
-
|
|
1834
|
+
// #4291: a component of this wave whose upstream failed, or was blocked by a failure, is blocked; the rest of the wave runs.
|
|
1835
|
+
const before = new Set(p.plan.waves.slice(0, k - 1).flat());
|
|
1836
|
+
const mine = leftIn(k);
|
|
1837
|
+
for (const name of mine) {
|
|
1838
|
+
failedSoFar.delete(name);
|
|
1839
|
+
blockedSoFar.delete(name);
|
|
1840
|
+
}
|
|
1841
|
+
const outcomesNow = outcomes();
|
|
1842
|
+
for (const [name, by] of blockedSoFar) outcomesNow.set(name, { status: "blocked", by });
|
|
1843
|
+
const blockedNow = blockedMembers(mine, upstreams, before, outcomesNow);
|
|
1844
|
+
for (const [name, by] of blockedNow) blockedSoFar.set(name, by);
|
|
1845
|
+
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." }));
|
|
1846
|
+
const wavePlan = prWavePlan(p.plan, p.components, k, [...completed, ...blockedNow.keys()]);
|
|
1847
|
+
if (wavePlan.order.length === 0) {
|
|
1848
|
+
if (blockedNow.size > 0) recordAttempt();
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1383
1851
|
const inThisWave = new Set(wavePlan.order);
|
|
1384
1852
|
|
|
1385
1853
|
// Wave 1 of the apply that just planned every member applies those plans:
|
|
@@ -1396,18 +1864,26 @@ async function prApplyWaves(
|
|
|
1396
1864
|
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
|
|
1397
1865
|
} else {
|
|
1398
1866
|
const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
|
|
1399
|
-
const fresh = await
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1867
|
+
const fresh = await hookedPlan(p, wavePlan.order, () =>
|
|
1868
|
+
planPrSet({
|
|
1869
|
+
components: p.components,
|
|
1870
|
+
plan: wavePlan,
|
|
1871
|
+
registry: p.registry,
|
|
1872
|
+
env: p.env,
|
|
1873
|
+
seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
|
|
1874
|
+
...(events ? { onRunEvent: (body: RunEventBody) => events.emit(body) } : {}),
|
|
1875
|
+
}),
|
|
1876
|
+
);
|
|
1407
1877
|
if (fresh.failed) {
|
|
1408
|
-
for (const m of fresh.members)
|
|
1878
|
+
for (const m of fresh.members) {
|
|
1879
|
+
after.set(m.member, m);
|
|
1880
|
+
if (m.status === "plan-failed") failedSoFar.add(m.component);
|
|
1881
|
+
}
|
|
1409
1882
|
setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
|
|
1410
|
-
|
|
1883
|
+
// #4291: the later waves still run what does not depend on this wave.
|
|
1884
|
+
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 + "." }));
|
|
1885
|
+
planFailedIn ??= k;
|
|
1886
|
+
continue;
|
|
1411
1887
|
}
|
|
1412
1888
|
const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
|
|
1413
1889
|
set = fresh;
|
|
@@ -1462,6 +1938,12 @@ async function prApplyWaves(
|
|
|
1462
1938
|
}
|
|
1463
1939
|
}
|
|
1464
1940
|
|
|
1941
|
+
// #4288: the wave's apply-side hooks, around its apply.
|
|
1942
|
+
const waveBefore = runStageHook(p, "before-apply", wavePlan.order, undefined, `nothing in wave ${k} was applied`);
|
|
1943
|
+
if (waveBefore) {
|
|
1944
|
+
setWave({ ...entry, status: "failed" });
|
|
1945
|
+
return stop(report({ status: "failed", message: waveBefore }), 1);
|
|
1946
|
+
}
|
|
1465
1947
|
const applied = await applyPrSet({
|
|
1466
1948
|
components: p.components,
|
|
1467
1949
|
plan: wavePlan,
|
|
@@ -1477,6 +1959,7 @@ async function prApplyWaves(
|
|
|
1477
1959
|
onComponentSettled: (settled, outputs) => {
|
|
1478
1960
|
if (settled.status === "ok") {
|
|
1479
1961
|
completed.add(settled.component);
|
|
1962
|
+
failedSoFar.delete(settled.component);
|
|
1480
1963
|
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
1481
1964
|
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
1482
1965
|
} else if (settled.status === "fail") {
|
|
@@ -1491,14 +1974,33 @@ async function prApplyWaves(
|
|
|
1491
1974
|
const outputs = applied.run.componentOutputs[name];
|
|
1492
1975
|
if (outputs) outputsSoFar[name] = outputs;
|
|
1493
1976
|
}
|
|
1494
|
-
|
|
1977
|
+
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`);
|
|
1978
|
+
setWave({ ...entry, status: applied.status === "applied" && !waveAfter ? "applied" : "failed" });
|
|
1495
1979
|
await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin, undefined, undefined, prAppliedDigests(p, applied.members));
|
|
1496
1980
|
if (applied.status !== "applied") {
|
|
1497
|
-
|
|
1981
|
+
// #4291: a failed member blocks only its dependents, so the later waves still run.
|
|
1982
|
+
lastFailure = { results: applied.run.results, gated: applied.run.status === "gated" };
|
|
1983
|
+
recordAttempt();
|
|
1984
|
+
continue;
|
|
1498
1985
|
}
|
|
1986
|
+
if (waveAfter) return stop(report({ status: "failed", message: waveAfter }), 1);
|
|
1499
1987
|
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(", ")}`));
|
|
1500
1988
|
}
|
|
1501
1989
|
|
|
1990
|
+
// #4291: a failure, or a member it blocked, fails the job once every other member applied.
|
|
1991
|
+
const blockedHere = [...blockedSoFar.keys()].filter((name) => (only === undefined ? true : (p.plan.waves[only - 1] ?? []).includes(name)));
|
|
1992
|
+
if (lastFailure || planFailedIn !== undefined || blockedHere.length > 0) {
|
|
1993
|
+
const blockedLine = blockedSoFar.size > 0 ? ` Blocked: ${blockedWords(blockedSoFar)}.` : "";
|
|
1994
|
+
const message = planFailedIn !== undefined && !lastFailure
|
|
1995
|
+
? `A member of wave ${planFailedIn} failed to plan, so nothing in wave ${planFailedIn} was applied.${blockedLine}`
|
|
1996
|
+
: blockedLine ? blockedLine.trim() : undefined;
|
|
1997
|
+
return stop(
|
|
1998
|
+
report({ status: "failed", ...(message ? { message } : {}) }),
|
|
1999
|
+
lastFailure ? failedRunExitCode(lastFailure.results) : 1,
|
|
2000
|
+
lastFailure?.gated ? "gated" : undefined,
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1502
2004
|
const done = p.plan.order.every((name) => completed.has(name));
|
|
1503
2005
|
if (green && done) await recordGreenApplied(p, green, approvedSet.doc.digest, approval, via);
|
|
1504
2006
|
return stop(report({ status: done ? "applied" : "wave-applied" }), 0);
|
|
@@ -1513,3 +2015,45 @@ function writeWaveChangeSet(ctx: CommandContext, wave: number, doc: PrPlanSet["d
|
|
|
1513
2015
|
writeFileSync(join(dir, name), body);
|
|
1514
2016
|
upload?.file(name, body);
|
|
1515
2017
|
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* chant components pr-post --pr <n> --head <sha> --env <env> --forge <kind> [--member <name>] [--output <dir>] [--result <result>] (#4295)
|
|
2021
|
+
*
|
|
2022
|
+
* Post the plan note and the `chant/plan` status from the report a plan job
|
|
2023
|
+
* wrote with `pr-plan --no-status`, in a job that runs none of the change's
|
|
2024
|
+
* code. The pull request, the head, the environment and the member come from
|
|
2025
|
+
* the flags, and a report that names others is refused (`../../pr-post.ts`).
|
|
2026
|
+
* It exits 0 once it has posted, whatever the plan said, so the plan job's
|
|
2027
|
+
* own result stays the one that fails the run.
|
|
2028
|
+
*/
|
|
2029
|
+
export async function runComponentsPrPost(ctx: CommandContext): Promise<number> {
|
|
2030
|
+
const { args } = ctx;
|
|
2031
|
+
if (args.pr === undefined) return fail("pr-post needs the pull or merge request: --pr <n>");
|
|
2032
|
+
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>");
|
|
2033
|
+
if (!args.env) return fail("pr-post needs the environment the plan was for: --env <env>");
|
|
2034
|
+
if (!args.forge || !(FORGE_KINDS as readonly string[]).includes(args.forge)) return fail(`pr-post posts to a forge: --forge ${FORGE_KINDS.join("|")}`);
|
|
2035
|
+
if (args.members && args.members.length > 1) return fail(`pr-post takes one --member, not ${args.members.join(", ")}`);
|
|
2036
|
+
let forge: PrForge;
|
|
2037
|
+
try {
|
|
2038
|
+
forge = forgeFromEnv(args.forge as ForgeKind);
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
2041
|
+
}
|
|
2042
|
+
const { postPlanReport } = await import("../../pr-post");
|
|
2043
|
+
const member = args.members?.[0];
|
|
2044
|
+
const posted = await postPlanReport({
|
|
2045
|
+
forge,
|
|
2046
|
+
dir: resolve(args.output ?? PR_REPORT_DIR),
|
|
2047
|
+
pr: args.pr,
|
|
2048
|
+
head: args.head,
|
|
2049
|
+
env: args.env,
|
|
2050
|
+
...(member ? { member } : {}),
|
|
2051
|
+
...(args.planResult ? { result: args.planResult } : {}),
|
|
2052
|
+
// #4289: the plan job could not ask the forge, so the note job does, from the Ops at the commit it checked out.
|
|
2053
|
+
overdue: () => overdueOps(forge, member),
|
|
2054
|
+
});
|
|
2055
|
+
for (const message of posted.warnings) console.error(formatWarning({ message }));
|
|
2056
|
+
if (posted.refused) console.error(formatWarning({ message: `the plan report was not used: ${posted.refused}` }));
|
|
2057
|
+
console.error(formatInfo(`pr-post: ${prStatusContext("plan", member)} ${posted.state}: ${posted.description}${posted.noted ? "; note posted" : ""}`));
|
|
2058
|
+
return 0;
|
|
2059
|
+
}
|