@intentius/chant 0.124.41 → 0.124.43
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/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +3 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +27 -3
- package/dist/cli/handlers/pr.d.ts +3 -1
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +113 -2
- 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 +8 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli/registry.d.ts +19 -0
- 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 +9 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +17 -1
- 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-pipeline.d.ts +20 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +54 -16
- 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 +3 -0
- 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/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.js +1 -1
- package/dist/op/index.d.ts +2 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/op-waves-run.d.ts +3 -79
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -133
- package/dist/op/stage-hooks.d.ts +152 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +199 -0
- package/dist/pr-forge.d.ts +56 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +320 -45
- package/dist/pr-loop.d.ts +18 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +7 -0
- package/dist/pr-requires.d.ts +47 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +53 -0
- package/dist/wave-runtime.mjs +213 -213
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- package/dist/workspace/ci-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +2 -1
- 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 +6 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +2 -2
- package/dist/workspace/pr-report.schema.json +23 -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/package.json +1 -1
- package/src/cli/handlers/build.ts +3 -0
- package/src/cli/handlers/fan-out.ts +27 -3
- package/src/cli/handlers/pr-before-merge.test.ts +276 -0
- package/src/cli/handlers/pr.ts +110 -2
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +8 -2
- package/src/cli/parse-args.ts +11 -0
- package/src/cli/registry.ts +19 -0
- 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 +19 -1
- package/src/components/gate-plan.ts +3 -0
- package/src/components/pr-pipeline.ts +64 -16
- package/src/components/pr-run.ts +9 -0
- package/src/components/wave-plan.ts +38 -3
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- package/src/op/activities/review-agent.ts +1 -1
- package/src/op/index.ts +4 -0
- package/src/op/op-waves-run.ts +24 -228
- package/src/op/stage-hooks-async.test.ts +79 -0
- package/src/op/stage-hooks.ts +346 -0
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.ts +334 -36
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.ts +25 -1
- package/src/pr-requires.test.ts +248 -0
- package/src/pr-requires.ts +93 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -0
- package/src/workspace/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- package/src/workspace/ci-init.ts +5 -4
- package/src/workspace/ci-run.ts +2 -1
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +2 -2
- package/src/workspace/declaration.ts +6 -3
- package/src/workspace/pr-report.schema.json +23 -3
- package/src/workspace/reason-codes.ts +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant components pr-apply --before-merge` (#4301, ws-106): the workflow
|
|
3
|
+
* read at base and the four requires checks, asked of a fake GitHub before
|
|
4
|
+
* any of the head's code loads, then the pull request's own gate, and the
|
|
5
|
+
* apply locks under ci.locks. Git, the change signal, the derivation and the
|
|
6
|
+
* gate ledger are fakes, as in pr.test.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
14
|
+
import type { DriverComponent } from "../../components/driver";
|
|
15
|
+
import type { DeployContext } from "../../components/capability";
|
|
16
|
+
import type { ParsedArgs } from "../registry";
|
|
17
|
+
|
|
18
|
+
const HEAD = "a".repeat(40);
|
|
19
|
+
const BASE = "b".repeat(40);
|
|
20
|
+
|
|
21
|
+
/** The security groups pr-apply reads from the declaration (#4168); none unless a test sets them. */
|
|
22
|
+
let groups: string[] = [];
|
|
23
|
+
vi.mock("../../workspace/security", async (original) => ({
|
|
24
|
+
...(await original<typeof import("../../workspace/security")>()),
|
|
25
|
+
securityGroupsAt: () => new Set(groups),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/** The ci.hooks pr-plan and pr-apply read at base (#4288); none unless a test sets them. */
|
|
29
|
+
let hooksAtBase: Record<string, unknown> = {};
|
|
30
|
+
vi.mock("../../components/pr-hooks", async (original) => ({
|
|
31
|
+
...(await original<typeof import("../../components/pr-hooks")>()),
|
|
32
|
+
prLoopHooksAt: (_cwd: string, base: string) => ({ hooks: hooksAtBase, base }),
|
|
33
|
+
}));
|
|
34
|
+
/** `ci.locks` pr-apply reads at base (#4293), and the locks other pull requests hold; none unless a test sets them. */
|
|
35
|
+
let locksMode: "apply" | "plan" | null = null;
|
|
36
|
+
let heldLocks: (gate: string, targets: readonly string[], pr: number | null) => Promise<unknown[]> = async () => [];
|
|
37
|
+
vi.mock("../../workspace/ci-locks", () => ({ ciLocksAt: () => locksMode }));
|
|
38
|
+
let taken: unknown[] = [];
|
|
39
|
+
vi.mock("../../lifecycle/target-lock", async (original) => ({
|
|
40
|
+
...(await original<typeof import("../../lifecycle/target-lock")>()),
|
|
41
|
+
heldTargetLocks: (gate: string, targets: readonly string[], pr: number | null) => heldLocks(gate, targets, pr),
|
|
42
|
+
takeTargetLocks: async (gate: string, targets: readonly string[], holder: unknown) => {
|
|
43
|
+
taken.push({ gate, targets, holder });
|
|
44
|
+
return { ok: true, taken: [...targets].sort(), released: [], expired: [], pushed: false };
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
/** The ci.workflow pr-apply --before-merge reads at base (#4301). */
|
|
48
|
+
let workflowAtBase: string | null = "apply-before-merge";
|
|
49
|
+
vi.mock("../../workspace/ci-workflow-at", () => ({ ciWorkflowAt: () => workflowAtBase }));
|
|
50
|
+
/** Whether the head's code was loaded: the config read and the derivation. */
|
|
51
|
+
let loaded: string[] = [];
|
|
52
|
+
|
|
53
|
+
/** What ci.policy at base says (#4294): no policy unless a test sets a verdict. The engine itself is gate-policy.test.ts's. */
|
|
54
|
+
let policyVerdict: import("../../gate-policy").GatePolicyVerdict | undefined;
|
|
55
|
+
vi.mock("../../components/pr-policy", async (original) => ({
|
|
56
|
+
...(await original<typeof import("../../components/pr-policy")>()),
|
|
57
|
+
prLoopPolicyAt: (_cwd: string, base: string) => ({ base }),
|
|
58
|
+
runPrPolicy: () => policyVerdict,
|
|
59
|
+
}));
|
|
60
|
+
vi.mock("../../override-gate", async (original) => ({
|
|
61
|
+
...(await original<typeof import("../../override-gate")>()),
|
|
62
|
+
overridersAt: (_repo: string, base: string) => ({ base, path: ".chant/overriders", active: true, principals: ["github:carol"] }),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
vi.mock("../../components/changed-files", () => ({
|
|
66
|
+
resolveCommit: async () => HEAD,
|
|
67
|
+
resolveMergeBase: async () => BASE,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
vi.mock("../../config", async () => {
|
|
71
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
72
|
+
return { ...actual, loadChantConfig: async () => { loaded.push("config"); return { config: {} }; } };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vi.mock("./fan-out", () => ({
|
|
76
|
+
resolveChangedUnits: async () => ({ units: { changed: ["net"], indeterminate: [] } }),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// #4022: pr-apply stores the change set its gate binds on chant/lifecycle and
|
|
80
|
+
// pushes it. These tests run in the chant checkout, so the store is replaced:
|
|
81
|
+
// it must never write or push the real repository's lifecycle branch.
|
|
82
|
+
vi.mock("../../lifecycle/gate-change-set", () => ({
|
|
83
|
+
storeGateChangeSet: async () => ({ written: false }),
|
|
84
|
+
readGraphIds: async () => ({ ids: [] }),
|
|
85
|
+
setGraphIdReader: () => {},
|
|
86
|
+
gateGraphReader: () => async () => ({ ids: [] }),
|
|
87
|
+
}));
|
|
88
|
+
vi.mock("../../lifecycle/git", async () => {
|
|
89
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/git")>("../../lifecycle/git");
|
|
90
|
+
return { ...actual, pushLifecycleReport: async () => { throw new Error("pr.test must not push chant/lifecycle"); } };
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
vi.mock("./run", () => ({
|
|
94
|
+
GATED_EXIT_CODE: 3,
|
|
95
|
+
autoReleaseRun: (id: string) => ({ runId: id, runOrigin: { forge: "local" } }),
|
|
96
|
+
recordAutoReleasesForRun: async () => [],
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
let gateLedger: ReturnType<typeof import("../../op/gate").memoryGateLedgerPort>;
|
|
100
|
+
vi.mock("../../op/gate", async () => {
|
|
101
|
+
const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
|
|
102
|
+
return { ...actual, gitGateLedgerPort: () => gateLedger, gateIsSealed: async () => false };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const failing = new Set<string>();
|
|
106
|
+
/** The error a root's apply throws, in place of applying (#3997). */
|
|
107
|
+
const refusal = new Map<string, Error>();
|
|
108
|
+
/** Called as a component's apply starts, before it can fail. */
|
|
109
|
+
let onRun: (root: string) => void = () => {};
|
|
110
|
+
vi.mock("../../components/fan-out-support", async () => {
|
|
111
|
+
const { CapabilityRegistry } = await import("../../components/capability");
|
|
112
|
+
const { planFanOut } = await import("../../components/fan-out");
|
|
113
|
+
const { computePlanDigest } = await import("../../lifecycle/plan-digest");
|
|
114
|
+
const registry = new CapabilityRegistry();
|
|
115
|
+
registry.register({
|
|
116
|
+
kind: "fake-root",
|
|
117
|
+
async plan(_ctx: DeployContext, input: { root: string }) {
|
|
118
|
+
const planDigest = computePlanDigest("fake-root", { root: input.root });
|
|
119
|
+
return {
|
|
120
|
+
member: input.root,
|
|
121
|
+
planDigest,
|
|
122
|
+
artifact: { planDigest },
|
|
123
|
+
changeSet: { member: { member: input.root, lexicon: "terraform", planner: "tofu", status: "planned", planDigest, holes: [] }, entries: [] },
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
async outputs() {
|
|
127
|
+
return {};
|
|
128
|
+
},
|
|
129
|
+
async run(_ctx: DeployContext, input: { root: string }) {
|
|
130
|
+
onRun(input.root);
|
|
131
|
+
if (refusal.has(input.root)) throw refusal.get(input.root);
|
|
132
|
+
if (failing.has(input.root)) throw new Error(`${input.root} failed`);
|
|
133
|
+
return { outputs: {} };
|
|
134
|
+
},
|
|
135
|
+
} as never);
|
|
136
|
+
return {
|
|
137
|
+
fanOutRegistry: async () => registry,
|
|
138
|
+
deriveFanOut: async () => (loaded.push("derive"), {
|
|
139
|
+
success: true,
|
|
140
|
+
components: ESTATE,
|
|
141
|
+
plan: planFanOut({ components: ESTATE, changed: ["net"] }),
|
|
142
|
+
signal: { changed: ["net"], indeterminate: [], unclaimed: [] },
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const root = (name: string, dependsOn: string[] = []): DriverComponent => ({
|
|
148
|
+
name,
|
|
149
|
+
dependsOn,
|
|
150
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "fake-root", root: name }] }],
|
|
151
|
+
});
|
|
152
|
+
const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"])];
|
|
153
|
+
|
|
154
|
+
const { memoryGateLedgerPort } = await import("../../op/gate");
|
|
155
|
+
const { runComponentsPrApply, runComponentsPrPlan } = await import("./pr");
|
|
156
|
+
|
|
157
|
+
let dir: string;
|
|
158
|
+
let out: string[];
|
|
159
|
+
let errors: string[];
|
|
160
|
+
|
|
161
|
+
const ctx = (a: Partial<ParsedArgs>) =>
|
|
162
|
+
({ args: { command: "components", path: "pr-apply", format: "", fix: false, watch: false, verbose: false, help: false, live: false, base: "main", env: "prod", output: dir, pr: 12, forge: "github", beforeMerge: true, ...a } as ParsedArgs, plugins: [] }) as never;
|
|
163
|
+
|
|
164
|
+
beforeEach(async () => {
|
|
165
|
+
dir = await mkdtemp(join(tmpdir(), "pr-before-merge-"));
|
|
166
|
+
taken = [];
|
|
167
|
+
workflowAtBase = "apply-before-merge";
|
|
168
|
+
loaded = [];
|
|
169
|
+
out = [];
|
|
170
|
+
errors = [];
|
|
171
|
+
failing.clear();
|
|
172
|
+
refusal.clear();
|
|
173
|
+
onRun = () => {};
|
|
174
|
+
groups = [];
|
|
175
|
+
hooksAtBase = {};
|
|
176
|
+
locksMode = null;
|
|
177
|
+
heldLocks = async () => [];
|
|
178
|
+
policyVerdict = undefined;
|
|
179
|
+
gateLedger = memoryGateLedgerPort();
|
|
180
|
+
vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
|
|
181
|
+
vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.map(String).join(" ")); });
|
|
182
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
afterEach(async () => {
|
|
186
|
+
vi.restoreAllMocks();
|
|
187
|
+
vi.unstubAllGlobals();
|
|
188
|
+
vi.unstubAllEnvs();
|
|
189
|
+
await rm(dir, { recursive: true, force: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/** The digest pr-plan printed, read back from its report. */
|
|
193
|
+
async function plannedDigest(): Promise<string> {
|
|
194
|
+
const { readFile } = await import("node:fs/promises");
|
|
195
|
+
return JSON.parse(await readFile(join(dir, "pr-plan.json"), "utf8")).digest as string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Pull request 12 on a fake GitHub, its head HEAD unless `over` says otherwise. */
|
|
199
|
+
function serveGithub(over: { head?: string; reviews?: unknown[]; mergeable?: boolean | null; behind?: number; statuses?: unknown[] } = {}): void {
|
|
200
|
+
const ok = (value: unknown) => ({ ok: true, status: 200, text: async () => JSON.stringify(value) });
|
|
201
|
+
vi.stubEnv("GITHUB_REPOSITORY", "acme/infra");
|
|
202
|
+
vi.stubEnv("GITHUB_TOKEN", "t0k");
|
|
203
|
+
vi.stubEnv("GITHUB_API_URL", "https://api.github.test");
|
|
204
|
+
vi.stubEnv("GITHUB_SERVER_URL", "https://github.com");
|
|
205
|
+
vi.stubGlobal("fetch", async (url: string, init: { method: string }) => {
|
|
206
|
+
const { pathname } = new URL(url);
|
|
207
|
+
if (init.method !== "GET") return ok({ id: 1 });
|
|
208
|
+
const pull = "/repos/acme/infra/pulls/12";
|
|
209
|
+
if (pathname === pull) return ok({ number: 12, state: "open", user: { login: "carol" }, head: { ref: "f", sha: over.head ?? HEAD }, base: { ref: "main" }, mergeable: over.mergeable === undefined ? true : over.mergeable, mergeable_state: "clean" });
|
|
210
|
+
if (pathname === `${pull}/reviews`) return ok(over.reviews ?? [{ user: { login: "bob" }, state: "APPROVED", commit_id: HEAD }]);
|
|
211
|
+
if (pathname === `${pull}/commits`) return ok([{ author: { login: "carol" }, committer: { login: "carol" } }]);
|
|
212
|
+
if (pathname === `/repos/acme/infra/compare/main...${HEAD}`) return ok({ behind_by: over.behind ?? 0 });
|
|
213
|
+
if (pathname === `/repos/acme/infra/commits/${HEAD}/status`) return ok({ statuses: over.statuses ?? [{ context: "chant/plan", state: "success" }, { context: "chant/apply", state: "failure" }] });
|
|
214
|
+
if (pathname === `/repos/acme/infra/commits/${HEAD}/check-runs`) return ok({ check_runs: [] });
|
|
215
|
+
return ok([]);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const applyReport = async () => {
|
|
220
|
+
const { readFile } = await import("node:fs/promises");
|
|
221
|
+
return JSON.parse(await readFile(join(dir, "pr-apply.json"), "utf8")) as Record<string, unknown> & { beforeMerge?: { requires: Array<{ name: string; passed: boolean; reason: string }> } };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
describe("pr-apply --before-merge (#4301)", () => {
|
|
225
|
+
test("without apply-before-merge at base it refuses before-merge-off, and none of the head's code loads", async () => {
|
|
226
|
+
workflowAtBase = "trunk-curated";
|
|
227
|
+
serveGithub();
|
|
228
|
+
expect(await runComponentsPrApply(ctx({}))).toBe(1);
|
|
229
|
+
const report = await applyReport();
|
|
230
|
+
expect(report).toMatchObject({ stage: "apply", pr: 12, status: "refused", refusal: "before-merge-off", beforeMerge: { requires: [] } });
|
|
231
|
+
expect(report.message).toContain("sets ci.workflow to trunk-curated");
|
|
232
|
+
expect(loaded).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("an unmet require refuses requires-unmet with each check in the report, and none of the head's code loads", async () => {
|
|
236
|
+
serveGithub({ behind: 2, reviews: [{ user: { login: "carol" }, state: "APPROVED", commit_id: HEAD }] });
|
|
237
|
+
expect(await runComponentsPrApply(ctx({}))).toBe(3);
|
|
238
|
+
const report = await applyReport();
|
|
239
|
+
expect(report).toMatchObject({ status: "refused", refusal: "requires-unmet" });
|
|
240
|
+
expect(report.beforeMerge!.requires.map((r) => [r.name, r.passed])).toEqual([["approved", false], ["mergeable", true], ["undiverged", false], ["checks", true]]);
|
|
241
|
+
expect(report.message).toContain("undiverged: aaaaaaaa is 2 commits behind main");
|
|
242
|
+
expect(loaded).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("it needs --pr and --forge, and takes no --green", async () => {
|
|
246
|
+
expect(await runComponentsPrApply(ctx({ pr: undefined }))).toBe(1);
|
|
247
|
+
expect(await runComponentsPrApply(ctx({ forge: undefined }))).toBe(1);
|
|
248
|
+
expect(await runComponentsPrApply(ctx({ green: true }))).toBe(1);
|
|
249
|
+
expect(loaded).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("all four passed: the pull request's own gate decides, and an unapproved plan applies nothing", async () => {
|
|
253
|
+
serveGithub();
|
|
254
|
+
expect(await runComponentsPrApply(ctx({}))).toBe(3);
|
|
255
|
+
const report = await applyReport();
|
|
256
|
+
expect(report).toMatchObject({ status: "refused", refusal: "not-approved", op: "pr-12" });
|
|
257
|
+
expect(report.beforeMerge!.requires.every((r) => r.passed)).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("approved: it applies at the head, and under ci.locks holds apply locks on what it reaches", async () => {
|
|
261
|
+
serveGithub();
|
|
262
|
+
await runComponentsPrPlan(ctx({ beforeMerge: false, forge: undefined, noStatus: true }));
|
|
263
|
+
const { readFile } = await import("node:fs/promises");
|
|
264
|
+
const digest = JSON.parse(await readFile(join(dir, "pr-plan.json"), "utf8")).digest as string;
|
|
265
|
+
gateLedger = memoryGateLedgerPort({
|
|
266
|
+
resolutions: [{ version: 1, kind: "resolution", op: "pr-12", gate: "pr-apply", resolvedBy: "github:bob", timestamp: new Date().toISOString(), planDigest: digest }],
|
|
267
|
+
});
|
|
268
|
+
locksMode = "apply";
|
|
269
|
+
const ran: string[] = [];
|
|
270
|
+
onRun = (r) => void ran.push(r);
|
|
271
|
+
expect(await runComponentsPrApply(ctx({}))).toBe(0);
|
|
272
|
+
expect(ran).toEqual(["net", "app"]);
|
|
273
|
+
expect(taken).toEqual([{ gate: "pr-apply", targets: ["net", "app"], holder: expect.objectContaining({ pr: 12, stage: "apply", head: HEAD }) }]);
|
|
274
|
+
expect(await applyReport()).toMatchObject({ status: "applied", beforeMerge: { requires: expect.any(Array) } });
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/cli/handlers/pr.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
prStatusState,
|
|
107
107
|
renderPrNote,
|
|
108
108
|
type PrApproval,
|
|
109
|
+
type PrBeforeMerge,
|
|
109
110
|
type PrMember,
|
|
110
111
|
type PrPolicyReport,
|
|
111
112
|
type PrReport,
|
|
@@ -173,6 +174,8 @@ export interface Prepared {
|
|
|
173
174
|
policy: PrLoopPolicy;
|
|
174
175
|
/** What the policy said about the plan, for the report's `policy`. */
|
|
175
176
|
policyReport?: PrPolicyReport;
|
|
177
|
+
/** `pr-apply --before-merge` (#4301): the requires checks, for the report's `beforeMerge`. */
|
|
178
|
+
beforeMerge?: PrBeforeMerge;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
const fail = (message: string, hint?: string): number => {
|
|
@@ -495,6 +498,7 @@ async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload
|
|
|
495
498
|
if (p.hookFailed) report = { ...report, message: p.hookFailed };
|
|
496
499
|
if (p.routedAway && report.status === "nothing" && !report.message) report = { ...report, message: p.routedAway };
|
|
497
500
|
if (p.policyReport) report = { ...report, policy: p.policyReport };
|
|
501
|
+
if (p.beforeMerge) report = { ...report, beforeMerge: p.beforeMerge };
|
|
498
502
|
const note = noteFor(report, p.forge);
|
|
499
503
|
writeOutputs(ctx, report, note, upload);
|
|
500
504
|
if (!ctx.args.noStatus) await publish(p.forge, report, note);
|
|
@@ -730,10 +734,19 @@ async function overdueOps(forge: PrForge | undefined, member: string | undefined
|
|
|
730
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>] */
|
|
731
735
|
export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
|
|
732
736
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
733
|
-
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
|
+
});
|
|
734
747
|
}
|
|
735
748
|
|
|
736
|
-
async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
749
|
+
async function prApply(ctx: CommandContext, stage: StageEvents, beforeMerge?: PrBeforeMerge): Promise<number> {
|
|
737
750
|
// #4168: the review-rules security group turns --require-review on, read from the declaration at base.
|
|
738
751
|
const { securityGroupsAt } = await import("../../workspace/security");
|
|
739
752
|
const reviewRules = !ctx.args.requireReview && securityGroupsAt(process.cwd(), ctx.args.base).has("review-rules");
|
|
@@ -756,6 +769,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
756
769
|
}
|
|
757
770
|
const p = await prepare(ctx, "apply");
|
|
758
771
|
if (typeof p === "number") return p;
|
|
772
|
+
if (beforeMerge) p.beforeMerge = beforeMerge;
|
|
759
773
|
|
|
760
774
|
let pr: number | null = args.pr ?? null;
|
|
761
775
|
if (pr === null && !green && p.forge && p.plan.order.length > 0) {
|
|
@@ -876,6 +890,12 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
876
890
|
}
|
|
877
891
|
const unspent = await spendPolicy(op, judged, p.head);
|
|
878
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
|
+
}
|
|
879
899
|
|
|
880
900
|
// The record is written before the first component runs and again as each
|
|
881
901
|
// one settles, so a run killed partway still knows what applied.
|
|
@@ -1014,6 +1034,94 @@ async function heldLocksOf(p: Prepared, pr: number | null): Promise<string | nul
|
|
|
1014
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"}.`;
|
|
1015
1035
|
}
|
|
1016
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
|
+
// No `only`: ws-106 gives the declaration no way to leave a check out, so all four are asked (#4336).
|
|
1080
|
+
const requires = await checkPrRequires(forge, args.pr, head, { ignore });
|
|
1081
|
+
for (const r of requires) console.error(formatInfo(`${r.name}: ${r.passed ? "passed" : r.pending ? "pending" : "unmet"}, ${r.reason}`));
|
|
1082
|
+
const unmet = unmetRequires(requires);
|
|
1083
|
+
if (unmet) {
|
|
1084
|
+
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);
|
|
1085
|
+
}
|
|
1086
|
+
return { requires };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** The refusal report of an apply before merge stopped before it planned (#4301): nothing selected, nothing planned. */
|
|
1090
|
+
function earlyStop(ctx: CommandContext, forge: PrForge, at: { pr: number; base: string; head: string; gate: string; member?: string }) {
|
|
1091
|
+
return async (beforeMerge: PrBeforeMerge, refusal: "requires-unmet" | "before-merge-off", message: string, code: number): Promise<number> => {
|
|
1092
|
+
const plan = { order: [], waves: [], skipped: [], seeds: [], indeterminate: [] } as unknown as FanOutPlan;
|
|
1093
|
+
const signal = { changed: [], indeterminate: [], unclaimed: [] } as unknown as ComponentChangeSignal;
|
|
1094
|
+
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;
|
|
1095
|
+
const set: PrPlanSet = { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1096
|
+
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 });
|
|
1097
|
+
return stopper(ctx, p, undefined, undefined)(report, code);
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Take `apply` locks on the targets an apply before merge reaches (#4293,
|
|
1103
|
+
* #4301), held for pull request `pr` until it merges or closes. Returns the
|
|
1104
|
+
* refusal when another open pull request holds one, null once taken, or the
|
|
1105
|
+
* exit code when the locks could not be written.
|
|
1106
|
+
*/
|
|
1107
|
+
async function takeApplyLocks(p: Prepared, pr: number): Promise<string | null | number> {
|
|
1108
|
+
const { takeTargetLocks, describeHeldTargetLocks } = await import("../../lifecycle/target-lock");
|
|
1109
|
+
const isOpen = p.forge?.pullRequestOpen?.bind(p.forge);
|
|
1110
|
+
const by = process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER ?? "unknown";
|
|
1111
|
+
let result;
|
|
1112
|
+
try {
|
|
1113
|
+
result = await takeTargetLocks(p.gate, p.plan.order, { pr, by, at: new Date().toISOString(), stage: "apply", head: p.head }, { ...(isOpen ? { isOpen } : {}) });
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
return fail(`could not take the target locks of ${p.gate}, so nothing was applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
1116
|
+
}
|
|
1117
|
+
if (result.ok) {
|
|
1118
|
+
if (result.taken.length > 0) console.error(formatInfo(`pull request ${pr} holds ${result.taken.join(", ")} on ${p.gate} until it merges or closes`));
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const name = (n: number) => (p.forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
|
|
1122
|
+
return `${describeHeldTargetLocks(result.held, name)}, so nothing was applied.`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1017
1125
|
/** #4274: a component that published nothing is recorded by the plan digest its approval bound. */
|
|
1018
1126
|
function prAppliedDigests(p: Prepared, members: PrMember[]) {
|
|
1019
1127
|
return appliedDigestOf({ components: p.components, env: p.env, projectRoot: resolve("."), members });
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { appliedDigestOf, extractRunDigest, maybeRecordAutoRelease, type Applied
|
|
|
34
34
|
import { maybePersistBuildManifest } from "../../components/manifest-persistence";
|
|
35
35
|
import { failedRunExitCode, type DriverComponentResult } from "../../components/driver";
|
|
36
36
|
import { isComponentList, type ComponentRunPlan, type RunComponentsResult } from "../../components/cli-support";
|
|
37
|
+
import { parseComponentTargets } from "../../components/capability";
|
|
37
38
|
import { isApplyRecord, resolveCiRun, type ReleaseRecord, type RunOrigin } from "../../lifecycle/release-ledger";
|
|
38
39
|
import { gateAdmission, gateApprover } from "../../components/promote";
|
|
39
40
|
import { runOpGenerate } from "./run-generate";
|
|
@@ -1202,10 +1203,19 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
|
|
|
1202
1203
|
if (plan.added.length > 0) console.error(formatInfo(`added as dependencies: ${plan.added.join(", ")}`));
|
|
1203
1204
|
}
|
|
1204
1205
|
: undefined;
|
|
1206
|
+
// #4300: `--target <component>:<address>`, the addresses a component's steps narrow to.
|
|
1207
|
+
let targets: Record<string, string[]> | undefined;
|
|
1208
|
+
try {
|
|
1209
|
+
targets = ctx.args.targets && ctx.args.targets.length > 0 ? parseComponentTargets(ctx.args.targets) : undefined;
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
1212
|
+
return 1;
|
|
1213
|
+
}
|
|
1205
1214
|
let result: RunComponentsResult;
|
|
1206
1215
|
try {
|
|
1207
1216
|
result = await runtime.runComponents(projectPath, selector, {
|
|
1208
1217
|
env: ctx.args.env,
|
|
1218
|
+
...(targets ? { targets } : {}),
|
|
1209
1219
|
componentOutputs: seededOutputs,
|
|
1210
1220
|
onProgress,
|
|
1211
1221
|
...(events ? { onRunEvent: events.emit } : {}),
|
package/src/cli/main.ts
CHANGED
|
@@ -347,7 +347,7 @@ CI:
|
|
|
347
347
|
key. --trust commits its runner entry on a branch of its
|
|
348
348
|
own and pushes it for review; the target branch is never
|
|
349
349
|
written
|
|
350
|
-
ci init --workflow trunk-curated|trunk-curated+apply-on-merge
|
|
350
|
+
ci init --workflow trunk-curated|trunk-curated+apply-on-merge|apply-before-merge
|
|
351
351
|
[--forge github|gitlab|forgejo] [--security <group>]... [--yes]
|
|
352
352
|
Set the workspace up for a prescribed delivery workflow:
|
|
353
353
|
print what it would write as a diff, then write it on
|
|
@@ -1011,7 +1011,8 @@ Component release ledger + status:
|
|
|
1011
1011
|
when the wave is reached, with --canary <name>
|
|
1012
1012
|
for wave 1 and --wave <n> for one wave per CI
|
|
1013
1013
|
job; --resume <file> finishes an attempt that
|
|
1014
|
-
stopped
|
|
1014
|
+
stopped; --target <component>:<address> plans
|
|
1015
|
+
and applies only that address)
|
|
1015
1016
|
components pr-plan Plan the components a pull request reaches and
|
|
1016
1017
|
the ones that depend on them, and write the
|
|
1017
1018
|
report and note (--base <ref> --pr <n>
|
|
@@ -1055,6 +1056,11 @@ Component release ledger + status:
|
|
|
1055
1056
|
from the one last applied to --env, under the
|
|
1056
1057
|
approved plans of the merges since, else at gate
|
|
1057
1058
|
op green-<sha>;
|
|
1059
|
+
--before-merge --pr <n>: apply the open pull
|
|
1060
|
+
request's head under its gate, once the
|
|
1061
|
+
declaration at base sets ci.workflow to
|
|
1062
|
+
apply-before-merge and the forge says it is
|
|
1063
|
+
approved, mergeable, undiverged and green;
|
|
1058
1064
|
--events-json, both commands: the run-event
|
|
1059
1065
|
stream on stdout; --report-url <s3 or file URL>,
|
|
1060
1066
|
both commands: copy the report, the change set
|
package/src/cli/parse-args.ts
CHANGED
|
@@ -378,6 +378,11 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
378
378
|
const share = Number(raw);
|
|
379
379
|
if (!Number.isInteger(share) || share < 1) throw new Error(`--share needs a share number from 1: got "${raw ?? ""}"`);
|
|
380
380
|
result.share = share;
|
|
381
|
+
} else if (arg === "--target") {
|
|
382
|
+
// `chant components fan-out|run --components --target <component>:<address>` (#4300), repeatable.
|
|
383
|
+
const raw = args[++i];
|
|
384
|
+
if (!raw || raw.startsWith("-") || !raw.includes(":")) throw new Error("--target needs a component and an address: --target <component>:<address>");
|
|
385
|
+
(result.targets ??= []).push(raw);
|
|
381
386
|
} else if (arg === "--canary") {
|
|
382
387
|
const raw = args[++i];
|
|
383
388
|
if (!raw || raw.startsWith("-")) throw new Error("--canary needs a component name: --canary <name>[,<name>...]");
|
|
@@ -436,6 +441,12 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
436
441
|
} else if (arg === "--green") {
|
|
437
442
|
// `chant components pr-apply --green` (#4168 step 9): apply the green commit at HEAD.
|
|
438
443
|
result.green = true;
|
|
444
|
+
} else if (arg === "--before-merge") {
|
|
445
|
+
// `chant components pr-apply --before-merge` (#4301, ws-106): apply an open pull request's plan from its head.
|
|
446
|
+
result.beforeMerge = true;
|
|
447
|
+
} else if (arg === "--apply-before-merge") {
|
|
448
|
+
// `chant build --pr-loop --apply-before-merge` and `chant ci run apply --apply-before-merge` (#4301): the job that applies before merge.
|
|
449
|
+
result.applyBeforeMerge = true;
|
|
439
450
|
} else if (arg === "--apply-on-merge") {
|
|
440
451
|
// `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
|
|
441
452
|
result.applyOnMerge = true;
|
package/src/cli/registry.ts
CHANGED
|
@@ -294,6 +294,12 @@ export interface ParsedArgs {
|
|
|
294
294
|
share?: number;
|
|
295
295
|
/** `chant run wave --group <name>` (#3914) — apply one job group's runs of a decided wave. */
|
|
296
296
|
group?: string;
|
|
297
|
+
/**
|
|
298
|
+
* `chant components fan-out --target <component>:<address>` and `chant run
|
|
299
|
+
* --components --target ...` (#4300), repeatable: the addresses a
|
|
300
|
+
* component's steps narrow to. Raw values; `parseComponentTargets` groups them.
|
|
301
|
+
*/
|
|
302
|
+
targets?: string[];
|
|
297
303
|
/** `chant components fan-out --canary <component>` (#3049), repeatable — components that form wave 1. */
|
|
298
304
|
canary?: string[];
|
|
299
305
|
/**
|
|
@@ -353,6 +359,19 @@ export interface ParsedArgs {
|
|
|
353
359
|
green?: boolean;
|
|
354
360
|
/** `chant build --pr-loop --pr-env` (#4298) — add the env-up, env-down and env-sweep jobs of a pull request's environment. */
|
|
355
361
|
prEnv?: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* `chant components pr-apply --before-merge` (#4301, ws-106): apply an open
|
|
364
|
+
* pull request's plan from its head, once the declaration at base sets
|
|
365
|
+
* `ci.workflow: "apply-before-merge"` and the pull request is approved,
|
|
366
|
+
* mergeable, undiverged and green.
|
|
367
|
+
*/
|
|
368
|
+
beforeMerge?: boolean;
|
|
369
|
+
/**
|
|
370
|
+
* `chant build --pr-loop --apply-before-merge` and `chant ci run apply
|
|
371
|
+
* --apply-before-merge` (#4301): the pipeline's apply job applies an open
|
|
372
|
+
* pull request before it merges, and a merge records.
|
|
373
|
+
*/
|
|
374
|
+
applyBeforeMerge?: boolean;
|
|
356
375
|
/** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
|
|
357
376
|
prBuild?: boolean;
|
|
358
377
|
/** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
|