@intentius/chant 0.124.41 → 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/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 +112 -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/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 +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -0
- package/dist/pr-forge.d.ts +28 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +251 -46
- 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 +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -0
- package/dist/wave-runtime.mjs +201 -201
- 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 +109 -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/op-waves-run.ts +20 -228
- package/src/op/stage-hooks.ts +251 -0
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.ts +235 -37
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.ts +25 -1
- 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/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
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
* `@<component>.publish.*`) resolves.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
|
|
30
|
+
import { assertTargetsAccepted, resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
|
|
31
31
|
import { componentPlanDigest } from "./gate-plan";
|
|
32
32
|
import { componentDeployInputs, type ComponentDeployInputs } from "./deploy-inputs";
|
|
33
33
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
34
|
-
import type { CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
|
|
34
|
+
import type { CapabilityPlan, CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
|
|
35
35
|
import type { WaveMember } from "../gated-waves";
|
|
36
36
|
import type { ChangeSetPart } from "../change-set";
|
|
37
37
|
|
|
@@ -101,12 +101,13 @@ export async function planWaveComponent(
|
|
|
101
101
|
const plans: Record<string, unknown> = {};
|
|
102
102
|
const parts: ChangeSetPart[] = [];
|
|
103
103
|
let outputs: PlannedOutputs | undefined;
|
|
104
|
+
assertTargetsAccepted(component, registry, ctx);
|
|
104
105
|
for (const step of capabilitySteps(component.deploy)) {
|
|
105
106
|
const capability = registry.resolve(step.kind);
|
|
106
107
|
if (!capability.plan) continue;
|
|
107
108
|
const { kind: _kind, ...rest } = step;
|
|
108
109
|
const input = resolveStepInput(rest, {}, componentOutputs);
|
|
109
|
-
const planned = await capability.plan(planContext(ctx, rest, componentOutputs), input as never);
|
|
110
|
+
const planned = bindTargets(step.kind, ctx, await capability.plan(planContext(ctx, rest, componentOutputs), input as never));
|
|
110
111
|
const member = { member: planned.member, planDigest: planned.planDigest };
|
|
111
112
|
members.push(member);
|
|
112
113
|
parts.push(planned.changeSet ?? emptyPart(member));
|
|
@@ -124,6 +125,7 @@ export async function planWaveComponent(
|
|
|
124
125
|
component,
|
|
125
126
|
...(ctx.vars ? { vars: ctx.vars } : {}),
|
|
126
127
|
inputs: read.inputs,
|
|
128
|
+
...(ctx.targets ? { targets: ctx.targets } : {}),
|
|
127
129
|
}),
|
|
128
130
|
};
|
|
129
131
|
members.push(member);
|
|
@@ -145,6 +147,39 @@ export async function planWaveComponent(
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A member's digest with its targets in it (#4300), the way terragucci's
|
|
152
|
+
* cone plan digest binds a cone: the digest over the plan's own digest and
|
|
153
|
+
* the sorted targets, so an approval of one narrowing covers no other.
|
|
154
|
+
*/
|
|
155
|
+
export function targetedPlanDigest(planDigest: string, targets: readonly string[]): string {
|
|
156
|
+
return computePlanDigest("capability-targets", { plan: planDigest, targets: [...new Set(targets)].sort() });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A step's plan made with `ctx.targets` (#4300), its digest bound to them.
|
|
161
|
+
* The plan must give back the targets it was asked for, which says it planned
|
|
162
|
+
* those alone; one that gives back others, or none, refuses. Without
|
|
163
|
+
* `ctx.targets`, the plan as it came.
|
|
164
|
+
*/
|
|
165
|
+
export function bindTargets(kind: string, ctx: DeployContext, planned: CapabilityPlan): CapabilityPlan {
|
|
166
|
+
const asked = ctx.targets;
|
|
167
|
+
if (!asked || asked.length === 0) return planned;
|
|
168
|
+
const want = [...new Set(asked)].sort();
|
|
169
|
+
const got = [...new Set(planned.targets ?? [])].sort();
|
|
170
|
+
if (got.length !== want.length || got.some((t, i) => t !== want[i])) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`component "${ctx.component}": ${kind} was asked to plan ${want.join(", ")} but its plan of ${planned.member} ` +
|
|
173
|
+
(got.length === 0 ? "names no targets" : `names ${got.join(", ")}`),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const planDigest = targetedPlanDigest(planned.planDigest, want);
|
|
177
|
+
const changeSet = planned.changeSet
|
|
178
|
+
? { ...planned.changeSet, member: { ...planned.changeSet.member, planDigest } }
|
|
179
|
+
: undefined;
|
|
180
|
+
return { ...planned, planDigest, targets: want, ...(changeSet ? { changeSet } : {}) };
|
|
181
|
+
}
|
|
182
|
+
|
|
148
183
|
/** `next` over `prior`, as a later step's outputs win over an earlier one's in a run. */
|
|
149
184
|
function mergePlannedOutputs(prior: PlannedOutputs | undefined, next: PlannedOutputs): PlannedOutputs {
|
|
150
185
|
const known = { ...(prior?.known ?? {}), ...next.known };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pull request's base and head branch names, read off the run's own
|
|
3
|
+
* event (#4299). Its own module so the review agent bundles without
|
|
4
|
+
* predict-behaviour's estate code.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Where the base branch name comes from, most specific first. Pure — exported for testing. */
|
|
8
|
+
export function baseRefFrom(
|
|
9
|
+
env: Record<string, string | undefined>,
|
|
10
|
+
explicit?: string,
|
|
11
|
+
): string | undefined {
|
|
12
|
+
const fromArgs = explicit?.trim();
|
|
13
|
+
if (fromArgs) return fromArgs;
|
|
14
|
+
for (const source of ["GITHUB_BASE_REF", "CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]) {
|
|
15
|
+
const value = env[source]?.trim();
|
|
16
|
+
if (value) return value;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** What the head side is called in the finding: the source branch when the run names one, else the short commit. */
|
|
22
|
+
export function headRefFrom(env: Record<string, string | undefined>, shortSha?: string): string {
|
|
23
|
+
for (const source of ["GITHUB_HEAD_REF", "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"]) {
|
|
24
|
+
const value = env[source]?.trim();
|
|
25
|
+
if (value) return value;
|
|
26
|
+
}
|
|
27
|
+
return shortSha?.trim() || "head";
|
|
28
|
+
}
|
|
@@ -85,6 +85,9 @@ import type { BehaviourKinds } from "../../behaviour-kinds";
|
|
|
85
85
|
import { createBehaviourPredict } from "../../behaviour-predict";
|
|
86
86
|
import type { SerializerResult } from "../../serializer";
|
|
87
87
|
import { markerSlug, reconcilePr, suppliedMarker, type ReconcileResult } from "./reconcile";
|
|
88
|
+
import { baseRefFrom, headRefFrom } from "./pr-refs";
|
|
89
|
+
|
|
90
|
+
export { baseRefFrom, headRefFrom };
|
|
88
91
|
|
|
89
92
|
const execAsync = promisify(exec);
|
|
90
93
|
|
|
@@ -294,29 +297,6 @@ export function noBaseRefMessage(): string {
|
|
|
294
297
|
);
|
|
295
298
|
}
|
|
296
299
|
|
|
297
|
-
/** Where the base branch name comes from, most specific first. Pure — exported for testing. */
|
|
298
|
-
export function baseRefFrom(
|
|
299
|
-
env: Record<string, string | undefined>,
|
|
300
|
-
explicit?: string,
|
|
301
|
-
): string | undefined {
|
|
302
|
-
const fromArgs = explicit?.trim();
|
|
303
|
-
if (fromArgs) return fromArgs;
|
|
304
|
-
for (const source of ["GITHUB_BASE_REF", "CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]) {
|
|
305
|
-
const value = env[source]?.trim();
|
|
306
|
-
if (value) return value;
|
|
307
|
-
}
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** What the head side is called in the finding: the source branch when the run names one, else the short commit. */
|
|
312
|
-
export function headRefFrom(env: Record<string, string | undefined>, shortSha?: string): string {
|
|
313
|
-
for (const source of ["GITHUB_HEAD_REF", "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"]) {
|
|
314
|
-
const value = env[source]?.trim();
|
|
315
|
-
if (value) return value;
|
|
316
|
-
}
|
|
317
|
-
return shortSha?.trim() || "head";
|
|
318
|
-
}
|
|
319
|
-
|
|
320
300
|
/** A checked-out base branch: where its project root is, and how to remove it. */
|
|
321
301
|
export interface BaseCheckout {
|
|
322
302
|
/** The base branch's counterpart of the working directory. */
|
|
@@ -27,7 +27,7 @@ import { spawn, execFile } from "node:child_process";
|
|
|
27
27
|
import { promisify } from "node:util";
|
|
28
28
|
import { readFile } from "node:fs/promises";
|
|
29
29
|
import type { ChangeSetDocument, ChangeSetEntry } from "../../change-set";
|
|
30
|
-
import { baseRefFrom } from "./
|
|
30
|
+
import { baseRefFrom } from "./pr-refs";
|
|
31
31
|
import { markerSlug, reconcilePr, suppliedMarker } from "./reconcile";
|
|
32
32
|
|
|
33
33
|
const execFileAsync = promisify(execFile);
|
package/src/op/op-waves-run.ts
CHANGED
|
@@ -105,6 +105,26 @@ import {
|
|
|
105
105
|
type OpWaveRun,
|
|
106
106
|
type OpWavesSpec,
|
|
107
107
|
} from "./op-waves";
|
|
108
|
+
import {
|
|
109
|
+
OP_WAVE_PENDING_RESERVED,
|
|
110
|
+
opWaveArgv,
|
|
111
|
+
pendingFields,
|
|
112
|
+
readHoldRequest,
|
|
113
|
+
runHookList,
|
|
114
|
+
runStageHooks,
|
|
115
|
+
spawnExec,
|
|
116
|
+
type HookPlace,
|
|
117
|
+
type OpWaveExec,
|
|
118
|
+
type OpWaveHoldRequest,
|
|
119
|
+
type OpWaveHookOutcome,
|
|
120
|
+
type OpWaveHookRun,
|
|
121
|
+
type OtherGateHold,
|
|
122
|
+
type RunStageHooksOptions,
|
|
123
|
+
type StageHooksRun,
|
|
124
|
+
} from "./stage-hooks";
|
|
125
|
+
|
|
126
|
+
export { OP_WAVE_PENDING_RESERVED, opWaveArgv, runStageHooks };
|
|
127
|
+
export type { OpWaveExec, OpWaveHoldRequest, OpWaveHookOutcome, OpWaveHookRun, RunStageHooksOptions, StageHooksRun };
|
|
108
128
|
|
|
109
129
|
/** What a run's plan command writes to `{plan}`. */
|
|
110
130
|
export interface OpWavePlanFile {
|
|
@@ -154,12 +174,6 @@ export interface OpWavePlansFile {
|
|
|
154
174
|
waves?: number;
|
|
155
175
|
}
|
|
156
176
|
|
|
157
|
-
/** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
|
|
158
|
-
export const OP_WAVE_PENDING_RESERVED = [
|
|
159
|
-
"version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
|
|
160
|
-
"environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy",
|
|
161
|
-
] as const;
|
|
162
|
-
|
|
163
177
|
/** What chant writes to `{runs}` for a wave-level command or a hook (#3762). */
|
|
164
178
|
export interface OpWaveRunsFile {
|
|
165
179
|
/** The wave's name. */
|
|
@@ -305,18 +319,6 @@ export interface OpWaveApplyOutcome {
|
|
|
305
319
|
|
|
306
320
|
export const OP_WAVE_APPLY_OUTCOMES = ["stood-down", "held", "replan"] as const;
|
|
307
321
|
|
|
308
|
-
/** What a hook asks for when it holds a wave on another gate (#3821): it writes this to `CHANT_HOLD` and exits non-zero. */
|
|
309
|
-
export interface OpWaveHoldRequest {
|
|
310
|
-
/** The op the gate is recorded under. Default: the spec's name. */
|
|
311
|
-
op?: string;
|
|
312
|
-
gate: string;
|
|
313
|
-
/** The digest an approval binds. */
|
|
314
|
-
digest: string;
|
|
315
|
-
description?: string;
|
|
316
|
-
/** Fields for the pending fact, as a plans file's `pending`. */
|
|
317
|
-
pending?: Record<string, unknown>;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
322
|
/** A hold on another gate, as the decision records it (#3821). */
|
|
321
323
|
export interface OpWaveHeldOn {
|
|
322
324
|
op: string;
|
|
@@ -347,26 +349,6 @@ export interface OpWaveHeldOnApproval {
|
|
|
347
349
|
approvedAt: string;
|
|
348
350
|
}
|
|
349
351
|
|
|
350
|
-
/** One hook's run (#3762). */
|
|
351
|
-
export interface OpWaveHookOutcome {
|
|
352
|
-
moment: OpWaveHookMoment;
|
|
353
|
-
name: string;
|
|
354
|
-
/** The run, for a `perRun` hook. */
|
|
355
|
-
target?: string;
|
|
356
|
-
exit: number;
|
|
357
|
-
outcome: "passed" | "failed" | "held" | "skipped";
|
|
358
|
-
/** Why the moment was skipped, for `skipped` (#3811). */
|
|
359
|
-
reason?: string;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** One run as a hook sees it (#3804). */
|
|
363
|
-
export interface OpWaveHookRun {
|
|
364
|
-
target: string;
|
|
365
|
-
op: string;
|
|
366
|
-
/** The run's plan file, once planned. */
|
|
367
|
-
plan?: string;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
352
|
/**
|
|
371
353
|
* What chant hands an apply command in `CHANT_HOOKS` (#3804), and what
|
|
372
354
|
* {@link opWaveHooksAtBase} builds for an apply outside the waves. Paths are
|
|
@@ -442,15 +424,6 @@ export interface OpWaveApplyHooksRun {
|
|
|
442
424
|
proceed: boolean;
|
|
443
425
|
}
|
|
444
426
|
|
|
445
|
-
/** Runs one argv; returns its exit code. `env` is added to the job's environment. Injected so tests run no processes. */
|
|
446
|
-
export type OpWaveExec = (argv: string[], cwd: string, env?: Record<string, string>) => number;
|
|
447
|
-
|
|
448
|
-
const spawnExec: OpWaveExec = (argv, cwd, env) => {
|
|
449
|
-
const result = spawnSync(argv[0]!, argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
450
|
-
if (result.error) throw result.error;
|
|
451
|
-
return result.status ?? 1;
|
|
452
|
-
};
|
|
453
|
-
|
|
454
427
|
/** Reads one file at one commit, or null when the commit has no such file. Injected for tests. */
|
|
455
428
|
export type OpWaveShowAtBase = (base: string, path: string, cwd: string) => { sha: string; text: string | null };
|
|
456
429
|
|
|
@@ -581,22 +554,6 @@ function pointAtBase(spec: OpWavesSpec, name: string, where: string): Pick<OpWav
|
|
|
581
554
|
return { point: opWavePointAt(spec, name)! };
|
|
582
555
|
}
|
|
583
556
|
|
|
584
|
-
/** Fill one argv template. A placeholder whose value is not given is left as it is. */
|
|
585
|
-
export function opWaveArgv(
|
|
586
|
-
template: readonly string[],
|
|
587
|
-
values: { op: string; wave: string; target?: string; plan?: string; runs?: string; plans?: string },
|
|
588
|
-
): string[] {
|
|
589
|
-
const pairs: Array<[string, string | undefined]> = [
|
|
590
|
-
["{op}", values.op],
|
|
591
|
-
["{target}", values.target],
|
|
592
|
-
["{wave}", values.wave],
|
|
593
|
-
["{plans}", values.plans],
|
|
594
|
-
["{plan}", values.plan],
|
|
595
|
-
["{runs}", values.runs],
|
|
596
|
-
];
|
|
597
|
-
return template.map((part) => pairs.reduce((out, [key, value]) => (value === undefined ? out : out.replaceAll(key, value)), part));
|
|
598
|
-
}
|
|
599
|
-
|
|
600
557
|
/** Where a run's plan file goes. */
|
|
601
558
|
export function opWavePlanPath(spec: Pick<OpWavesSpec, "name">, wave: number, target: string): string {
|
|
602
559
|
return `.chant/op-waves/${spec.name}/wave-${wave}/${target.replace(/[^A-Za-z0-9._-]/g, "_")}.plan.json`;
|
|
@@ -853,16 +810,6 @@ function extraMembers(value: unknown, targets: ReadonlySet<string>, where: strin
|
|
|
853
810
|
return out.length > 0 ? out : undefined;
|
|
854
811
|
}
|
|
855
812
|
|
|
856
|
-
/** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
|
|
857
|
-
function pendingFields(value: unknown, where: string): Record<string, unknown> | undefined {
|
|
858
|
-
if (value === undefined) return undefined;
|
|
859
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where}: \`pending\` must be an object of fields for the pending fact`);
|
|
860
|
-
const reserved = Object.keys(value).filter((key) => (OP_WAVE_PENDING_RESERVED as readonly string[]).includes(key));
|
|
861
|
-
if (reserved.length > 0) throw new Error(`${where}: \`pending\` sets ${reserved.join(", ")}, which chant writes itself`);
|
|
862
|
-
if ("description" in value && typeof (value as { description?: unknown }).description !== "string") throw new Error(`${where}: pending.description must be a string`);
|
|
863
|
-
return Object.keys(value).length > 0 ? (value as Record<string, unknown>) : undefined;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
813
|
/**
|
|
867
814
|
* A wave's set digest (#3821): {@link waveSetDigest} over its runs' plans,
|
|
868
815
|
* or under `digest.over: "changing"` over the runs whose plan is not empty,
|
|
@@ -883,115 +830,6 @@ interface HooksOutcome {
|
|
|
883
830
|
waitingOn?: OpWaveHeldOn;
|
|
884
831
|
}
|
|
885
832
|
|
|
886
|
-
/** A hook call that held the wave on another gate (#3821), kept so it can run again once that gate is approved. */
|
|
887
|
-
interface OtherGateHold {
|
|
888
|
-
label: string;
|
|
889
|
-
name: string;
|
|
890
|
-
moment: OpWaveHookMoment;
|
|
891
|
-
target?: string;
|
|
892
|
-
argv: string[];
|
|
893
|
-
env: Record<string, string>;
|
|
894
|
-
request: OpWaveHoldRequest;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
|
|
898
|
-
function readHoldRequest(path: string, log: (line: string) => void, label: string): OpWaveHoldRequest | undefined {
|
|
899
|
-
if (!existsSync(path)) return undefined;
|
|
900
|
-
try {
|
|
901
|
-
const r = JSON.parse(readFileSync(path, "utf-8")) as Partial<OpWaveHoldRequest>;
|
|
902
|
-
if (typeof r.gate !== "string" || r.gate === "" || typeof r.digest !== "string" || r.digest === "") throw new Error("it names no gate and digest");
|
|
903
|
-
if (r.op !== undefined && (typeof r.op !== "string" || r.op === "")) throw new Error("its op is not a name");
|
|
904
|
-
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`) ?? {} } : {}) };
|
|
905
|
-
} catch (err) {
|
|
906
|
-
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`);
|
|
907
|
-
return undefined;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/** Where hooks run: the wave they belong to and the job's tools. */
|
|
912
|
-
interface HookPlace {
|
|
913
|
-
/** The spec's name and `op`. */
|
|
914
|
-
name: string;
|
|
915
|
-
op: string;
|
|
916
|
-
waveName: string;
|
|
917
|
-
k: number;
|
|
918
|
-
cwd: string;
|
|
919
|
-
exec: OpWaveExec;
|
|
920
|
-
log: (line: string) => void;
|
|
921
|
-
/** Where a hook writes a hold on another gate (`CHANT_HOLD`). Default: under the wave's directory. */
|
|
922
|
-
holdPath?: string;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Run one moment's hooks, in order: each once for the wave, or once per run
|
|
927
|
-
* for a `perRun` hook. A hook that fails stops the rest, unless its
|
|
928
|
-
* `onFailure` is `approve`: then it holds the wave and the hooks after it run.
|
|
929
|
-
*/
|
|
930
|
-
function runHookList(
|
|
931
|
-
at: HookPlace,
|
|
932
|
-
moment: OpWaveHookMoment,
|
|
933
|
-
hooks: readonly OpWaveHook[] | undefined,
|
|
934
|
-
runs: readonly OpWaveHookRun[],
|
|
935
|
-
runsPath: string,
|
|
936
|
-
extraEnv: Record<string, string> = {},
|
|
937
|
-
): HooksOutcome & { outcomes: OpWaveHookOutcome[]; otherGates: OtherGateHold[] } {
|
|
938
|
-
const out: HooksOutcome & { outcomes: OpWaveHookOutcome[]; otherGates: OtherGateHold[] } = { holds: [], outcomes: [], otherGates: [] };
|
|
939
|
-
if (!hooks || hooks.length === 0) return out;
|
|
940
|
-
const { name: specName, op, waveName, k, cwd, exec, log } = at;
|
|
941
|
-
// #3821: a hook that holds the wave on another gate names it in this file.
|
|
942
|
-
const holdPath = at.holdPath ?? `.chant/op-waves/${specName}/wave-${k}/hold.json`;
|
|
943
|
-
const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
|
|
944
|
-
for (const hook of hooks) {
|
|
945
|
-
const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
|
|
946
|
-
// #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.
|
|
947
|
-
const mine = opWaveHookRuns(hook, runs);
|
|
948
|
-
if (hook.targets && (hook.perRun ? mine.length === 0 : mine.length === 0 && runs.length > 0)) {
|
|
949
|
-
log(`${moment} hook ${name}: no run matches its targets (${hook.targets.join(", ")}), so it did not run`);
|
|
950
|
-
continue;
|
|
951
|
-
}
|
|
952
|
-
const calls = hook.perRun
|
|
953
|
-
? mine.map((run) => ({
|
|
954
|
-
target: run.target as string | undefined,
|
|
955
|
-
argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
|
|
956
|
-
env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) } as Record<string, string>,
|
|
957
|
-
}))
|
|
958
|
-
: [{ target: undefined, argv: opWaveArgv(hook.command, { op, wave: waveName, runs: runsPath }), env: env as Record<string, string> }];
|
|
959
|
-
for (const call of calls) {
|
|
960
|
-
const label = `${moment} hook ${name}${call.target ? ` (${call.target})` : ""}`;
|
|
961
|
-
let code: number;
|
|
962
|
-
rmSync(join(cwd, holdPath), { force: true });
|
|
963
|
-
try {
|
|
964
|
-
code = exec(call.argv, cwd, call.env);
|
|
965
|
-
} catch (err) {
|
|
966
|
-
log(`${label} did not run: ${err instanceof Error ? err.message : String(err)}`);
|
|
967
|
-
code = 127;
|
|
968
|
-
}
|
|
969
|
-
const base = { moment, name, ...(call.target ? { target: call.target } : {}), exit: code };
|
|
970
|
-
if (code === 0) {
|
|
971
|
-
out.outcomes.push({ ...base, outcome: "passed" });
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
if (hook.onFailure === "approve") {
|
|
975
|
-
out.outcomes.push({ ...base, outcome: "held" });
|
|
976
|
-
const request = readHoldRequest(join(cwd, holdPath), log, label);
|
|
977
|
-
if (request) {
|
|
978
|
-
out.otherGates.push({ label, name, moment, ...(call.target ? { target: call.target } : {}), argv: call.argv, env: call.env, request });
|
|
979
|
-
log(`${label} exited ${code} and holds wave ${k} (${waveName}) on gate ${request.op ?? specName} / ${request.gate}, digest ${request.digest}`);
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
out.holds.push(label);
|
|
983
|
-
log(`${label} exited ${code}, so wave ${k} (${waveName}) waits for an approval of its set digest`);
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
out.outcomes.push({ ...base, outcome: "failed" });
|
|
987
|
-
out.failed = `${label} exited ${code}`;
|
|
988
|
-
log(out.failed);
|
|
989
|
-
return out;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return out;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
833
|
const placeOf = (ctx: WaveContext): HookPlace => ({ name: ctx.spec.name, op: ctx.spec.op, waveName: ctx.wave.name, k: ctx.k, cwd: ctx.cwd, exec: ctx.exec, log: ctx.log });
|
|
996
834
|
|
|
997
835
|
const hookRuns = (spec: OpWavesSpec, k: number, runs: readonly OpWaveRun[], members?: ReadonlyMap<string, OpWaveMember>): OpWaveHookRun[] =>
|
|
@@ -1076,52 +914,6 @@ async function runHooks(
|
|
|
1076
914
|
return back;
|
|
1077
915
|
}
|
|
1078
916
|
|
|
1079
|
-
/** The options of {@link runStageHooks} (#4288). */
|
|
1080
|
-
export interface RunStageHooksOptions {
|
|
1081
|
-
/** What the hooks belong to: `CHANT_OP_WAVES`, and the `{op}` of a hook that runs once. */
|
|
1082
|
-
name: string;
|
|
1083
|
-
op: string;
|
|
1084
|
-
/** `{wave}` and `CHANT_WAVE`. */
|
|
1085
|
-
wave: string;
|
|
1086
|
-
/** `CHANT_WAVE_NUMBER`. Default 0. */
|
|
1087
|
-
k?: number;
|
|
1088
|
-
/** The runs a `perRun` hook runs for and `targets` match against. */
|
|
1089
|
-
runs: readonly OpWaveHookRun[];
|
|
1090
|
-
/** `{runs}` and `CHANT_RUNS`, relative to `cwd`; the caller writes it. */
|
|
1091
|
-
runsFile: string;
|
|
1092
|
-
/** `CHANT_HOLD`, relative to `cwd`. */
|
|
1093
|
-
holdFile: string;
|
|
1094
|
-
/** Added to every hook's environment. */
|
|
1095
|
-
env?: Record<string, string>;
|
|
1096
|
-
cwd?: string;
|
|
1097
|
-
exec?: OpWaveExec;
|
|
1098
|
-
log?: (line: string) => void;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
/** What {@link runStageHooks} gave. */
|
|
1102
|
-
export interface StageHooksRun {
|
|
1103
|
-
outcomes: OpWaveHookOutcome[];
|
|
1104
|
-
/** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
|
|
1105
|
-
failed?: string;
|
|
1106
|
-
/** The `onFailure: approve` hooks that failed. A hold on another gate counts as one here. */
|
|
1107
|
-
holds: string[];
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* Run one moment's hooks outside an Op waves job (#4288): the pull-request
|
|
1112
|
-
* jobs, or a product's own stage, with the same `targets`, `perRun`,
|
|
1113
|
-
* `onFailure` and argv filling a wave job uses. A hook that holds on another
|
|
1114
|
-
* gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
|
|
1115
|
-
* caller's.
|
|
1116
|
-
*/
|
|
1117
|
-
export function runStageHooks(moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, options: RunStageHooksOptions): StageHooksRun {
|
|
1118
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1119
|
-
const log = options.log ?? ((line: string) => console.error(line));
|
|
1120
|
-
const place: HookPlace = { name: options.name, op: options.op, waveName: options.wave, k: options.k ?? 0, cwd, exec: options.exec ?? spawnExec, log, holdPath: options.holdFile };
|
|
1121
|
-
const out = runHookList(place, moment, hooks, options.runs, options.runsFile, options.env);
|
|
1122
|
-
return { outcomes: out.outcomes, ...(out.failed ? { failed: out.failed } : {}), holds: [...out.holds, ...out.otherGates.map((g) => `${g.label} (on ${g.request.op ?? options.name} / ${g.request.gate})`)] };
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
917
|
/**
|
|
1126
918
|
* The hooks that run before a job plans (#4288): `before-observe`, then
|
|
1127
919
|
* `before-plan`. A failure or a hold on another gate in the first ends it
|