@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage-hook runner (#4288): one moment's hooks, with the `targets`,
|
|
3
|
+
* `perRun`, `onFailure` and argv filling a wave job uses. Its own module so
|
|
4
|
+
* a product that runs hooks around its own stages (terragucci) bundles it
|
|
5
|
+
* without the wave loop, the gate or the ledger. `op-waves-run.ts` runs
|
|
6
|
+
* the same hooks inside a wave job.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { opWaveHookRuns, type OpWaveHook, type OpWaveHookMoment } from "./op-waves";
|
|
13
|
+
|
|
14
|
+
/** What one moment's hooks came to, before any hold on another gate is settled. */
|
|
15
|
+
export interface HookListOutcome {
|
|
16
|
+
/** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
|
|
17
|
+
failed?: string;
|
|
18
|
+
/** The `onFailure: approve` hooks that failed. */
|
|
19
|
+
holds: string[];
|
|
20
|
+
outcomes: OpWaveHookOutcome[];
|
|
21
|
+
/** The hook calls that held the wave on another gate. */
|
|
22
|
+
otherGates: OtherGateHold[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
|
|
26
|
+
export const OP_WAVE_PENDING_RESERVED = [
|
|
27
|
+
"version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
|
|
28
|
+
"environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/** What a hook asks for when it holds a wave on another gate (#3821): it writes this to `CHANT_HOLD` and exits non-zero. */
|
|
32
|
+
export interface OpWaveHoldRequest {
|
|
33
|
+
/** The op the gate is recorded under. Default: the spec's name. */
|
|
34
|
+
op?: string;
|
|
35
|
+
gate: string;
|
|
36
|
+
/** The digest an approval binds. */
|
|
37
|
+
digest: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
/** Fields for the pending fact, as a plans file's `pending`. */
|
|
40
|
+
pending?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One hook's run (#3762). */
|
|
44
|
+
export interface OpWaveHookOutcome {
|
|
45
|
+
moment: OpWaveHookMoment;
|
|
46
|
+
name: string;
|
|
47
|
+
/** The run, for a `perRun` hook. */
|
|
48
|
+
target?: string;
|
|
49
|
+
exit: number;
|
|
50
|
+
outcome: "passed" | "failed" | "held" | "skipped";
|
|
51
|
+
/** Why the moment was skipped, for `skipped` (#3811). */
|
|
52
|
+
reason?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One run as a hook sees it (#3804). */
|
|
56
|
+
export interface OpWaveHookRun {
|
|
57
|
+
target: string;
|
|
58
|
+
op: string;
|
|
59
|
+
/** The run's plan file, once planned. */
|
|
60
|
+
plan?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Runs one argv; returns its exit code. `env` is added to the job's environment. Injected so tests run no processes. */
|
|
64
|
+
export type OpWaveExec = (argv: string[], cwd: string, env?: Record<string, string>) => number;
|
|
65
|
+
|
|
66
|
+
export const spawnExec: OpWaveExec = (argv, cwd, env) => {
|
|
67
|
+
const result = spawnSync(argv[0]!, argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
68
|
+
if (result.error) throw result.error;
|
|
69
|
+
return result.status ?? 1;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** {@link OpWaveExec} for {@link runStageHooksAsync} (#4334): the exit code comes back as a promise, so the event loop runs while a hook does. */
|
|
73
|
+
export type OpWaveAsyncExec = (argv: string[], cwd: string, env?: Record<string, string>) => Promise<number>;
|
|
74
|
+
|
|
75
|
+
/** {@link spawnExec} on `spawn`: it rejects when the command cannot start. */
|
|
76
|
+
export const spawnExecAsync: OpWaveAsyncExec = (argv, cwd, env) =>
|
|
77
|
+
new Promise((resolve, reject) => {
|
|
78
|
+
const child = spawn(argv[0]!, argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
79
|
+
child.once("error", reject);
|
|
80
|
+
child.once("close", (code) => resolve(code ?? 1));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** Fill one argv template. A placeholder whose value is not given is left as it is. */
|
|
84
|
+
export function opWaveArgv(
|
|
85
|
+
template: readonly string[],
|
|
86
|
+
values: { op: string; wave: string; target?: string; plan?: string; runs?: string; plans?: string },
|
|
87
|
+
): string[] {
|
|
88
|
+
const pairs: Array<[string, string | undefined]> = [
|
|
89
|
+
["{op}", values.op],
|
|
90
|
+
["{target}", values.target],
|
|
91
|
+
["{wave}", values.wave],
|
|
92
|
+
["{plans}", values.plans],
|
|
93
|
+
["{plan}", values.plan],
|
|
94
|
+
["{runs}", values.runs],
|
|
95
|
+
];
|
|
96
|
+
return template.map((part) => pairs.reduce((out, [key, value]) => (value === undefined ? out : out.replaceAll(key, value)), part));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
|
|
100
|
+
export function pendingFields(value: unknown, where: string): Record<string, unknown> | undefined {
|
|
101
|
+
if (value === undefined) return undefined;
|
|
102
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where}: \`pending\` must be an object of fields for the pending fact`);
|
|
103
|
+
const reserved = Object.keys(value).filter((key) => (OP_WAVE_PENDING_RESERVED as readonly string[]).includes(key));
|
|
104
|
+
if (reserved.length > 0) throw new Error(`${where}: \`pending\` sets ${reserved.join(", ")}, which chant writes itself`);
|
|
105
|
+
if ("description" in value && typeof (value as { description?: unknown }).description !== "string") throw new Error(`${where}: pending.description must be a string`);
|
|
106
|
+
return Object.keys(value).length > 0 ? (value as Record<string, unknown>) : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A hook call that held the wave on another gate (#3821), kept so it can run again once that gate is approved. */
|
|
110
|
+
export interface OtherGateHold {
|
|
111
|
+
label: string;
|
|
112
|
+
name: string;
|
|
113
|
+
moment: OpWaveHookMoment;
|
|
114
|
+
target?: string;
|
|
115
|
+
argv: string[];
|
|
116
|
+
env: Record<string, string>;
|
|
117
|
+
request: OpWaveHoldRequest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
|
|
121
|
+
export function readHoldRequest(path: string, log: (line: string) => void, label: string): OpWaveHoldRequest | undefined {
|
|
122
|
+
if (!existsSync(path)) return undefined;
|
|
123
|
+
try {
|
|
124
|
+
const r = JSON.parse(readFileSync(path, "utf-8")) as Partial<OpWaveHoldRequest>;
|
|
125
|
+
if (typeof r.gate !== "string" || r.gate === "" || typeof r.digest !== "string" || r.digest === "") throw new Error("it names no gate and digest");
|
|
126
|
+
if (r.op !== undefined && (typeof r.op !== "string" || r.op === "")) throw new Error("its op is not a name");
|
|
127
|
+
return { gate: r.gate, digest: r.digest, ...(r.op ? { op: r.op } : {}), ...(typeof r.description === "string" ? { description: r.description } : {}), ...(r.pending ? { pending: pendingFields(r.pending, `the hold ${label} wrote`) ?? {} } : {}) };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
log(`${label} wrote a hold file that cannot be read (${err instanceof Error ? err.message : String(err)}), so it holds the wave on its set digest`);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Where hooks run: the wave they belong to and the job's tools. */
|
|
135
|
+
export interface HookPlace {
|
|
136
|
+
/** The spec's name and `op`. */
|
|
137
|
+
name: string;
|
|
138
|
+
op: string;
|
|
139
|
+
waveName: string;
|
|
140
|
+
k: number;
|
|
141
|
+
cwd: string;
|
|
142
|
+
exec: OpWaveExec;
|
|
143
|
+
log: (line: string) => void;
|
|
144
|
+
/** Where a hook writes a hold on another gate (`CHANT_HOLD`). Default: under the wave's directory. */
|
|
145
|
+
holdPath?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One hook call {@link hookListSteps} asks its driver to run. The driver sends back the exit code. */
|
|
149
|
+
interface HookCall {
|
|
150
|
+
label: string;
|
|
151
|
+
argv: string[];
|
|
152
|
+
cwd: string;
|
|
153
|
+
env: Record<string, string>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The hook list as steps (#4334): it yields each call and takes its exit
|
|
158
|
+
* code back, so the sync and async runners share the targets, hold, outcome
|
|
159
|
+
* and other-gate handling and differ only in how they wait for a call.
|
|
160
|
+
*/
|
|
161
|
+
function* hookListSteps(
|
|
162
|
+
at: Omit<HookPlace, "exec">,
|
|
163
|
+
moment: OpWaveHookMoment,
|
|
164
|
+
hooks: readonly OpWaveHook[] | undefined,
|
|
165
|
+
runs: readonly OpWaveHookRun[],
|
|
166
|
+
runsPath: string,
|
|
167
|
+
extraEnv: Record<string, string>,
|
|
168
|
+
): Generator<HookCall, HookListOutcome, number> {
|
|
169
|
+
const out: HookListOutcome = { holds: [], outcomes: [], otherGates: [] };
|
|
170
|
+
if (!hooks || hooks.length === 0) return out;
|
|
171
|
+
const { name: specName, op, waveName, k, cwd, log } = at;
|
|
172
|
+
// #3821: a hook that holds the wave on another gate names it in this file.
|
|
173
|
+
const holdPath = at.holdPath ?? `.chant/op-waves/${specName}/wave-${k}/hold.json`;
|
|
174
|
+
const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
|
|
175
|
+
for (const hook of hooks) {
|
|
176
|
+
const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
|
|
177
|
+
// #4288: a hook with `targets` is for the runs they match. Once for the wave, it runs when one matches or no run is known yet.
|
|
178
|
+
const mine = opWaveHookRuns(hook, runs);
|
|
179
|
+
if (hook.targets && (hook.perRun ? mine.length === 0 : mine.length === 0 && runs.length > 0)) {
|
|
180
|
+
log(`${moment} hook ${name}: no run matches its targets (${hook.targets.join(", ")}), so it did not run`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const calls = hook.perRun
|
|
184
|
+
? mine.map((run) => ({
|
|
185
|
+
target: run.target as string | undefined,
|
|
186
|
+
argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
|
|
187
|
+
env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) } as Record<string, string>,
|
|
188
|
+
}))
|
|
189
|
+
: [{ target: undefined, argv: opWaveArgv(hook.command, { op, wave: waveName, runs: runsPath }), env: env as Record<string, string> }];
|
|
190
|
+
for (const call of calls) {
|
|
191
|
+
const label = `${moment} hook ${name}${call.target ? ` (${call.target})` : ""}`;
|
|
192
|
+
rmSync(join(cwd, holdPath), { force: true });
|
|
193
|
+
const code = yield { label, argv: call.argv, cwd, env: call.env };
|
|
194
|
+
const base = { moment, name, ...(call.target ? { target: call.target } : {}), exit: code };
|
|
195
|
+
if (code === 0) {
|
|
196
|
+
out.outcomes.push({ ...base, outcome: "passed" });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (hook.onFailure === "approve") {
|
|
200
|
+
out.outcomes.push({ ...base, outcome: "held" });
|
|
201
|
+
const request = readHoldRequest(join(cwd, holdPath), log, label);
|
|
202
|
+
if (request) {
|
|
203
|
+
out.otherGates.push({ label, name, moment, ...(call.target ? { target: call.target } : {}), argv: call.argv, env: call.env, request });
|
|
204
|
+
log(`${label} exited ${code} and holds wave ${k} (${waveName}) on gate ${request.op ?? specName} / ${request.gate}, digest ${request.digest}`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
out.holds.push(label);
|
|
208
|
+
log(`${label} exited ${code}, so wave ${k} (${waveName}) waits for an approval of its set digest`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
out.outcomes.push({ ...base, outcome: "failed" });
|
|
212
|
+
out.failed = `${label} exited ${code}`;
|
|
213
|
+
log(out.failed);
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The exit code a call that did not start counts as, after its error is logged. */
|
|
221
|
+
function didNotRun(call: HookCall, err: unknown, log: (line: string) => void): number {
|
|
222
|
+
log(`${call.label} did not run: ${err instanceof Error ? err.message : String(err)}`);
|
|
223
|
+
return 127;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Run one moment's hooks, in order: each once for the wave, or once per run
|
|
228
|
+
* for a `perRun` hook. A hook that fails stops the rest, unless its
|
|
229
|
+
* `onFailure` is `approve`: then it holds the wave and the hooks after it run.
|
|
230
|
+
*/
|
|
231
|
+
export function runHookList(
|
|
232
|
+
at: HookPlace,
|
|
233
|
+
moment: OpWaveHookMoment,
|
|
234
|
+
hooks: readonly OpWaveHook[] | undefined,
|
|
235
|
+
runs: readonly OpWaveHookRun[],
|
|
236
|
+
runsPath: string,
|
|
237
|
+
extraEnv: Record<string, string> = {},
|
|
238
|
+
): HookListOutcome {
|
|
239
|
+
const steps = hookListSteps(at, moment, hooks, runs, runsPath, extraEnv);
|
|
240
|
+
let step = steps.next();
|
|
241
|
+
while (!step.done) {
|
|
242
|
+
const call = step.value;
|
|
243
|
+
let code: number;
|
|
244
|
+
try {
|
|
245
|
+
code = at.exec(call.argv, call.cwd, call.env);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
code = didNotRun(call, err, at.log);
|
|
248
|
+
}
|
|
249
|
+
step = steps.next(code);
|
|
250
|
+
}
|
|
251
|
+
return step.value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** {@link runHookList} with an exec that returns a promise (#4334). */
|
|
255
|
+
export async function runHookListAsync(
|
|
256
|
+
at: Omit<HookPlace, "exec"> & { exec: OpWaveAsyncExec },
|
|
257
|
+
moment: OpWaveHookMoment,
|
|
258
|
+
hooks: readonly OpWaveHook[] | undefined,
|
|
259
|
+
runs: readonly OpWaveHookRun[],
|
|
260
|
+
runsPath: string,
|
|
261
|
+
extraEnv: Record<string, string> = {},
|
|
262
|
+
): Promise<HookListOutcome> {
|
|
263
|
+
const steps = hookListSteps(at, moment, hooks, runs, runsPath, extraEnv);
|
|
264
|
+
let step = steps.next();
|
|
265
|
+
while (!step.done) {
|
|
266
|
+
const call = step.value;
|
|
267
|
+
let code: number;
|
|
268
|
+
try {
|
|
269
|
+
code = await at.exec(call.argv, call.cwd, call.env);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
code = didNotRun(call, err, at.log);
|
|
272
|
+
}
|
|
273
|
+
step = steps.next(code);
|
|
274
|
+
}
|
|
275
|
+
return step.value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The options of {@link runStageHooks} (#4288). */
|
|
279
|
+
export interface RunStageHooksOptions {
|
|
280
|
+
/** What the hooks belong to: `CHANT_OP_WAVES`, and the `{op}` of a hook that runs once. */
|
|
281
|
+
name: string;
|
|
282
|
+
op: string;
|
|
283
|
+
/** `{wave}` and `CHANT_WAVE`. */
|
|
284
|
+
wave: string;
|
|
285
|
+
/** `CHANT_WAVE_NUMBER`. Default 0. */
|
|
286
|
+
k?: number;
|
|
287
|
+
/** The runs a `perRun` hook runs for and `targets` match against. */
|
|
288
|
+
runs: readonly OpWaveHookRun[];
|
|
289
|
+
/** `{runs}` and `CHANT_RUNS`, relative to `cwd`; the caller writes it. */
|
|
290
|
+
runsFile: string;
|
|
291
|
+
/** `CHANT_HOLD`, relative to `cwd`. */
|
|
292
|
+
holdFile: string;
|
|
293
|
+
/** Added to every hook's environment. */
|
|
294
|
+
env?: Record<string, string>;
|
|
295
|
+
cwd?: string;
|
|
296
|
+
exec?: OpWaveExec;
|
|
297
|
+
log?: (line: string) => void;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** What {@link runStageHooks} gave. */
|
|
301
|
+
export interface StageHooksRun {
|
|
302
|
+
outcomes: OpWaveHookOutcome[];
|
|
303
|
+
/** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
|
|
304
|
+
failed?: string;
|
|
305
|
+
/** The `onFailure: approve` hooks that failed. A hold on another gate counts as one here. */
|
|
306
|
+
holds: string[];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** {@link RunStageHooksOptions} for {@link runStageHooksAsync} (#4334). */
|
|
310
|
+
export interface RunStageHooksAsyncOptions extends Omit<RunStageHooksOptions, "exec"> {
|
|
311
|
+
/** Default: {@link spawnExecAsync}. */
|
|
312
|
+
exec?: OpWaveAsyncExec;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Where a stage runner's hooks run, without the exec. */
|
|
316
|
+
function stagePlace(options: Omit<RunStageHooksOptions, "exec">): Omit<HookPlace, "exec"> {
|
|
317
|
+
return { name: options.name, op: options.op, waveName: options.wave, k: options.k ?? 0, cwd: options.cwd ?? process.cwd(), log: options.log ?? ((line: string) => console.error(line)), holdPath: options.holdFile };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A stage run's result: a hold on another gate counts as a plain hold. */
|
|
321
|
+
function stageRun(out: HookListOutcome, name: string): StageHooksRun {
|
|
322
|
+
return { outcomes: out.outcomes, ...(out.failed ? { failed: out.failed } : {}), holds: [...out.holds, ...out.otherGates.map((g) => `${g.label} (on ${g.request.op ?? name} / ${g.request.gate})`)] };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Run one moment's hooks outside an Op waves job (#4288): the pull-request
|
|
327
|
+
* jobs, or a product's own stage, with the same `targets`, `perRun`,
|
|
328
|
+
* `onFailure` and argv filling a wave job uses. A hook that holds on another
|
|
329
|
+
* gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
|
|
330
|
+
* caller's.
|
|
331
|
+
*/
|
|
332
|
+
export function runStageHooks(moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, options: RunStageHooksOptions): StageHooksRun {
|
|
333
|
+
const place = { ...stagePlace(options), exec: options.exec ?? spawnExec };
|
|
334
|
+
return stageRun(runHookList(place, moment, hooks, options.runs, options.runsFile, options.env), options.name);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* {@link runStageHooks} with an exec that returns a promise (#4334), so a
|
|
339
|
+
* product can run several stages' hooks side by side. The hooks of one call
|
|
340
|
+
* still run one after another. Give each concurrent call its own `holdFile`
|
|
341
|
+
* and `runsFile`.
|
|
342
|
+
*/
|
|
343
|
+
export async function runStageHooksAsync(moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, options: RunStageHooksAsyncOptions): Promise<StageHooksRun> {
|
|
344
|
+
const place = { ...stagePlace(options), exec: options.exec ?? spawnExecAsync };
|
|
345
|
+
return stageRun(await runHookListAsync(place, moment, hooks, options.runs, options.runsFile, options.env), options.name);
|
|
346
|
+
}
|
|
@@ -34,7 +34,7 @@ describe("the pr-forge bundle", () => {
|
|
|
34
34
|
expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
|
|
35
35
|
}
|
|
36
36
|
expect(inputs.map((i) => i.replace(/^.*?packages\//, "packages/")).sort()).toEqual(["packages/core/src/pr-forge.ts"]);
|
|
37
|
-
// About
|
|
38
|
-
expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(
|
|
37
|
+
// About 50 KB today, after the pull request authors (#4000), pullRequestOpen for target locks (#4293), the four requires checks of an apply before merge (#4301) and the approver filter (#4337).
|
|
38
|
+
expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(52 * 1024);
|
|
39
39
|
});
|
|
40
40
|
});
|