@intentius/chant 0.124.43 → 0.124.45
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/ci-identity.d.ts +183 -12
- package/dist/ci-identity.d.ts.map +1 -1
- package/dist/ci-identity.js +362 -53
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +17 -1
- package/dist/cli/handlers/pr-env.d.ts +8 -2
- package/dist/cli/handlers/pr-env.d.ts.map +1 -1
- package/dist/cli/handlers/pr-env.js +126 -225
- package/dist/cli/handlers/pr.d.ts +3 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +1 -0
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +4 -2
- package/dist/components/fan-out-support.d.ts +3 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out-support.js +1 -1
- package/dist/components/pr-env-run.d.ts +270 -0
- package/dist/components/pr-env-run.d.ts.map +1 -0
- package/dist/components/pr-env-run.js +440 -0
- package/dist/components/pr-environment.d.ts +89 -14
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +94 -8
- package/dist/components/pr-pipeline.d.ts +10 -1
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/op/op-waves.d.ts +17 -0
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +23 -1
- package/dist/pr-forge.d.ts +15 -1
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +28 -16
- package/dist/pr-requires.d.ts +5 -1
- package/dist/pr-requires.d.ts.map +1 -1
- package/dist/wave-runtime.mjs +179 -179
- package/dist/workspace/ci-cloud-identity.d.ts +3 -1
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
- package/dist/workspace/ci-cloud-identity.js +16 -1
- package/dist/workspace/ci-run.d.ts +35 -8
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +95 -21
- package/dist/workspace/ci-secrets.d.ts.map +1 -1
- package/dist/workspace/ci-secrets.js +4 -3
- package/dist/workspace/declaration.d.ts +10 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +17 -1
- package/dist/workspace/declaration.schema.json +36 -1
- package/package.json +1 -1
- package/src/ci-identity-targets.test.ts +275 -0
- package/src/ci-identity.ts +394 -61
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/pr-env.ts +136 -227
- package/src/cli/handlers/pr.ts +4 -0
- package/src/components/cli-support.ts +6 -1
- package/src/components/fan-out-support.ts +4 -1
- package/src/components/pr-env-run.test.ts +290 -0
- package/src/components/pr-env-run.ts +634 -0
- package/src/components/pr-environment.ts +159 -12
- package/src/components/pr-pipeline.ts +10 -1
- package/src/op/op-waves.ts +38 -1
- package/src/pr-forge-bundle.test.ts +1 -1
- package/src/pr-forge.ts +44 -17
- package/src/pr-requires.test.ts +43 -6
- package/src/pr-requires.ts +5 -1
- package/src/workspace/ci-cloud-identity.test.ts +20 -0
- package/src/workspace/ci-cloud-identity.ts +14 -1
- package/src/workspace/ci-run.test.ts +74 -1
- package/src/workspace/ci-run.ts +122 -29
- package/src/workspace/ci-secrets.test.ts +1 -1
- package/src/workspace/ci-secrets.ts +4 -3
- package/src/workspace/declaration.schema.json +36 -1
- package/src/workspace/declaration.test.ts +7 -0
- package/src/workspace/declaration.ts +24 -2
|
@@ -25,8 +25,14 @@
|
|
|
25
25
|
* `down` reads the environment's record on chant/lifecycle, checks it names
|
|
26
26
|
* this pull request's environment, and tears down what `planTeardown` finds
|
|
27
27
|
* under the record's marker, after dropping any entry whose marker is not
|
|
28
|
-
* exactly that one.
|
|
29
|
-
*
|
|
28
|
+
* exactly that one. The lexicons tear down what they own, and the command
|
|
29
|
+
* `ci.prEnvironment.teardown` names tears down the rest (#4330). No gate
|
|
30
|
+
* holds it: closing the pull request is the decision, as is the TTL the base
|
|
31
|
+
* set for `sweep`.
|
|
32
|
+
*
|
|
33
|
+
* The runs themselves are ../../components/pr-env-run.ts, which a product
|
|
34
|
+
* calls with an adapter of its own (#4330). This file supplies the
|
|
35
|
+
* components adapter and the CLI's ports.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -37,39 +43,41 @@ import { isEmptyChangeSet } from "../../change-set";
|
|
|
37
43
|
import {
|
|
38
44
|
PR_ENV_EXIT,
|
|
39
45
|
PR_ENV_GATE,
|
|
40
|
-
PR_ENV_LEDGER_DIR,
|
|
41
|
-
PR_ENV_LEDGER_FILE,
|
|
42
46
|
PR_ENV_RECORD_FILE,
|
|
43
47
|
PR_ENV_REPORT_DIR,
|
|
44
48
|
acceptHandedRecord,
|
|
45
49
|
handedPrEnvReport,
|
|
46
50
|
PR_ENV_TEMPLATE_VAR,
|
|
47
51
|
derivePrEnvironment,
|
|
48
|
-
expired,
|
|
49
|
-
livePrEnvironments,
|
|
50
|
-
parsePrEnvRecords,
|
|
51
52
|
prEnvApproveCommand,
|
|
52
|
-
prEnvNoteMarker,
|
|
53
|
-
prEnvStatusContext,
|
|
54
|
-
prEnvStatusDescription,
|
|
55
53
|
prEnvStatusState,
|
|
56
54
|
prEnvironmentName,
|
|
55
|
+
prEnvironmentSite,
|
|
57
56
|
renderPrEnvNote,
|
|
58
|
-
teardownComplete,
|
|
59
|
-
teardownCounts,
|
|
60
|
-
tearDownPrEnvironment,
|
|
61
57
|
ttlMs,
|
|
62
|
-
type LivePrEnvironment,
|
|
63
|
-
type PrEnvRecord,
|
|
64
58
|
type PrEnvReport,
|
|
59
|
+
type PrEnvReportRow,
|
|
60
|
+
type PrEnvTeardownAdapter,
|
|
65
61
|
} from "../../components/pr-environment";
|
|
66
|
-
import {
|
|
67
|
-
|
|
62
|
+
import {
|
|
63
|
+
commandTeardownAdapter,
|
|
64
|
+
forgePrEnvPublisher,
|
|
65
|
+
lifecyclePrEnvPorts,
|
|
66
|
+
prEnvDown as prEnvDownRun,
|
|
67
|
+
prEnvOp,
|
|
68
|
+
prEnvSweep as prEnvSweepRun,
|
|
69
|
+
prEnvUp as prEnvUpRun,
|
|
70
|
+
type PrEnvAdapter,
|
|
71
|
+
type PrEnvPorts,
|
|
72
|
+
type PrEnvTarget,
|
|
73
|
+
} from "../../components/pr-env-run";
|
|
74
|
+
import { applyPrSet, type PrPlanSet } from "../../components/pr-run";
|
|
68
75
|
import { prOp } from "../../pr-loop";
|
|
69
76
|
import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
70
77
|
import { formatError, formatInfo, formatWarning } from "../format";
|
|
71
78
|
import { withComponentEndpoint } from "../component-endpoint";
|
|
72
79
|
import type { CommandContext } from "../registry";
|
|
80
|
+
import type { Prepared } from "./pr";
|
|
73
81
|
|
|
74
82
|
const USAGE = "chant components pr-env up --base <ref> --pr <n> [--no-push] | post --pr <n> --head <sha> | down --pr <n> [--reason closed|expired] | sweep";
|
|
75
83
|
|
|
@@ -107,36 +115,30 @@ function memberOf(ctx: CommandContext): string | undefined {
|
|
|
107
115
|
return ctx.args.members?.length === 1 ? ctx.args.members[0] : undefined;
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
// ── the
|
|
118
|
+
// ── the ports ──────────────────────────────────────────────────────────────
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await fetchLifecycle().catch(() => false);
|
|
115
|
-
return parsePrEnvRecords(await readBlobFromPath(PR_ENV_LEDGER_DIR, PR_ENV_LEDGER_FILE));
|
|
116
|
-
}
|
|
120
|
+
const log = (line: string): void => console.error(formatInfo(line));
|
|
121
|
+
const warn = (message: string): void => console.error(formatWarning({ message }));
|
|
117
122
|
|
|
118
123
|
/**
|
|
119
|
-
*
|
|
120
|
-
* the
|
|
121
|
-
*
|
|
122
|
-
*
|
|
124
|
+
* The ports `pr-env` runs with (../../components/pr-env-run.ts): the records
|
|
125
|
+
* and the gate ledger on chant/lifecycle here, and the report written under
|
|
126
|
+
* `--output` and posted through `forge`. With `--no-push` (#4295) the record
|
|
127
|
+
* is written to `pr-env-record.json` under `--output` instead, for `pr-env
|
|
128
|
+
* post` to append from a job that runs none of the change's code.
|
|
123
129
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (pushed.pushWarning) console.error(formatWarning({ message: pushed.pushWarning }));
|
|
137
|
-
} catch (err) {
|
|
138
|
-
console.error(formatWarning({ message: `could not record ${record.env}'s ${record.kind} on chant/lifecycle: ${err instanceof Error ? err.message : String(err)}` }));
|
|
139
|
-
}
|
|
130
|
+
function cliPorts(ctx: CommandContext, forge: PrForge | undefined): PrEnvPorts {
|
|
131
|
+
const lifecycle = lifecyclePrEnvPorts({ ...(forge ? { forge } : {}), log, warn });
|
|
132
|
+
return {
|
|
133
|
+
...lifecycle,
|
|
134
|
+
async appendRecord(record) {
|
|
135
|
+
if (!ctx.args.noPush) return lifecycle.appendRecord(record);
|
|
136
|
+
const dir = resolve(ctx.args.output ?? PR_ENV_REPORT_DIR);
|
|
137
|
+
mkdirSync(dir, { recursive: true });
|
|
138
|
+
writeFileSync(join(dir, PR_ENV_RECORD_FILE), JSON.stringify(record, null, 2) + "\n");
|
|
139
|
+
},
|
|
140
|
+
publish: (report) => finish(ctx, forge, report),
|
|
141
|
+
};
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
// ── the report and the note ────────────────────────────────────────────────
|
|
@@ -148,24 +150,7 @@ async function finish(ctx: CommandContext, forge: PrForge | undefined, report: P
|
|
|
148
150
|
writeFileSync(join(dir, `pr-env-${report.stage}.json`), JSON.stringify(report, null, 2) + "\n");
|
|
149
151
|
writeFileSync(join(dir, "pr-env-note.md"), note);
|
|
150
152
|
if (ctx.args.json) console.log(JSON.stringify(report, null, 2));
|
|
151
|
-
if (
|
|
152
|
-
if (report.head) {
|
|
153
|
-
try {
|
|
154
|
-
await forge.setStatus(report.head, {
|
|
155
|
-
context: prEnvStatusContext(report.member),
|
|
156
|
-
state: prEnvStatusState(report),
|
|
157
|
-
description: prEnvStatusDescription(report),
|
|
158
|
-
...(forge.runUrl ? { url: forge.runUrl } : {}),
|
|
159
|
-
});
|
|
160
|
-
} catch (err) {
|
|
161
|
-
console.error(formatWarning({ message: `could not set the ${prEnvStatusContext(report.member)} status: ${err instanceof Error ? err.message : String(err)}` }));
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
await forge.upsertNote(report.pr, prEnvNoteMarker(report.env, report.member), note);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
console.error(formatWarning({ message: `could not post the note on #${report.pr}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
168
|
-
}
|
|
153
|
+
if (forge) await forgePrEnvPublisher(forge, warn)(report);
|
|
169
154
|
}
|
|
170
155
|
|
|
171
156
|
// ── up ─────────────────────────────────────────────────────────────────────
|
|
@@ -190,7 +175,6 @@ async function prEnvUp(ctx: CommandContext): Promise<number> {
|
|
|
190
175
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
191
176
|
const derived = derivePrEnvironment(config, setting.template, pr);
|
|
192
177
|
if ("refusal" in derived) return fail(derived.refusal);
|
|
193
|
-
const ttl = ttlMs(setting.ttl) ?? ttlMs("72h")!;
|
|
194
178
|
|
|
195
179
|
// The build sees pr-<n> as its environment and the template's name beside it.
|
|
196
180
|
process.env[ENV_VAR] = derived.env;
|
|
@@ -200,111 +184,76 @@ async function prEnvUp(ctx: CommandContext): Promise<number> {
|
|
|
200
184
|
args: { ...args, env: derived.env, param: [...(args.param ?? []).filter((p) => !p.startsWith(`${derived.param}=`)), `${derived.param}=${derived.env}`] },
|
|
201
185
|
};
|
|
202
186
|
console.error(formatInfo(`pr-env up: ${derived.env}, derived from ${derived.template}${derived.endpoint ? ` at ${derived.endpoint}` : ""}, marker ${derived.stack}/${derived.env}`));
|
|
203
|
-
return withComponentEndpoint(envCtx, () => up(envCtx, pr, derived, ttl), { config: { ...config, environments: derived.environments }, environment: derived.env });
|
|
187
|
+
return withComponentEndpoint(envCtx, () => up(envCtx, pr, derived, setting.ttl), { config: { ...config, environments: derived.environments }, environment: derived.env });
|
|
204
188
|
}
|
|
205
189
|
|
|
206
|
-
async function up(ctx: CommandContext, pr: number, derived: Exclude<ReturnType<typeof derivePrEnvironment>, { refusal: string }>, ttl:
|
|
190
|
+
async function up(ctx: CommandContext, pr: number, derived: Exclude<ReturnType<typeof derivePrEnvironment>, { refusal: string }>, ttl: string): Promise<number> {
|
|
207
191
|
const { prepare, planAll, placeHooks, runStageHook } = await import("./pr");
|
|
208
192
|
const p = await prepare(ctx, "plan");
|
|
209
193
|
if (typeof p === "number") return p;
|
|
210
|
-
const forge = p.forge;
|
|
211
|
-
const op = prOp(pr, p.member);
|
|
212
194
|
// ci.hooks (#4288), read at base as the other pull-request jobs read them, around the plan and the apply.
|
|
213
|
-
placeHooks(ctx, p, "apply",
|
|
214
|
-
const
|
|
215
|
-
stage: "up",
|
|
195
|
+
placeHooks(ctx, p, "apply", prEnvOp(pr, p.member));
|
|
196
|
+
const result = await prEnvUpRun({
|
|
216
197
|
pr,
|
|
217
|
-
env: derived.env,
|
|
218
|
-
...(p.member ? { member: p.member } : {}),
|
|
219
|
-
template: derived.template,
|
|
220
198
|
head: p.head,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
199
|
+
template: derived.template,
|
|
200
|
+
ttl,
|
|
201
|
+
stack: derived.stack,
|
|
202
|
+
...(p.member ? { member: p.member } : {}),
|
|
225
203
|
...(derived.endpoint ? { endpoint: derived.endpoint } : {}),
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
204
|
+
adapter: componentsPrEnvAdapter(p, { planAll, runStageHook }),
|
|
205
|
+
ports: cliPorts(ctx, p.forge),
|
|
206
|
+
actor: actor(),
|
|
207
|
+
...(p.forge?.runUrl ? { runUrl: p.forge.runUrl } : {}),
|
|
208
|
+
});
|
|
209
|
+
if (result.report.status === "waiting" || result.report.status === "changed") console.error(formatInfo(`approve : ${prEnvApproveCommand(result.report.op, result.report.digest)}`));
|
|
210
|
+
return result.exit;
|
|
211
|
+
}
|
|
233
212
|
|
|
234
|
-
|
|
235
|
-
|
|
213
|
+
/**
|
|
214
|
+
* The components as a pull request environment's adapter: the plan is
|
|
215
|
+
* `pr-plan`'s, with the plan-side hooks around it, and the apply is the
|
|
216
|
+
* fan-out's, with the apply-side hooks around it. Each row carries the
|
|
217
|
+
* component's site in `pr-<n>`, carried over from the template's (#4330,
|
|
218
|
+
* `prEnvironmentSite`).
|
|
219
|
+
*/
|
|
220
|
+
function componentsPrEnvAdapter(p: Prepared, run: { planAll: typeof import("./pr").planAll; runStageHook: typeof import("./pr").runStageHook }): PrEnvAdapter {
|
|
221
|
+
let set: PrPlanSet | undefined;
|
|
222
|
+
const rows = (target: PrEnvTarget, status: (component: string) => string): PrEnvReportRow[] =>
|
|
236
223
|
p.plan.order.map((component) => {
|
|
237
|
-
const ms = set
|
|
224
|
+
const ms = (set?.members ?? []).filter((m) => m.component === component);
|
|
238
225
|
const sum = (k: "create" | "update" | "replace" | "delete") => ms.reduce((n, m) => n + m.counts[k], 0);
|
|
239
|
-
|
|
226
|
+
const found = prEnvironmentSite(p.sites?.[component], target.template, target.env);
|
|
227
|
+
const site = found ? { ...(found.runtime ? { runtime: found.runtime } : {}), ...(found.url ? { url: found.url } : {}), ...(found.domain ? { domain: found.domain } : {}) } : undefined;
|
|
228
|
+
return { component, status: status(component), create: sum("create"), update: sum("update"), replace: sum("replace"), delete: sum("delete"), ...(site && Object.keys(site).length > 0 ? { site } : {}) };
|
|
240
229
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
await finish(ctx, forge, { ...base, digest, status: "failed", message: hookStop, components: rows(() => "planned"), ...(approvedBy ? { approvedBy } : {}) });
|
|
269
|
-
return PR_ENV_EXIT.failed;
|
|
270
|
-
}
|
|
271
|
-
const applied = await applyPrSet({ components: p.components, plan: p.plan, registry: p.registry, env: derived.env, set, gate: PR_ENV_GATE });
|
|
272
|
-
const afterHook = runStageHook(p, "after-apply", p.plan.order, set, "the environment stands as applied");
|
|
273
|
-
const status = (component: string) => applied.members.find((m) => m.component === component)?.status ?? "planned";
|
|
274
|
-
const ok = applied.status === "applied";
|
|
275
|
-
const now = new Date();
|
|
276
|
-
const expiresAt = new Date(now.getTime() + ttl).toISOString();
|
|
277
|
-
// A copy that failed part way is live too: what applied is torn down on close or at expiry like the rest.
|
|
278
|
-
await appendRecord(ctx, {
|
|
279
|
-
version: 1,
|
|
280
|
-
kind: "up",
|
|
281
|
-
pr,
|
|
282
|
-
env: derived.env,
|
|
283
|
-
...(p.member ? { member: p.member } : {}),
|
|
284
|
-
template: derived.template,
|
|
285
|
-
marker: { stack: derived.stack, env: derived.env },
|
|
286
|
-
commit: p.head,
|
|
287
|
-
components: p.plan.order,
|
|
288
|
-
planDigest: digest,
|
|
289
|
-
result: ok ? "applied" : "failed",
|
|
290
|
-
by: actor(),
|
|
291
|
-
timestamp: now.toISOString(),
|
|
292
|
-
expiresAt,
|
|
293
|
-
...(approvedBy ? { approvedBy } : {}),
|
|
294
|
-
releaseTarget: false,
|
|
295
|
-
...(forge?.runUrl ? { runUrl: forge.runUrl } : {}),
|
|
296
|
-
});
|
|
297
|
-
await finish(ctx, forge, {
|
|
298
|
-
...base,
|
|
299
|
-
digest,
|
|
300
|
-
status: ok ? "applied" : "failed",
|
|
301
|
-
components: rows(status),
|
|
302
|
-
...(approvedBy ? { approvedBy } : {}),
|
|
303
|
-
expiresAt,
|
|
304
|
-
...(ok ? (afterHook ? { message: afterHook } : {}) : { message: applied.status === "waiting" ? "A component read an upstream output that moved when it applied; run the job again to plan it against the new value." : "A component failed to apply; what applied stays in the environment and is torn down with it." }),
|
|
305
|
-
});
|
|
306
|
-
console.error((ok ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-env up: ${derived.env} ${ok ? "applied" : applied.status}; it expires ${expiresAt}`));
|
|
307
|
-
return ok && !afterHook ? PR_ENV_EXIT.done : PR_ENV_EXIT.failed;
|
|
230
|
+
return {
|
|
231
|
+
async plan(target) {
|
|
232
|
+
if (p.plan.order.length === 0) return { digest: "", empty: true, rows: [], ...(p.routedAway ? { message: p.routedAway } : {}) };
|
|
233
|
+
set = await run.planAll(p);
|
|
234
|
+
const planned = set;
|
|
235
|
+
return {
|
|
236
|
+
digest: planned.doc.digest,
|
|
237
|
+
empty: isEmptyChangeSet(planned.doc),
|
|
238
|
+
...(planned.failed ? { failed: true } : {}),
|
|
239
|
+
rows: rows(target, (c) => (planned.failed && planned.members.some((m) => m.component === c && m.status === "plan-failed") ? "plan failed" : "planned")),
|
|
240
|
+
...(p.hookFailed ? { message: p.hookFailed } : {}),
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
async apply(target) {
|
|
244
|
+
const planned = set!;
|
|
245
|
+
const hookStop = run.runStageHook(p, "before-apply", p.plan.order, planned);
|
|
246
|
+
if (hookStop) return { status: "stopped", rows: rows(target, () => "planned"), message: hookStop };
|
|
247
|
+
const applied = await applyPrSet({ components: p.components, plan: p.plan, registry: p.registry, env: target.env, set: planned, gate: PR_ENV_GATE });
|
|
248
|
+
const afterHook = run.runStageHook(p, "after-apply", p.plan.order, planned, "the environment stands as applied");
|
|
249
|
+
const status = (component: string) => applied.members.find((m) => m.component === component)?.status ?? "planned";
|
|
250
|
+
return {
|
|
251
|
+
status: applied.status === "applied" ? "applied" : applied.status === "waiting" ? "waiting" : "failed",
|
|
252
|
+
rows: rows(target, status),
|
|
253
|
+
...(afterHook ? { afterFailure: afterHook } : {}),
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
};
|
|
308
257
|
}
|
|
309
258
|
|
|
310
259
|
// ── post ───────────────────────────────────────────────────────────────────
|
|
@@ -352,7 +301,7 @@ async function prEnvPost(ctx: CommandContext): Promise<number> {
|
|
|
352
301
|
report.message = `${report.message ? `${report.message} ` : ""}${accepted.refusal}`;
|
|
353
302
|
code = PR_ENV_EXIT.failed;
|
|
354
303
|
} else {
|
|
355
|
-
await
|
|
304
|
+
await lifecyclePrEnvPorts({ warn }).appendRecord(accepted.record);
|
|
356
305
|
if (accepted.record.expiresAt) report.expiresAt = accepted.record.expiresAt;
|
|
357
306
|
}
|
|
358
307
|
}
|
|
@@ -363,6 +312,19 @@ async function prEnvPost(ctx: CommandContext): Promise<number> {
|
|
|
363
312
|
|
|
364
313
|
// ── down ───────────────────────────────────────────────────────────────────
|
|
365
314
|
|
|
315
|
+
/**
|
|
316
|
+
* What tears the environment down: the lexicons, and the command
|
|
317
|
+
* `ci.prEnvironment.teardown` names in the declaration at this commit
|
|
318
|
+
* (#4330), which the down and sweep jobs check out from the base branch.
|
|
319
|
+
*/
|
|
320
|
+
async function teardownSources(): Promise<{ config: ChantConfig; adapters: PrEnvTeardownAdapter[] }> {
|
|
321
|
+
const projectPath = resolve(".");
|
|
322
|
+
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
323
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
324
|
+
const hook = scopeSource(projectPath, "HEAD").declaration?.ci?.prEnvironment?.teardown;
|
|
325
|
+
return { config, adapters: hook ? [commandTeardownAdapter(hook, { cwd: projectPath })] : [] };
|
|
326
|
+
}
|
|
327
|
+
|
|
366
328
|
async function prEnvDown(ctx: CommandContext): Promise<number> {
|
|
367
329
|
const { args } = ctx;
|
|
368
330
|
if (args.pr === undefined) return fail("pr-env down needs the pull or merge request: --pr <n>");
|
|
@@ -370,57 +332,20 @@ async function prEnvDown(ctx: CommandContext): Promise<number> {
|
|
|
370
332
|
if (reason !== "closed" && reason !== "expired") return fail(`--reason ${reason} is not closed or expired`);
|
|
371
333
|
const forge = forgeOf(ctx);
|
|
372
334
|
if (typeof forge === "number") return forge;
|
|
373
|
-
const live = livePrEnvironments(await readRecords()).get(prEnvironmentName(args.pr));
|
|
374
|
-
return teardown(ctx, forge, args.pr, live, reason);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function teardown(ctx: CommandContext, forge: PrForge | undefined, pr: number, live: LivePrEnvironment | undefined, reason: "closed" | "expired"): Promise<number> {
|
|
378
|
-
const env = prEnvironmentName(pr);
|
|
379
335
|
const member = memberOf(ctx);
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
console.error(formatInfo(`pr-env down: ${env} has no live record, so there is nothing to tear down`));
|
|
383
|
-
if (forge) await finish(ctx, forge, report);
|
|
384
|
-
return PR_ENV_EXIT.done;
|
|
385
|
-
}
|
|
386
|
-
const { config } = await loadChantConfig(resolve(".")).catch(() => ({ config: {} as ChantConfig }));
|
|
387
|
-
const torn = await tearDownPrEnvironment({ pr, live, config, plugins: ctx.plugins, warn: (message) => console.error(formatWarning({ message })) });
|
|
388
|
-
if ("refusal" in torn) {
|
|
389
|
-
await finish(ctx, forge, { ...report, template: live.template, status: "refused", message: torn.refusal });
|
|
390
|
-
return fail(torn.refusal);
|
|
391
|
-
}
|
|
392
|
-
const { report: result, dropped, marker } = torn;
|
|
393
|
-
const counts = teardownCounts(result);
|
|
394
|
-
const complete = teardownComplete(result);
|
|
395
|
-
const lexicons = [...new Set(result.outcomes.map((o) => o.lexicon))].sort();
|
|
396
|
-
const head = await headCommit();
|
|
397
|
-
await appendRecord(ctx, {
|
|
398
|
-
version: 1,
|
|
399
|
-
kind: "down",
|
|
400
|
-
pr,
|
|
401
|
-
env,
|
|
402
|
-
...(live.member ? { member: live.member } : {}),
|
|
403
|
-
template: live.template,
|
|
404
|
-
marker,
|
|
405
|
-
commit: head,
|
|
406
|
-
components: lexicons,
|
|
407
|
-
planDigest: "",
|
|
408
|
-
result: complete ? "destroyed" : "incomplete",
|
|
409
|
-
by: actor(),
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
336
|
+
const result = await prEnvDownRun({
|
|
337
|
+
pr: args.pr,
|
|
411
338
|
reason,
|
|
412
|
-
|
|
413
|
-
|
|
339
|
+
...(member ? { member } : {}),
|
|
340
|
+
ports: cliPorts(ctx, forge),
|
|
341
|
+
...(await teardownSources()),
|
|
342
|
+
plugins: ctx.plugins,
|
|
343
|
+
commit: await headCommit(),
|
|
344
|
+
actor: actor(),
|
|
414
345
|
...(forge?.runUrl ? { runUrl: forge.runUrl } : {}),
|
|
415
346
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (result.unimplemented.length > 0) why.push(`no teardown execution in ${result.unimplemented.join(", ")}`);
|
|
419
|
-
if (result.plan.holes.length > 0) why.push(`${result.plan.holes.length} part${result.plan.holes.length === 1 ? "" : "s"} could not be read`);
|
|
420
|
-
for (const o of result.outcomes.filter((x) => x.outcome === "failed")) why.push(`${o.lexicon} ${o.name}: ${o.detail ?? "failed"}`);
|
|
421
|
-
await finish(ctx, forge, { ...report, template: live.template, status: complete ? "destroyed" : "incomplete", outcomes: counts, ...(why.length > 0 ? { message: why.join("; ") } : {}) });
|
|
422
|
-
console.error((complete ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-env down: ${env} ${complete ? "torn down" : "not wholly torn down; it stays live and the sweep tries again"} (${counts.deleted} deleted, ${counts.failed} failed, ${counts.skipped} skipped)`));
|
|
423
|
-
return complete ? PR_ENV_EXIT.done : PR_ENV_EXIT.failed;
|
|
347
|
+
if (result.report.status === "refused") return fail(result.report.message ?? "refused");
|
|
348
|
+
return result.exit;
|
|
424
349
|
}
|
|
425
350
|
|
|
426
351
|
async function headCommit(): Promise<string> {
|
|
@@ -434,29 +359,13 @@ async function headCommit(): Promise<string> {
|
|
|
434
359
|
async function prEnvSweep(ctx: CommandContext): Promise<number> {
|
|
435
360
|
const forge = forgeOf(ctx);
|
|
436
361
|
if (typeof forge === "number") return forge;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (!reason && forge?.pullRequestState) {
|
|
447
|
-
try {
|
|
448
|
-
const state = await forge.pullRequestState(e.pr);
|
|
449
|
-
if (state !== "open") reason = "closed";
|
|
450
|
-
} catch (err) {
|
|
451
|
-
console.error(formatWarning({ message: `could not read whether #${e.pr} is open (${err instanceof Error ? err.message : String(err)}); its TTL still holds` }));
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (!reason) {
|
|
455
|
-
console.error(formatInfo(`pr-env sweep: ${e.env} stays; it expires ${e.expiresAt}`));
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
const c = await teardown(ctx, forge, e.pr, e, reason);
|
|
459
|
-
if (c !== PR_ENV_EXIT.done) code = PR_ENV_EXIT.failed;
|
|
460
|
-
}
|
|
461
|
-
return code;
|
|
362
|
+
const result = await prEnvSweepRun({
|
|
363
|
+
ports: cliPorts(ctx, forge),
|
|
364
|
+
...(await teardownSources()),
|
|
365
|
+
plugins: ctx.plugins,
|
|
366
|
+
commit: await headCommit(),
|
|
367
|
+
actor: actor(),
|
|
368
|
+
...(forge?.runUrl ? { runUrl: forge.runUrl } : {}),
|
|
369
|
+
});
|
|
370
|
+
return result.exit;
|
|
462
371
|
}
|
package/src/cli/handlers/pr.ts
CHANGED
|
@@ -129,6 +129,7 @@ import { withComponentEndpoint } from "../component-endpoint";
|
|
|
129
129
|
import type { ComponentChangeSignal, FanOutPlan } from "../../components/fan-out";
|
|
130
130
|
import { failedRunExitCode, type DriverComponent } from "../../components/driver";
|
|
131
131
|
import type { CapabilityRegistry } from "../../components/capability";
|
|
132
|
+
import type { ComponentEnvironmentDeclaration } from "../../components/component";
|
|
132
133
|
import { prLoopHooksAt, runPrHooks, type PrHookPlace, type PrLoopHooks } from "../../components/pr-hooks";
|
|
133
134
|
import { decidePrPolicyOverride, prLoopPolicyAt, prPolicyReport, runPrPolicy, spendPrPolicyOverride, type PrLoopPolicy } from "../../components/pr-policy";
|
|
134
135
|
import type { OpWaveHookMoment } from "../../op/op-waves";
|
|
@@ -157,6 +158,8 @@ export interface Prepared {
|
|
|
157
158
|
signal: ComponentChangeSignal;
|
|
158
159
|
registry: CapabilityRegistry;
|
|
159
160
|
seededOutputs: Record<string, Record<string, unknown>>;
|
|
161
|
+
/** Component name to the environments it declares (#3153), whose sites `pr-env up` carries over (#4330). */
|
|
162
|
+
sites?: Record<string, ComponentEnvironmentDeclaration[]>;
|
|
160
163
|
/** `ci.hooks` from the declaration at base (#4288). */
|
|
161
164
|
hooks: PrLoopHooks;
|
|
162
165
|
/** Every hook the stage ran so far, for the report's `hooks`. */
|
|
@@ -296,6 +299,7 @@ export async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "re
|
|
|
296
299
|
components: derived.components,
|
|
297
300
|
plan: routedAway ? { ...derived.plan, order: [], waves: [], seeds: [], indeterminate: [] } : derived.plan,
|
|
298
301
|
signal: derived.signal,
|
|
302
|
+
...(derived.sites ? { sites: derived.sites } : {}),
|
|
299
303
|
registry,
|
|
300
304
|
seededOutputs,
|
|
301
305
|
hooks,
|
|
@@ -545,6 +545,8 @@ export interface ResolvedComponentTargets {
|
|
|
545
545
|
targets: DriverComponent[];
|
|
546
546
|
/** For a list selector (#3950): the dependencies the closure added that the list did not name, in discovery order. */
|
|
547
547
|
added?: string[];
|
|
548
|
+
/** Component name to the environments it declares (#3153), for the components that declare any (#4330). */
|
|
549
|
+
sites?: Record<string, ComponentEnvironmentDeclaration[]>;
|
|
548
550
|
error?: string;
|
|
549
551
|
}
|
|
550
552
|
|
|
@@ -577,7 +579,10 @@ export async function resolveComponentTargets(
|
|
|
577
579
|
|
|
578
580
|
if (isComponentList(selector)) return resolveComponentList(selector, discovered, all);
|
|
579
581
|
|
|
580
|
-
if (selector === "all")
|
|
582
|
+
if (selector === "all") {
|
|
583
|
+
const sites = Object.fromEntries(discovered.filter((c) => c.environments && c.environments.length > 0).map((c) => [c.name, c.environments!.map((e) => ({ ...e }))]));
|
|
584
|
+
return { success: true, targets: withoutDisabled(discovered, all), ...(Object.keys(sites).length > 0 ? { sites } : {}) };
|
|
585
|
+
}
|
|
581
586
|
|
|
582
587
|
const found = all.find((c) => c.name === selector);
|
|
583
588
|
const disabledByName = discovered.find((c) => c.name === selector && c.enabled === false);
|
|
@@ -20,6 +20,7 @@ import { buildCapabilityRegistry } from "./capability-plugin-loader";
|
|
|
20
20
|
import { componentsForUnits, planFanOut, type ChangedUnits, type ComponentChangeSignal, type FanOutPlan } from "./fan-out";
|
|
21
21
|
import type { CapabilityRegistry } from "./capability";
|
|
22
22
|
import type { DriverComponent } from "./driver";
|
|
23
|
+
import type { ComponentEnvironmentDeclaration } from "./component";
|
|
23
24
|
import type { BuildParamProvenance } from "../provenance";
|
|
24
25
|
import { loadChantConfig, type ChantConfig } from "../config";
|
|
25
26
|
|
|
@@ -49,6 +50,8 @@ export interface DerivedFanOut {
|
|
|
49
50
|
/** What the stack-level signal meant in component terms, `unclaimed` included. */
|
|
50
51
|
signal: ComponentChangeSignal;
|
|
51
52
|
plan: FanOutPlan;
|
|
53
|
+
/** Component name to the environments it declares, for the components that declare any (#4330). */
|
|
54
|
+
sites?: Record<string, ComponentEnvironmentDeclaration[]>;
|
|
52
55
|
error?: string;
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -85,7 +88,7 @@ export async function deriveFanOut(options: DeriveFanOutOptions): Promise<Derive
|
|
|
85
88
|
...(options.canary?.length ? { canary: options.canary } : {}),
|
|
86
89
|
});
|
|
87
90
|
|
|
88
|
-
return { success: true, components, signal, plan };
|
|
91
|
+
return { success: true, components, signal, plan, ...(resolved.sites ? { sites: resolved.sites } : {}) };
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/**
|