@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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource targeting (#4300): `--target <component>:<address>` narrows a
|
|
3
|
+
* component's steps, each capability accepts or refuses by name, and the
|
|
4
|
+
* targets join the plan digest an approval binds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { CapabilityRegistry, parseComponentTargets, targetRefusal, type CapabilityPlan, type DeployContext } from "./capability";
|
|
9
|
+
import { deployContext, runComponentDeploy, type DriverComponent } from "./driver";
|
|
10
|
+
import { bindTargets, planWaveComponent, targetedPlanDigest } from "./wave-plan";
|
|
11
|
+
import { fanOutGateDigest, runFanOut } from "./fan-out-run";
|
|
12
|
+
import { planFanOut } from "./fan-out";
|
|
13
|
+
import { memoryGateLedgerPort } from "../op/gate";
|
|
14
|
+
|
|
15
|
+
const component = (name: string, ...kinds: string[]): DriverComponent => ({
|
|
16
|
+
name,
|
|
17
|
+
deploy: [{ phase: "Deploy", steps: kinds.map((kind) => ({ kind })) }],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** A registry with a verb that accepts targets, one that refuses, and one that says nothing. */
|
|
21
|
+
function registry(seen: DeployContext[] = []): CapabilityRegistry {
|
|
22
|
+
const r = new CapabilityRegistry();
|
|
23
|
+
r.register({
|
|
24
|
+
kind: "narrow",
|
|
25
|
+
targets: "accept",
|
|
26
|
+
async plan(ctx: DeployContext): Promise<CapabilityPlan> {
|
|
27
|
+
seen.push(ctx);
|
|
28
|
+
return { member: ctx.component, planDigest: "sha256:base", ...(ctx.targets ? { targets: ctx.targets } : {}) };
|
|
29
|
+
},
|
|
30
|
+
async run(ctx: DeployContext) {
|
|
31
|
+
seen.push(ctx);
|
|
32
|
+
return { ok: true };
|
|
33
|
+
},
|
|
34
|
+
} as never);
|
|
35
|
+
r.register({
|
|
36
|
+
kind: "whole",
|
|
37
|
+
targets: { refuse: "whole applies everything" },
|
|
38
|
+
async plan(ctx: DeployContext): Promise<CapabilityPlan> {
|
|
39
|
+
return { member: ctx.component, planDigest: "sha256:whole" };
|
|
40
|
+
},
|
|
41
|
+
async run() {
|
|
42
|
+
return { ok: true };
|
|
43
|
+
},
|
|
44
|
+
} as never);
|
|
45
|
+
r.register({
|
|
46
|
+
kind: "silent",
|
|
47
|
+
async run() {
|
|
48
|
+
return { ok: true };
|
|
49
|
+
},
|
|
50
|
+
} as never);
|
|
51
|
+
return r;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("parseComponentTargets", () => {
|
|
55
|
+
test("groups by component, sorted and unique, splitting at the first colon", () => {
|
|
56
|
+
expect(parseComponentTargets(["net:aws_vpc.main", "net:aws_subnet.a", "net:aws_vpc.main", "app:Deployment/prod/web", "net:x[\"a:b\"]"])).toEqual({
|
|
57
|
+
app: ["Deployment/prod/web"],
|
|
58
|
+
net: ["aws_subnet.a", "aws_vpc.main", 'x["a:b"]'],
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("refuses a value with no component or no address, and an unknown component", () => {
|
|
63
|
+
expect(() => parseComponentTargets([":aws_vpc.main"])).toThrow(/--target <component>:<address>/);
|
|
64
|
+
expect(() => parseComponentTargets(["net:"])).toThrow(/--target <component>:<address>/);
|
|
65
|
+
expect(() => parseComponentTargets(["nope:a"], new Set(["net"]))).toThrow(/no component is named nope/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("targetRefusal", () => {
|
|
70
|
+
test("accept, refuse with its reason, and unset refusing by name", () => {
|
|
71
|
+
expect(targetRefusal({ kind: "a", targets: "accept" })).toBeUndefined();
|
|
72
|
+
expect(targetRefusal({ kind: "b", targets: { refuse: "no" } })).toBe("no");
|
|
73
|
+
expect(targetRefusal({ kind: "c" })).toMatch(/^c does not say/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("deployContext", () => {
|
|
78
|
+
test("gives a component its own targets, sorted, and none to another", () => {
|
|
79
|
+
const options = { env: "dev", targets: { net: ["b", "a", "a"] } };
|
|
80
|
+
expect(deployContext(options, "net").targets).toEqual(["a", "b"]);
|
|
81
|
+
expect(deployContext(options, "app").targets).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("planning a targeted component", () => {
|
|
86
|
+
test("binds the targets into the member's digest and its change-set part", async () => {
|
|
87
|
+
const seen: DeployContext[] = [];
|
|
88
|
+
const ctx = deployContext({ env: "dev", targets: { net: ["aws_vpc.main"] } }, "net");
|
|
89
|
+
const planned = await planWaveComponent(component("net", "narrow"), registry(seen), ctx, {});
|
|
90
|
+
expect("error" in planned).toBe(false);
|
|
91
|
+
if ("error" in planned) return;
|
|
92
|
+
expect(seen[0]!.targets).toEqual(["aws_vpc.main"]);
|
|
93
|
+
const digest = targetedPlanDigest("sha256:base", ["aws_vpc.main"]);
|
|
94
|
+
expect(planned.members).toEqual([{ member: "net", planDigest: digest }]);
|
|
95
|
+
expect(planned.parts[0]!.member.planDigest).toBe(digest);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("another set of targets is another digest, and none is the plan's own", async () => {
|
|
99
|
+
const a = targetedPlanDigest("sha256:base", ["x"]);
|
|
100
|
+
const b = targetedPlanDigest("sha256:base", ["y"]);
|
|
101
|
+
expect(a).not.toBe(b);
|
|
102
|
+
expect(targetedPlanDigest("sha256:base", ["y", "x"])).toBe(targetedPlanDigest("sha256:base", ["x", "y"]));
|
|
103
|
+
const untargeted = await planWaveComponent(component("net", "narrow"), registry(), deployContext({ env: "dev" }, "net"), {});
|
|
104
|
+
expect("error" in untargeted ? undefined : untargeted.members[0]!.planDigest).toBe("sha256:base");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a verb that refuses names itself, and so does one that says nothing", async () => {
|
|
108
|
+
const ctx = deployContext({ env: "dev", targets: { net: ["a"] } }, "net");
|
|
109
|
+
const refused = await planWaveComponent(component("net", "narrow", "whole"), registry(), ctx, {});
|
|
110
|
+
expect(refused).toEqual({ component: "net", error: 'component "net": whole refuses targets: whole applies everything' });
|
|
111
|
+
const silent = await planWaveComponent(component("net", "silent"), registry(), ctx, {});
|
|
112
|
+
expect("error" in silent && silent.error).toMatch(/silent refuses targets: silent does not say/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a plan that does not give the targets back refuses", () => {
|
|
116
|
+
const ctx: DeployContext = { env: "dev", component: "net", targets: ["a", "b"] };
|
|
117
|
+
expect(() => bindTargets("narrow", ctx, { member: "net", planDigest: "d" })).toThrow(/names no targets/);
|
|
118
|
+
expect(() => bindTargets("narrow", ctx, { member: "net", planDigest: "d", targets: ["a"] })).toThrow(/names a$/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("running a targeted component", () => {
|
|
123
|
+
test("refuses before any step runs when one verb can't narrow", async () => {
|
|
124
|
+
const seen: DeployContext[] = [];
|
|
125
|
+
const ctx = deployContext({ env: "dev", targets: { net: ["a"] } }, "net");
|
|
126
|
+
const result = await runComponentDeploy(component("net", "narrow", "whole"), ctx, registry(seen), {}, undefined, { port: memoryGateLedgerPort() });
|
|
127
|
+
expect(result.status).toBe("fail");
|
|
128
|
+
expect(result.records[0]).toMatchObject({ phase: "Targets", kind: "whole", status: "fail" });
|
|
129
|
+
expect(seen).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("hands the targets to a verb that accepts them", async () => {
|
|
133
|
+
const seen: DeployContext[] = [];
|
|
134
|
+
const ctx = deployContext({ env: "dev", targets: { net: ["a"] } }, "net");
|
|
135
|
+
const result = await runComponentDeploy(component("net", "narrow"), ctx, registry(seen), {}, undefined, { port: memoryGateLedgerPort() });
|
|
136
|
+
expect(result.status).toBe("ok");
|
|
137
|
+
expect(seen[0]!.targets).toEqual(["a"]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("a gate over the whole set", () => {
|
|
142
|
+
test("binds the targets, so the untargeted approval does not cover a targeted run", async () => {
|
|
143
|
+
expect(fanOutGateDigest("sha256:set", undefined)).toBe("sha256:set");
|
|
144
|
+
expect(fanOutGateDigest("sha256:set", { net: [] })).toBe("sha256:set");
|
|
145
|
+
const narrowed = fanOutGateDigest("sha256:set", { net: ["a"] });
|
|
146
|
+
expect(narrowed).not.toBe("sha256:set");
|
|
147
|
+
expect(fanOutGateDigest("sha256:set", { net: ["b"] })).not.toBe(narrowed);
|
|
148
|
+
|
|
149
|
+
const estate = [component("net", "narrow")];
|
|
150
|
+
const plan = planFanOut({ components: estate, changed: ["net"] });
|
|
151
|
+
const gates = memoryGateLedgerPort();
|
|
152
|
+
const result = await runFanOut(plan, estate, registry(), {
|
|
153
|
+
env: "dev",
|
|
154
|
+
gates,
|
|
155
|
+
now: "2026-01-01T00:00:00Z",
|
|
156
|
+
gate: { op: "fan-out", gate: "set" },
|
|
157
|
+
targets: { net: ["a"] },
|
|
158
|
+
});
|
|
159
|
+
expect(result.status).toBe("gated");
|
|
160
|
+
expect(result.gate?.planDigest).toBe(fanOutGateDigest(plan.digest, { net: ["a"] }));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -72,6 +72,14 @@ export interface DeployContext {
|
|
|
72
72
|
* against missing values.
|
|
73
73
|
*/
|
|
74
74
|
unresolvedInputs?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* The addresses this component's steps narrow to (#4300), from
|
|
77
|
+
* `--target <component>:<address>`, sorted. A step's `plan` plans only
|
|
78
|
+
* these, and its `run` applies only these. Every capability step of the
|
|
79
|
+
* component must accept targets ({@link Capability.targets}), or the
|
|
80
|
+
* component refuses before anything plans or runs. Absent: the whole step.
|
|
81
|
+
*/
|
|
82
|
+
targets?: string[];
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -101,6 +109,14 @@ export interface CapabilityPlan {
|
|
|
101
109
|
* applies.
|
|
102
110
|
*/
|
|
103
111
|
outputs?: PlannedOutputs;
|
|
112
|
+
/**
|
|
113
|
+
* The targets this plan narrowed to (#4300), sorted. A plan made with
|
|
114
|
+
* `DeployContext.targets` gives them back, which says it planned only
|
|
115
|
+
* those. Planning binds them into the member's digest
|
|
116
|
+
* ({@link targetedPlanDigest} in ./wave-plan.ts), so an approval of a
|
|
117
|
+
* targeted plan covers that plan and no other.
|
|
118
|
+
*/
|
|
119
|
+
targets?: string[];
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
@@ -176,11 +192,44 @@ export interface Capability<In = unknown, Out = unknown> {
|
|
|
176
192
|
* compensation gap. See ../lint/rules/comp/comp003-mutating-no-rollback.ts.
|
|
177
193
|
*/
|
|
178
194
|
readonly rollbackPolicy?: RollbackPolicy;
|
|
195
|
+
/**
|
|
196
|
+
* Whether this verb narrows to `DeployContext.targets` (#4300). `"accept"`:
|
|
197
|
+
* its `plan` and `run` act on the targets alone, and its plan gives them
|
|
198
|
+
* back in {@link CapabilityPlan.targets}. `{ refuse }`: it can't, and says
|
|
199
|
+
* why. Unset refuses too, with a reason that names the verb.
|
|
200
|
+
*/
|
|
201
|
+
readonly targets?: TargetSupport;
|
|
179
202
|
}
|
|
180
203
|
|
|
181
204
|
/** A capability's relationship to rollback — see `Capability.rollbackPolicy`. */
|
|
182
205
|
export type RollbackPolicy = "native" | "none-by-design" | "needs-opt-out";
|
|
183
206
|
|
|
207
|
+
/** Whether a capability accepts `DeployContext.targets` (#4300). See `Capability.targets`. */
|
|
208
|
+
export type TargetSupport = "accept" | { refuse: string };
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A component was asked to narrow to targets (#4300) and one of its steps'
|
|
212
|
+
* capabilities refuses them. Nothing in the component plans or runs.
|
|
213
|
+
*/
|
|
214
|
+
export class TargetsRefusedError extends Error {
|
|
215
|
+
constructor(
|
|
216
|
+
readonly component: string,
|
|
217
|
+
readonly kind: string,
|
|
218
|
+
readonly reason: string,
|
|
219
|
+
) {
|
|
220
|
+
super(`component "${component}": ${kind} refuses targets: ${reason}`);
|
|
221
|
+
this.name = "TargetsRefusedError";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Why `capability` refuses targets, or `undefined` when it accepts them (#4300). */
|
|
226
|
+
export function targetRefusal(capability: Pick<Capability<never, unknown>, "kind" | "targets">): string | undefined {
|
|
227
|
+
const t = capability.targets;
|
|
228
|
+
if (t === "accept") return undefined;
|
|
229
|
+
if (t && typeof t === "object" && typeof t.refuse === "string") return t.refuse;
|
|
230
|
+
return `${capability.kind} does not say it can narrow to targets`;
|
|
231
|
+
}
|
|
232
|
+
|
|
184
233
|
/** Extract a capability's `In` type. */
|
|
185
234
|
export type CapabilityInput<C> = C extends Capability<infer In, unknown> ? In : never;
|
|
186
235
|
/** Extract a capability's `Out` type. */
|
|
@@ -257,3 +306,25 @@ export class CapabilityRegistry {
|
|
|
257
306
|
return [...this.capabilities.keys()].sort();
|
|
258
307
|
}
|
|
259
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* `--target <component>:<address>` values (#4300), grouped by component with
|
|
312
|
+
* each component's addresses sorted and unique. The address is everything
|
|
313
|
+
* after the first `:`, so it may hold colons of its own. Throws for a value
|
|
314
|
+
* with no component or no address, and, given `known`, for a component not
|
|
315
|
+
* among them.
|
|
316
|
+
*/
|
|
317
|
+
export function parseComponentTargets(values: readonly string[], known?: ReadonlySet<string>): Record<string, string[]> {
|
|
318
|
+
const out: Record<string, Set<string>> = {};
|
|
319
|
+
for (const value of values) {
|
|
320
|
+
const at = value.indexOf(":");
|
|
321
|
+
const component = at > 0 ? value.slice(0, at).trim() : "";
|
|
322
|
+
const address = at > 0 ? value.slice(at + 1).trim() : "";
|
|
323
|
+
if (component === "" || address === "") throw new Error(`--target needs a component and an address: --target <component>:<address>, got "${value}"`);
|
|
324
|
+
if (known && !known.has(component)) {
|
|
325
|
+
throw new Error(`--target ${value}: no component is named ${component} (components: ${[...known].sort().join(", ") || "none"})`);
|
|
326
|
+
}
|
|
327
|
+
(out[component] ??= new Set()).add(address);
|
|
328
|
+
}
|
|
329
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)).map(([c, s]) => [c, [...s].sort()]));
|
|
330
|
+
}
|
|
@@ -390,6 +390,12 @@ export function findComponentGate(component: DriverComponent): { gate: string }
|
|
|
390
390
|
export interface RunComponentsOptions {
|
|
391
391
|
/** Target environment name, threaded into every capability's `DeployContext.env` (default: "local"). */
|
|
392
392
|
env?: string;
|
|
393
|
+
/**
|
|
394
|
+
* The addresses each component's steps narrow to (#4300), by component
|
|
395
|
+
* (`--target <component>:<address>`). A name the run does not run refuses
|
|
396
|
+
* the run before anything starts.
|
|
397
|
+
*/
|
|
398
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
393
399
|
/**
|
|
394
400
|
* chant #1051 — opt-in: discover `*.component.ts` files in a sandboxed
|
|
395
401
|
* child process (`chant run --components <name|all> --sandbox`) instead of
|
|
@@ -664,6 +670,12 @@ export async function runComponents(
|
|
|
664
670
|
return { success: false, selected: [], error: resolved.error };
|
|
665
671
|
}
|
|
666
672
|
const targets = resolved.targets;
|
|
673
|
+
const narrowed = options.targets ?? {};
|
|
674
|
+
const outside = Object.keys(narrowed).filter((name) => !targets.some((c) => c.name === name));
|
|
675
|
+
if (outside.length > 0) {
|
|
676
|
+
return { success: false, selected: [], error: `--target names ${outside.join(", ")}, which this run does not run` };
|
|
677
|
+
}
|
|
678
|
+
const targeted = Object.keys(narrowed).length > 0 ? { targets: narrowed } : {};
|
|
667
679
|
|
|
668
680
|
// (#629) Resolve `chant.config.ts`'s `sbom`/`signing`/`vulnPolicy` sections
|
|
669
681
|
// and fill their defaults into every recognized step (`generate-sbom`,
|
|
@@ -712,6 +724,7 @@ export async function runComponents(
|
|
|
712
724
|
const run = await runInterpretDriver(resolvedTargets, registry, {
|
|
713
725
|
env,
|
|
714
726
|
releaseIdentity,
|
|
727
|
+
...targeted,
|
|
715
728
|
componentOutputs: seedOutputs,
|
|
716
729
|
...(emit ? { onRunEvent: emit } : {}),
|
|
717
730
|
...(gates ? { gates } : {}),
|
|
@@ -755,7 +768,7 @@ export async function runComponents(
|
|
|
755
768
|
emit?.({ type: "component-start", wave: 1, component: componentName });
|
|
756
769
|
const componentResult = await runComponentDeploy(
|
|
757
770
|
resolvedTargets[0],
|
|
758
|
-
deployContext({ env, releaseIdentity }, componentName),
|
|
771
|
+
deployContext({ env, releaseIdentity, ...targeted }, componentName),
|
|
759
772
|
registry,
|
|
760
773
|
componentOutputs,
|
|
761
774
|
undefined,
|
package/src/components/driver.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { componentPlanDigest } from "./gate-plan";
|
|
|
55
55
|
import { componentDeployInputs } from "./deploy-inputs";
|
|
56
56
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
57
57
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
58
|
-
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
|
|
58
|
+
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE, TargetsRefusedError, targetRefusal } from "./capability";
|
|
59
59
|
import { isPromotedArtifact } from "./auto-release";
|
|
60
60
|
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
61
61
|
import type { RunProgressEvent } from "./run-progress";
|
|
@@ -889,6 +889,18 @@ export async function runComponentDeploy(
|
|
|
889
889
|
onRunEvent?: RunEventSink,
|
|
890
890
|
): Promise<DriverComponentResult> {
|
|
891
891
|
const emit = joinRunEventSinks(onRunEvent, progressSinkOf(onProgress));
|
|
892
|
+
// #4300: a component narrowed to targets runs only when every step can narrow.
|
|
893
|
+
try {
|
|
894
|
+
assertTargetsAccepted(component, registry, ctx);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
if (!(err instanceof TargetsRefusedError)) throw err;
|
|
897
|
+
return {
|
|
898
|
+
component: component.name,
|
|
899
|
+
ok: false,
|
|
900
|
+
status: "fail",
|
|
901
|
+
records: [{ component: component.name, phase: "Targets", kind: err.kind, status: "fail", durationMs: 0, error: err.message }],
|
|
902
|
+
};
|
|
903
|
+
}
|
|
892
904
|
// Each component tracks its own release: a digest one component promoted never stamps another's deploy.
|
|
893
905
|
if (ctx.release) ctx = { ...ctx, release: { ...ctx.release } };
|
|
894
906
|
const phaseOutputs: Record<string, Record<string, unknown>> = {};
|
|
@@ -908,6 +920,7 @@ export async function runComponentDeploy(
|
|
|
908
920
|
component,
|
|
909
921
|
...(ctx.vars ? { vars: ctx.vars } : {}),
|
|
910
922
|
...(release !== undefined ? { release } : {}),
|
|
923
|
+
...(ctx.targets ? { targets: ctx.targets } : {}),
|
|
911
924
|
// #4155: the files the steps deploy from, read where the steps read
|
|
912
925
|
// them. Only a gate reads the digest, so a run with none skips the hashing.
|
|
913
926
|
...(hasGateStep(component) ? { inputs: componentDeployInputs(component.deploy, process.cwd()).inputs } : {}),
|
|
@@ -1121,6 +1134,8 @@ export interface InterpretRunOptions {
|
|
|
1121
1134
|
* step promotes an artifact. Absent: no deploy step sees a release.
|
|
1122
1135
|
*/
|
|
1123
1136
|
releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
|
|
1137
|
+
/** The addresses each component narrows to (#4300), by component: its `DeployContext.targets`. */
|
|
1138
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
1124
1139
|
}
|
|
1125
1140
|
|
|
1126
1141
|
/**
|
|
@@ -1144,11 +1159,47 @@ export interface InterpretRunOptions {
|
|
|
1144
1159
|
*/
|
|
1145
1160
|
/** A component's `DeployContext` for a run: its env, vars and, when the caller knows it, its release. */
|
|
1146
1161
|
export function deployContext(
|
|
1147
|
-
options: {
|
|
1162
|
+
options: {
|
|
1163
|
+
env: string;
|
|
1164
|
+
vars?: Record<string, unknown>;
|
|
1165
|
+
releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
|
|
1166
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
1167
|
+
},
|
|
1148
1168
|
component: string,
|
|
1149
1169
|
): DeployContext {
|
|
1150
1170
|
const release = options.releaseIdentity?.(component);
|
|
1151
|
-
|
|
1171
|
+
const targets = options.targets?.[component];
|
|
1172
|
+
return {
|
|
1173
|
+
env: options.env,
|
|
1174
|
+
component,
|
|
1175
|
+
...(options.vars ? { vars: options.vars } : {}),
|
|
1176
|
+
...(release ? { release: { ...release } } : {}),
|
|
1177
|
+
...(targets && targets.length > 0 ? { targets: [...new Set(targets)].sort() } : {}),
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Refuse a component asked to narrow to `ctx.targets` (#4300) when one of its
|
|
1183
|
+
* capability steps can't: throws {@link TargetsRefusedError} naming the first
|
|
1184
|
+
* such verb, in composition order. A step whose kind is not registered
|
|
1185
|
+
* refuses too. Does nothing when `ctx.targets` is absent.
|
|
1186
|
+
*/
|
|
1187
|
+
export function assertTargetsAccepted(component: DriverComponent, registry: CapabilityRegistry, ctx: DeployContext): void {
|
|
1188
|
+
if (!ctx.targets || ctx.targets.length === 0) return;
|
|
1189
|
+
const walk = (steps: DriverPhase["steps"]): void => {
|
|
1190
|
+
for (const step of steps) {
|
|
1191
|
+
if (typeof (step as DriverPhase).phase === "string" && Array.isArray((step as DriverPhase).steps)) {
|
|
1192
|
+
walk((step as DriverPhase).steps);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const kind = (step as { kind?: unknown }).kind;
|
|
1196
|
+
if (kind === "gate" || typeof kind !== "string") continue;
|
|
1197
|
+
if (!registry.has(kind)) throw new TargetsRefusedError(component.name, kind, `no capability is registered for ${kind}`);
|
|
1198
|
+
const reason = targetRefusal(registry.resolve(kind));
|
|
1199
|
+
if (reason !== undefined) throw new TargetsRefusedError(component.name, kind, reason);
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
for (const phase of component.deploy) walk(phase.steps);
|
|
1152
1203
|
}
|
|
1153
1204
|
|
|
1154
1205
|
export async function runInterpretDriver(
|
|
@@ -54,6 +54,7 @@ import type { CapabilityRegistry } from "./capability";
|
|
|
54
54
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
55
55
|
import type { DeployContext } from "./capability";
|
|
56
56
|
import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
|
|
57
|
+
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
57
58
|
import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
|
|
58
59
|
|
|
59
60
|
/** The sink a fan-out emits to: run events, and the older stream through them. */
|
|
@@ -187,6 +188,8 @@ export interface FanOutGate {
|
|
|
187
188
|
export interface FanOutRunOptions {
|
|
188
189
|
env: string;
|
|
189
190
|
vars?: Record<string, unknown>;
|
|
191
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
192
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
190
193
|
/** Outputs of components this run is not running: the ones the plan lists in `seeds`. */
|
|
191
194
|
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
192
195
|
onProgress?: (event: RunProgressEvent) => void;
|
|
@@ -319,7 +322,7 @@ export async function runFanOut(
|
|
|
319
322
|
evaluateGate(gates.port, {
|
|
320
323
|
op: gate.op,
|
|
321
324
|
gate: gate.gate,
|
|
322
|
-
planDigest: active.digest,
|
|
325
|
+
planDigest: fanOutGateDigest(active.digest, options.targets),
|
|
323
326
|
...(gate.description ? { description: gate.description } : {}),
|
|
324
327
|
...(gate.timeout ? { timeout: gate.timeout } : {}),
|
|
325
328
|
...(options.now ? { now: options.now } : {}),
|
|
@@ -455,6 +458,21 @@ export async function runFanOut(
|
|
|
455
458
|
};
|
|
456
459
|
}
|
|
457
460
|
|
|
461
|
+
/**
|
|
462
|
+
* The digest a gate over the whole set binds: the derivation's digest, and,
|
|
463
|
+
* when the run narrows components to targets (#4300), the digest over it and
|
|
464
|
+
* every component's sorted targets. An approval of the whole set then covers
|
|
465
|
+
* no narrowed run, and one narrowed run's approval covers no other.
|
|
466
|
+
*/
|
|
467
|
+
export function fanOutGateDigest(digest: string, targets: FanOutRunOptions["targets"]): string {
|
|
468
|
+
const narrowed = Object.entries(targets ?? {})
|
|
469
|
+
.filter(([, t]) => t.length > 0)
|
|
470
|
+
.map(([component, t]) => [component, [...new Set(t)].sort()] as const)
|
|
471
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
472
|
+
if (narrowed.length === 0) return digest;
|
|
473
|
+
return computePlanDigest("fan-out-targets", { plan: digest, targets: Object.fromEntries(narrowed) });
|
|
474
|
+
}
|
|
475
|
+
|
|
458
476
|
/** The run's `deployContext`, with the carried state and the carry callback (#3459) when the caller gave them. */
|
|
459
477
|
function fanOutContext(options: FanOutRunOptions, component: string): DeployContext {
|
|
460
478
|
return {
|
|
@@ -45,6 +45,8 @@ export interface ComponentGatePlan {
|
|
|
45
45
|
release?: string;
|
|
46
46
|
/** What the steps deploy from (`componentDeployInputs`), when the caller read it. */
|
|
47
47
|
inputs?: readonly DeployInputDigest[];
|
|
48
|
+
/** The addresses the component narrows to (#4300), sorted. A gate's approval then covers that narrowing alone. */
|
|
49
|
+
targets?: readonly string[];
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** The plan digest a component gate records and approves. */
|
|
@@ -58,5 +60,6 @@ export function componentPlanDigest(plan: ComponentGatePlan): string {
|
|
|
58
60
|
vars: plan.vars ?? null,
|
|
59
61
|
release: plan.release ?? null,
|
|
60
62
|
...(plan.inputs && plan.inputs.length > 0 ? { inputs: plan.inputs } : {}),
|
|
63
|
+
...(plan.targets && plan.targets.length > 0 ? { targets: [...plan.targets].sort() } : {}),
|
|
61
64
|
});
|
|
62
65
|
}
|
|
@@ -148,6 +148,16 @@ export interface PrLoopPipelineOptions {
|
|
|
148
148
|
* false, so a merge deploys nothing. `waveGate` needs it.
|
|
149
149
|
*/
|
|
150
150
|
applyOnMerge?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Apply an open pull request before it merges (#4301, ws-106): the apply
|
|
153
|
+
* job runs `pr-apply --before-merge` at the pull request's head, started
|
|
154
|
+
* by a `/chant apply` comment on GitHub and Forgejo, or on GitLab by a
|
|
155
|
+
* pipeline of the target branch with `CHANT_APPLY_PR` set. A merge still
|
|
156
|
+
* records. `pr-apply` refuses unless the declaration at base sets
|
|
157
|
+
* `ci.workflow: "apply-before-merge"`. It takes neither `applyOnMerge`,
|
|
158
|
+
* `waveGate` nor `build`.
|
|
159
|
+
*/
|
|
160
|
+
applyBeforeMerge?: boolean;
|
|
151
161
|
/**
|
|
152
162
|
* Stream each job's run events (#4007): `--events-json` on both commands,
|
|
153
163
|
* and `CHANT_RUN_EVENTS_DIR` set to {@link PR_LOOP_EVENTS_DIR}. Default false.
|
|
@@ -259,6 +269,10 @@ export function prLoopBuildLines(env: string, options: PrLoopPipelineOptions = {
|
|
|
259
269
|
*/
|
|
260
270
|
export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelineOptions = {}, member?: string): PrLoopJob[] {
|
|
261
271
|
const gate = options.gate ?? PR_APPLY_GATE;
|
|
272
|
+
const early = options.applyBeforeMerge === true;
|
|
273
|
+
if (early && options.applyOnMerge) throw new Error("--apply-before-merge applies an open pull request and a merge records, so it takes no --apply-on-merge; pass one of them");
|
|
274
|
+
if (early && options.waveGate) throw new Error("--apply-before-merge applies the pull request's plan in one job, so it takes no --wave-gate");
|
|
275
|
+
if (early && (options.build || options.buildCommand)) throw new Error("--apply-before-merge runs no build before its requires checks, so it takes no --pr-build or --pr-build-command yet");
|
|
262
276
|
const common = [
|
|
263
277
|
"--env", env, "--gate", gate, "--output", PR_LOOP_REPORT_DIR, "--forge", forge,
|
|
264
278
|
...(member ? ["--member", member] : []),
|
|
@@ -275,7 +289,8 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
275
289
|
// #4156: the flags `chant ci run` rebuilds `command` from. It finds the member from the directory it runs in.
|
|
276
290
|
const ciRun = (job: PrLoopJob["jobName"]): string[] => [
|
|
277
291
|
"chant", "ci", "run", job, "--in-place", ...(job === "plan" ? ["--no-push"] : []),
|
|
278
|
-
...(job === "apply" ? [] : ["--base", '"$BASE_SHA"']), ...(job === "plan" ? ["--pr", '"$PR_NUMBER"'] : []),
|
|
292
|
+
...(job === "apply" && !early ? [] : ["--base", '"$BASE_SHA"']), ...(job === "plan" || (job === "apply" && early) ? ["--pr", '"$PR_NUMBER"'] : []),
|
|
293
|
+
...(job === "apply" && early ? ["--apply-before-merge"] : []),
|
|
279
294
|
"--env", env, "--gate", gate, "--forge", forge,
|
|
280
295
|
...(job !== "record" && options.waveGate ? ["--wave-gate", options.waveGate] : []),
|
|
281
296
|
...(job !== "plan" && options.branch ? ["--branch", JSON.stringify(options.branch)] : []),
|
|
@@ -292,6 +307,37 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
292
307
|
: { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID", ...withoutForgeTokens(jobEnv) };
|
|
293
308
|
const greenBranch = github ? JSON.stringify(options.branch ?? "main") : options.branch ? JSON.stringify(options.branch) : "$CI_DEFAULT_BRANCH";
|
|
294
309
|
const applyEnv = github ? { BASE_SHA: "${{ github.event.before }}", ...jobEnv } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA", ...jobEnv };
|
|
310
|
+
// #4301: the apply before merge runs in a job of the target branch, whose commit is the base, at the pull request's head.
|
|
311
|
+
const earlyEnv = github
|
|
312
|
+
? { BASE_SHA: "${{ github.sha }}", PR_NUMBER: "${{ github.event.issue.number }}", ...jobEnv }
|
|
313
|
+
: { BASE_SHA: "$CI_COMMIT_SHA", PR_NUMBER: `$${GITLAB_APPLY_PR_VAR}`, ...jobEnv };
|
|
314
|
+
const greenApply: PrLoopJob = {
|
|
315
|
+
// #4168 step 9: the apply applies the newest green commit, measured from the one applied before it.
|
|
316
|
+
jobName: "apply",
|
|
317
|
+
command: [
|
|
318
|
+
"chant", "components", "pr-apply", "--green", ...common,
|
|
319
|
+
...(options.requireReview === true ? ["--require-review"] : []),
|
|
320
|
+
...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
|
|
321
|
+
"--resume", PR_APPLY_RECORD,
|
|
322
|
+
].join(" "),
|
|
323
|
+
env: jobEnv,
|
|
324
|
+
checkout: [greenCheckout(greenBranch)],
|
|
325
|
+
greenBranch,
|
|
326
|
+
...build,
|
|
327
|
+
run: { command: ciRun("apply").join(" "), env: runEnv(jobEnv) },
|
|
328
|
+
};
|
|
329
|
+
const earlyApply: PrLoopJob = {
|
|
330
|
+
// #4301: the requires checks run inside pr-apply, before any of the head's code loads.
|
|
331
|
+
jobName: "apply",
|
|
332
|
+
command: [
|
|
333
|
+
"chant", "components", "pr-apply", "--before-merge", "--base", '"$BASE_SHA"', "--pr", '"$PR_NUMBER"', ...common,
|
|
334
|
+
...(options.requireReview === true ? ["--require-review"] : []),
|
|
335
|
+
"--resume", PR_APPLY_RECORD,
|
|
336
|
+
].join(" "),
|
|
337
|
+
env: earlyEnv,
|
|
338
|
+
checkout: [pullRequestCheckout(forge)],
|
|
339
|
+
run: { command: ciRun("apply").join(" "), env: runEnv(earlyEnv) },
|
|
340
|
+
};
|
|
295
341
|
return [
|
|
296
342
|
{
|
|
297
343
|
jobName: "plan",
|
|
@@ -301,21 +347,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
301
347
|
...build,
|
|
302
348
|
run: { command: ciRun("plan").join(" "), env: runEnv(planEnv) },
|
|
303
349
|
},
|
|
304
|
-
|
|
305
|
-
// #4168 step 9: the apply applies the newest green commit, measured from the one applied before it.
|
|
306
|
-
jobName: "apply",
|
|
307
|
-
command: [
|
|
308
|
-
"chant", "components", "pr-apply", "--green", ...common,
|
|
309
|
-
...(options.requireReview === true ? ["--require-review"] : []),
|
|
310
|
-
...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
|
|
311
|
-
"--resume", PR_APPLY_RECORD,
|
|
312
|
-
].join(" "),
|
|
313
|
-
env: jobEnv,
|
|
314
|
-
checkout: [greenCheckout(greenBranch)],
|
|
315
|
-
greenBranch,
|
|
316
|
-
...build,
|
|
317
|
-
run: { command: ciRun("apply").join(" "), env: runEnv(jobEnv) },
|
|
318
|
-
},
|
|
350
|
+
early ? earlyApply : greenApply,
|
|
319
351
|
{
|
|
320
352
|
jobName: "record",
|
|
321
353
|
command: ["chant", "components", "pr-record", "--base", '"$BASE_SHA"', ...common].join(" "),
|
|
@@ -460,6 +492,22 @@ function pushBaseFallback(target: string, commit: string, diffBase?: string): st
|
|
|
460
492
|
);
|
|
461
493
|
}
|
|
462
494
|
|
|
495
|
+
/** The GitLab variable a person sets on a pipeline of the target branch to apply a merge request before it merges (#4301). */
|
|
496
|
+
export const GITLAB_APPLY_PR_VAR = "CHANT_APPLY_PR";
|
|
497
|
+
|
|
498
|
+
/** The comment that starts the apply before merge on GitHub and Forgejo (#4301). */
|
|
499
|
+
export const APPLY_COMMENT = "/chant apply";
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The line that checks out pull request `$PR_NUMBER`'s head (#4301) in a job
|
|
503
|
+
* of the target branch: the forge's `refs/pull/<n>/head`, or GitLab's
|
|
504
|
+
* `refs/merge-requests/<n>/head`. A number that is not one fails the job.
|
|
505
|
+
*/
|
|
506
|
+
export function pullRequestCheckout(forge: ForgeKind): string {
|
|
507
|
+
const ref = forge === "gitlab" ? 'refs/merge-requests/$PR_NUMBER/head' : 'refs/pull/$PR_NUMBER/head';
|
|
508
|
+
return `echo "$PR_NUMBER" | grep -Eq '^[0-9]+$' && git fetch --quiet origin "+${ref}:refs/remotes/origin/chant-apply/$PR_NUMBER" && git checkout --quiet --detach "refs/remotes/origin/chant-apply/$PR_NUMBER"`;
|
|
509
|
+
}
|
|
510
|
+
|
|
463
511
|
/**
|
|
464
512
|
* The line that checks out the newest green commit of `branch` (#4168 step
|
|
465
513
|
* 9), after fetching the branch and its tags, for the apply job. `chant ci
|
package/src/components/pr-run.ts
CHANGED
|
@@ -110,6 +110,8 @@ export interface PrPlanSetOptions {
|
|
|
110
110
|
vars?: Record<string, unknown>;
|
|
111
111
|
/** Outputs supplied by the caller (`--seed-outputs`). They win over outputs read here. */
|
|
112
112
|
seededOutputs?: Record<string, Record<string, unknown>>;
|
|
113
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
114
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
113
115
|
/**
|
|
114
116
|
* Where the planning's events go (#4041): a `plan` phase with no
|
|
115
117
|
* component, and in it one `plan` step per component, started as its wave
|
|
@@ -384,6 +386,8 @@ export interface PrApplyOptions {
|
|
|
384
386
|
vars?: Record<string, unknown>;
|
|
385
387
|
/** The set {@link planPrSet} planned and the gate approved, or {@link resumePrSet}'s set. */
|
|
386
388
|
set: PrPlanSet;
|
|
389
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
390
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
387
391
|
onProgress?: (event: RunProgressEvent) => void;
|
|
388
392
|
/** Where the apply's events go (#4041): the fan-out's run events. */
|
|
389
393
|
onRunEvent?: RunEventSink;
|
|
@@ -467,6 +471,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
|
|
|
467
471
|
env: options.env,
|
|
468
472
|
...(options.vars ? { vars: options.vars } : {}),
|
|
469
473
|
plans,
|
|
474
|
+
...(options.targets ? { targets: options.targets } : {}),
|
|
470
475
|
componentOutputs: outputs,
|
|
471
476
|
hold: (name, now) => heldReason(byName.get(name)!, read, now, inChange),
|
|
472
477
|
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
@@ -491,6 +496,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
|
|
|
491
496
|
env: options.env,
|
|
492
497
|
...(options.vars ? { vars: options.vars } : {}),
|
|
493
498
|
seededOutputs: run.componentOutputs,
|
|
499
|
+
...(options.targets ? { targets: options.targets } : {}),
|
|
494
500
|
});
|
|
495
501
|
for (const name of plan.order) roundOf.set(name, { round, set });
|
|
496
502
|
const record = (status: PrRound["status"], approval: PrApproval, message?: string): void => {
|
|
@@ -626,6 +632,8 @@ export interface PrResumeOptions {
|
|
|
626
632
|
env: string;
|
|
627
633
|
vars?: Record<string, unknown>;
|
|
628
634
|
seededOutputs?: Record<string, Record<string, unknown>>;
|
|
635
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
636
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
629
637
|
/** The approved set, from the attempt record. */
|
|
630
638
|
record: PrApplyRecord;
|
|
631
639
|
/** Components an earlier attempt applied. */
|
|
@@ -677,6 +685,7 @@ export async function resumePrSet(options: PrResumeOptions): Promise<PrResume> {
|
|
|
677
685
|
env: options.env,
|
|
678
686
|
...(options.vars ? { vars: options.vars } : {}),
|
|
679
687
|
seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs },
|
|
688
|
+
...(options.targets ? { targets: options.targets } : {}),
|
|
680
689
|
});
|
|
681
690
|
const reasons = fresh.doc.members
|
|
682
691
|
.map((m) => planCoveredBy(options.record.changeSet, fresh.doc, m.member))
|