@intentius/chant 0.124.33 → 0.124.35
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/graph.js +2 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +147 -51
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +284 -81
- package/dist/cli/handlers/release.d.ts +12 -0
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +125 -10
- package/dist/cli/handlers/run.d.ts +3 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +12 -2
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +37 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -0
- package/dist/cli/registry.d.ts +17 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +8 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +3 -2
- package/dist/components/green-cover.d.ts +77 -0
- package/dist/components/green-cover.d.ts.map +1 -0
- package/dist/components/green-cover.js +153 -0
- package/dist/components/pr-pipeline.d.ts +34 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +39 -5
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/promote.d.ts +4 -1
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +6 -3
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +9 -0
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +15 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +11 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/graph-declared.d.ts +9 -1
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +18 -3
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +30 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +13 -2
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +5 -1
- package/dist/lifecycle/compose-refs.d.ts +52 -0
- package/dist/lifecycle/compose-refs.d.ts.map +1 -0
- package/dist/lifecycle/compose-refs.js +115 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.js +3 -2
- package/dist/lifecycle/green-apply.d.ts +39 -0
- package/dist/lifecycle/green-apply.d.ts.map +1 -0
- package/dist/lifecycle/green-apply.js +51 -0
- package/dist/lifecycle/publish-declaration.d.ts +4 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +15 -0
- package/dist/lifecycle/publish-npm.d.ts +7 -4
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +60 -13
- package/dist/lifecycle/release-compose.d.ts +91 -0
- package/dist/lifecycle/release-compose.d.ts.map +1 -0
- package/dist/lifecycle/release-compose.js +235 -0
- package/dist/lifecycle/release-cut.d.ts +8 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +2 -0
- package/dist/lifecycle/release-publish.d.ts +2 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +2 -1
- package/dist/lifecycle/release-store.d.ts +15 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +5 -1
- package/dist/wave-runtime.mjs +226 -224
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +20 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +130 -0
- package/dist/workspace/checks/security.d.ts +16 -1
- package/dist/workspace/checks/security.d.ts.map +1 -1
- package/dist/workspace/checks/security.js +4 -4
- package/dist/workspace/checks.d.ts +1 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +4 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +5 -3
- package/dist/workspace/ci-green-workflow.d.ts +18 -2
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +27 -5
- package/dist/workspace/ci-green.d.ts +31 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +78 -4
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-run.d.ts +6 -0
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +2 -1
- package/dist/workspace/ci-serve.d.ts +94 -13
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +248 -30
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +18 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +9 -2
- package/dist/workspace/declaration.schema.json +28 -0
- package/dist/workspace/delivery-metrics.d.ts +3 -1
- package/dist/workspace/delivery-metrics.d.ts.map +1 -1
- package/dist/workspace/delivery-metrics.js +12 -3
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +4 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +1 -0
- package/dist/workspace/reason-codes.d.ts +9 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +10 -0
- package/dist/workspace/release-candidates.d.ts +28 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -1
- package/dist/workspace/release-candidates.js +38 -2
- package/dist/workspace/release-candidates.schema.json +51 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/releases.schema.json +30 -0
- package/package.json +1 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/pr-green.test.ts +230 -0
- package/src/cli/handlers/pr.ts +148 -66
- package/src/cli/handlers/promote.test.ts +34 -0
- package/src/cli/handlers/promote.ts +302 -91
- package/src/cli/handlers/release-compose.e2e.test.ts +133 -0
- package/src/cli/handlers/release.ts +112 -10
- package/src/cli/handlers/run.ts +12 -1
- package/src/cli/main.ts +37 -2
- package/src/cli/parse-args.ts +18 -0
- package/src/cli/registry.ts +17 -1
- package/src/codegen/release-wiring.test.ts +56 -9
- package/src/components/auto-release.test.ts +22 -0
- package/src/components/auto-release.ts +9 -2
- package/src/components/green-cover.test.ts +170 -0
- package/src/components/green-cover.ts +186 -0
- package/src/components/pr-pipeline.test.ts +14 -4
- package/src/components/pr-pipeline.ts +56 -5
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/promote.test.ts +7 -0
- package/src/components/promote.ts +9 -4
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +24 -1
- package/src/components/release-promote.ts +15 -0
- package/src/deep-observation.ts +8 -0
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.test.ts +18 -0
- package/src/graph-declared.ts +17 -2
- package/src/graph-detail.test.ts +68 -1
- package/src/graph-detail.ts +29 -1
- package/src/graph-ir.ts +12 -4
- package/src/lifecycle/__fixtures__/publish-fakes.ts +5 -1
- package/src/lifecycle/compose-refs.ts +125 -0
- package/src/lifecycle/deep-observe.test.ts +20 -0
- package/src/lifecycle/deep-observe.ts +4 -2
- package/src/lifecycle/green-apply.ts +71 -0
- package/src/lifecycle/publish-declaration.ts +15 -0
- package/src/lifecycle/publish-npm.ts +55 -13
- package/src/lifecycle/publish-targets.test.ts +45 -0
- package/src/lifecycle/release-compose.test.ts +125 -0
- package/src/lifecycle/release-compose.ts +312 -0
- package/src/lifecycle/release-cut.ts +6 -0
- package/src/lifecycle/release-publish.ts +4 -1
- package/src/lifecycle/release-store.ts +16 -1
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +20 -2
- package/src/workspace/checks/ci-workflow.test.ts +160 -0
- package/src/workspace/checks/ci-workflow.ts +135 -0
- package/src/workspace/checks/security.ts +4 -4
- package/src/workspace/checks.ts +4 -1
- package/src/workspace/ci-green-cli.ts +5 -3
- package/src/workspace/ci-green-workflow.test.ts +52 -0
- package/src/workspace/ci-green-workflow.ts +34 -5
- package/src/workspace/ci-green.test.ts +33 -0
- package/src/workspace/ci-green.ts +85 -4
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run.test.ts +18 -13
- package/src/workspace/ci-run.ts +8 -1
- package/src/workspace/ci-serve-land.e2e.test.ts +138 -0
- package/src/workspace/ci-serve-land.test.ts +176 -0
- package/src/workspace/ci-serve.test.ts +77 -43
- package/src/workspace/ci-serve.ts +268 -34
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +28 -0
- package/src/workspace/declaration.ts +32 -3
- package/src/workspace/delivery-metrics.ts +12 -3
- package/src/workspace/graph-contract.test.ts +3 -2
- package/src/workspace/init.ts +2 -2
- package/src/workspace/member-commands.test.ts +11 -4
- package/src/workspace/member-commands.ts +3 -0
- package/src/workspace/nested-graph.test.ts +4 -0
- package/src/workspace/nested-graph.ts +1 -0
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +10 -0
- package/src/workspace/release-candidates.schema.json +51 -0
- package/src/workspace/release-candidates.test.ts +30 -3
- package/src/workspace/release-candidates.ts +54 -2
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/releases.schema.json +30 -0
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* ran from it, through the release gate `promote --release` binds (#4045).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { ENV_VAR } from "../../env";
|
|
25
|
+
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
26
27
|
import { loadChantConfig, type ChantConfig } from "../../config";
|
|
27
28
|
import { promotionOrder } from "../../workspace/promotion-order";
|
|
28
29
|
import { resolvePromotionSource } from "../../components/promote-order";
|
|
@@ -70,7 +71,7 @@ import type { CommandContext } from "../registry";
|
|
|
70
71
|
import { redeployLine, releaseRollbackLine, rollbackTarget, unsettledComponents } from "../../lifecycle/mixed-environment";
|
|
71
72
|
|
|
72
73
|
const PROMOTE_USAGE =
|
|
73
|
-
"chant components promote --to <env> [--from <env>] [--allow-skip] [--release <label|digest>] [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
|
|
74
|
+
"chant components promote --to <env> [--from <env>] [--allow-skip] [--release <label|digest>] [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--wave-gate <gate>] [--resume <file>] [--dry-run] [--json]";
|
|
74
75
|
const ROLLBACK_USAGE =
|
|
75
76
|
"chant components rollback <env> (--component <name> [--digest <sha256:...>] | --release <label|digest>) [--dry-run] [--json]";
|
|
76
77
|
const REDEPLOY_USAGE =
|
|
@@ -116,9 +117,16 @@ function resolveActor(ctx: CommandContext): string | undefined {
|
|
|
116
117
|
return ctx.args.actor ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Config, params and the declared components, or the exit code of a refusal
|
|
122
|
+
* already printed. `env` is the environment being deployed: it is set as
|
|
123
|
+
* `CHANT_ENV` before the components load, as `--env` sets it for `chant run`
|
|
124
|
+
* (../main.ts), so a component that reads it (a stack name per environment)
|
|
125
|
+
* deploys to that environment and not to the one it was promoted from (#4222).
|
|
126
|
+
*/
|
|
127
|
+
async function loadProject(ctx: CommandContext, selector: string, env: string) {
|
|
121
128
|
const { args } = ctx;
|
|
129
|
+
process.env[ENV_VAR] = env;
|
|
122
130
|
const projectPath = resolve(".");
|
|
123
131
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
124
132
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
@@ -152,6 +160,20 @@ async function readLedger(env: string): Promise<ReleaseRecord[]> {
|
|
|
152
160
|
return ledger.records;
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
/** What {@link bindReleaseGate} hands the run: the round-one binding, and how to plan and bind a later round or wave (#4168 step 4). */
|
|
164
|
+
interface BoundReleaseGate {
|
|
165
|
+
binding: ReleaseGateBinding;
|
|
166
|
+
doc: ChangeSetDocument;
|
|
167
|
+
empty: boolean;
|
|
168
|
+
reads: import("../../components/release-gate").ReleasePlan["reads"];
|
|
169
|
+
plannedOutputs?: Record<string, string[]>;
|
|
170
|
+
unknownInputs?: Record<string, string[]>;
|
|
171
|
+
/** The approval a resume record carried on, when it still covers what is left. */
|
|
172
|
+
resumedFrom?: string;
|
|
173
|
+
replan: (only: ReadonlySet<string>, outputs: Record<string, Record<string, unknown>>) => Promise<ChangeSetDocument>;
|
|
174
|
+
bind: (doc: ChangeSetDocument, gates: string[]) => Promise<ReleaseGateBinding>;
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
/**
|
|
156
178
|
* Plan, store and bind the release gate for a promote under a release
|
|
157
179
|
* (#4045, ws-104 questions 2 and 6). Undefined when no promoted composition
|
|
@@ -159,6 +181,11 @@ async function readLedger(env: string): Promise<ReleaseRecord[]> {
|
|
|
159
181
|
* a refusal already printed: a provisional member, or a member that failed
|
|
160
182
|
* to plan. On an empty change set, each gate gets an `empty: true`
|
|
161
183
|
* resolution the run then counts once it reads the stored document back.
|
|
184
|
+
*
|
|
185
|
+
* A dependent is planned against what a promoted upstream's plan says it will
|
|
186
|
+
* expose (#4168 step 4). With a resume record, only what is left is planned,
|
|
187
|
+
* against the outputs the record kept, and the approval the record names
|
|
188
|
+
* carries on when it covers that plan.
|
|
162
189
|
*/
|
|
163
190
|
async function bindReleaseGate(input: {
|
|
164
191
|
plan: PromotionPlan;
|
|
@@ -170,7 +197,8 @@ async function bindReleaseGate(input: {
|
|
|
170
197
|
actor: string;
|
|
171
198
|
projectPath: string;
|
|
172
199
|
head: string | undefined;
|
|
173
|
-
|
|
200
|
+
resume?: import("../../components/promote-rounds").PromoteResume;
|
|
201
|
+
}): Promise<BoundReleaseGate | number | undefined> {
|
|
174
202
|
const { plan, under, port } = input;
|
|
175
203
|
const gate = await import("../../components/release-gate");
|
|
176
204
|
const gates = gate.gateNamesOf(input.prepared);
|
|
@@ -199,75 +227,162 @@ async function bindReleaseGate(input: {
|
|
|
199
227
|
unsettled.set(component, `the ${u.outcome.verb} run ${u.outcome.runId} left ${component} ${u.state} in "${env}" and nothing has been recorded for it since, so what it runs is not known`);
|
|
200
228
|
}
|
|
201
229
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
message: `the change set of release ${under.label ?? releaseDigest} for "${env}" has provisional members (${provisional.join(", ")}), which no approval covers; nothing was deployed`,
|
|
216
|
-
hint: "Deploy what they read first, so they plan against real values.",
|
|
217
|
-
}));
|
|
218
|
-
return 1;
|
|
219
|
-
}
|
|
220
|
-
const failed = doc.members.filter((m) => m.status === "failed");
|
|
221
|
-
if (failed.length > 0) {
|
|
222
|
-
for (const m of failed) console.error(formatError({ message: `${m.member} failed to plan for "${env}"${m.error ? `: ${m.error}` : ""}` }));
|
|
223
|
-
console.error(formatError({ message: "nothing was deployed" }));
|
|
224
|
-
return 1;
|
|
225
|
-
}
|
|
230
|
+
const digests = new Map(plan.items.map((i) => [i.component, i.digest]));
|
|
231
|
+
const planSet = (only: ReadonlySet<string> | undefined, outputs: Record<string, Record<string, unknown>>) =>
|
|
232
|
+
gate.planReleaseSet({
|
|
233
|
+
release: under.release,
|
|
234
|
+
env,
|
|
235
|
+
components: input.prepared,
|
|
236
|
+
digests,
|
|
237
|
+
registry: input.registry,
|
|
238
|
+
current,
|
|
239
|
+
unsettled,
|
|
240
|
+
seededOutputs: outputs,
|
|
241
|
+
...(only ? { only } : {}),
|
|
242
|
+
});
|
|
226
243
|
|
|
227
|
-
// The document is stored before the gate is decided, so the plan a pending
|
|
228
|
-
// gate waits on can be read, and an empty pass can be checked.
|
|
229
244
|
const { storeGateChangeSet } = await import("../../lifecycle/gate-change-set");
|
|
230
|
-
|
|
231
|
-
const stored = await storeGateChangeSet({ doc, cwd: input.projectPath, at: input.head ?? null });
|
|
232
|
-
if (stored.warning) console.error(formatWarning({ message: stored.warning }));
|
|
233
|
-
} catch (err) {
|
|
234
|
-
console.error(formatWarning({ message: `could not store the change set ${doc.digest}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
235
|
-
}
|
|
245
|
+
const { readStoredChangeSet } = await import("../../lifecycle/change-set-store");
|
|
236
246
|
const { isEmptyChangeSet } = await import("../../change-set");
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
for (const name of gates) {
|
|
249
|
-
const standing = resolutions.some(
|
|
250
|
-
(r) => r.empty === true && r.gate === name && r.environment === env && r.releaseDigest === releaseDigest && r.planDigest === doc.digest,
|
|
251
|
-
);
|
|
252
|
-
if (standing) continue;
|
|
253
|
-
// Pushed with the resolution; the deploy records the promote writes push the same branch.
|
|
254
|
-
await port.appendResolution(
|
|
255
|
-
gate.emptyResolution({ gate: name, doc, actor: input.actor, timestamp, ...(input.head ? { gitSha: input.head } : {}) }),
|
|
256
|
-
);
|
|
247
|
+
const verifyEmpty = gate.emptyVerifier((digest) => readStoredChangeSet(digest, { cwd: input.projectPath }), { releaseDigest, environment: env });
|
|
248
|
+
|
|
249
|
+
/** Refuse, store, and pass the gates of an empty plan; the binding, or the exit code of a refusal already printed. */
|
|
250
|
+
const bindDoc = async (doc: ChangeSetDocument, names: readonly string[], what: string): Promise<{ binding: ReleaseGateBinding; empty: boolean } | number> => {
|
|
251
|
+
const provisional = gate.provisionalMembers(doc);
|
|
252
|
+
if (provisional.length > 0) {
|
|
253
|
+
console.error(formatError({
|
|
254
|
+
message: `${what} of release ${under.label ?? releaseDigest} for "${env}" has provisional members (${provisional.join(", ")}), which no approval covers; nothing more was deployed`,
|
|
255
|
+
hint: "Deploy what they read first, so they plan against real values.",
|
|
256
|
+
}));
|
|
257
|
+
return 1;
|
|
257
258
|
}
|
|
258
|
-
|
|
259
|
+
const failed = doc.members.filter((m) => m.status === "failed");
|
|
260
|
+
if (failed.length > 0) {
|
|
261
|
+
for (const m of failed) console.error(formatError({ message: `${m.member} failed to plan for "${env}"${m.error ? `: ${m.error}` : ""}` }));
|
|
262
|
+
console.error(formatError({ message: "nothing more was deployed" }));
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
// The document is stored before the gate is decided, so the plan a pending
|
|
266
|
+
// gate waits on can be read, and an empty pass can be checked.
|
|
267
|
+
try {
|
|
268
|
+
const stored = await storeGateChangeSet({ doc, cwd: input.projectPath, at: input.head ?? null });
|
|
269
|
+
if (stored.warning) console.error(formatWarning({ message: stored.warning }));
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error(formatWarning({ message: `could not store the change set ${doc.digest}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
272
|
+
}
|
|
273
|
+
const empty = isEmptyChangeSet(doc);
|
|
274
|
+
console.error(formatInfo(
|
|
275
|
+
`release gate: ${what} ${doc.digest} for "${env}"` +
|
|
276
|
+
(empty ? ", empty: its gates pass without an approval" : `, ${doc.summary.entries} entr${doc.summary.entries === 1 ? "y" : "ies"}, ${doc.summary.holes} hole(s)`) +
|
|
277
|
+
` (chant change-set show ${doc.digest})`,
|
|
278
|
+
));
|
|
279
|
+
if (empty) {
|
|
280
|
+
if (!port.appendResolution) throw new Error("the gate ledger can't record a resolution, so an empty change set can't pass its gates");
|
|
281
|
+
const { resolutions } = await port.read(gate.RELEASE_GATE_OP);
|
|
282
|
+
const timestamp = new Date().toISOString();
|
|
283
|
+
for (const name of names) {
|
|
284
|
+
const standing = resolutions.some(
|
|
285
|
+
(r) => r.empty === true && r.gate === name && r.environment === env && r.releaseDigest === releaseDigest && r.planDigest === doc.digest,
|
|
286
|
+
);
|
|
287
|
+
if (standing) continue;
|
|
288
|
+
// Pushed with the resolution; the deploy records the promote writes push the same branch.
|
|
289
|
+
await port.appendResolution(
|
|
290
|
+
gate.emptyResolution({ gate: name, doc, actor: input.actor, timestamp, ...(input.head ? { gitSha: input.head } : {}) }),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { binding: { op: gate.RELEASE_GATE_OP, planDigest: doc.digest, releaseDigest, verifyEmpty }, empty };
|
|
295
|
+
};
|
|
259
296
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
doc,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
planDigest: doc.digest,
|
|
267
|
-
releaseDigest,
|
|
268
|
-
verifyEmpty: gate.emptyVerifier((digest) => readStoredChangeSet(digest, { cwd: input.projectPath }), { releaseDigest, environment: env }),
|
|
297
|
+
const later = {
|
|
298
|
+
replan: async (only: ReadonlySet<string>, outputs: Record<string, Record<string, unknown>>) => (await planSet(only, outputs)).doc,
|
|
299
|
+
bind: async (doc: ChangeSetDocument, names: string[]) => {
|
|
300
|
+
const bound = await bindDoc(doc, names, "a later plan");
|
|
301
|
+
if (typeof bound === "number") throw new PromoteStageRefused();
|
|
302
|
+
return bound.binding;
|
|
269
303
|
},
|
|
270
304
|
};
|
|
305
|
+
|
|
306
|
+
// #4168 step 4: a resume plans what is left against the outputs it kept.
|
|
307
|
+
if (input.resume) {
|
|
308
|
+
const done = new Set(input.resume.completed);
|
|
309
|
+
const left = new Set(plan.items.map((i) => i.component).filter((c) => !done.has(c)));
|
|
310
|
+
const fresh = await planSet(left, { ...input.seededOutputs, ...input.resume.outputs });
|
|
311
|
+
const stored = await readStoredChangeSet(input.resume.approvedDigest, { cwd: input.projectPath });
|
|
312
|
+
if (stored.status === "found") {
|
|
313
|
+
const { waveNotCovered } = await import("../../components/pr-run");
|
|
314
|
+
const notCovered = waveNotCovered(stored.document, fresh.doc);
|
|
315
|
+
if (notCovered.length === 0) {
|
|
316
|
+
console.error(formatInfo(`resuming: ${done.size} component(s) applied earlier; what is left is covered by change set ${stored.document.digest}`));
|
|
317
|
+
return {
|
|
318
|
+
doc: stored.document,
|
|
319
|
+
empty: isEmptyChangeSet(stored.document),
|
|
320
|
+
binding: { op: gate.RELEASE_GATE_OP, planDigest: stored.document.digest, releaseDigest, verifyEmpty },
|
|
321
|
+
reads: fresh.reads,
|
|
322
|
+
resumedFrom: stored.document.digest,
|
|
323
|
+
...(fresh.plannedOutputs ? { plannedOutputs: fresh.plannedOutputs } : {}),
|
|
324
|
+
...(fresh.unknownInputs ? { unknownInputs: fresh.unknownInputs } : {}),
|
|
325
|
+
...later,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
for (const line of notCovered) console.error(formatWarning({ message: `resume: ${line}` }));
|
|
329
|
+
}
|
|
330
|
+
const bound = await bindDoc(fresh.doc, gates, "change set");
|
|
331
|
+
if (typeof bound === "number") return bound;
|
|
332
|
+
return { doc: fresh.doc, ...bound, reads: fresh.reads, ...(fresh.plannedOutputs ? { plannedOutputs: fresh.plannedOutputs } : {}), ...(fresh.unknownInputs ? { unknownInputs: fresh.unknownInputs } : {}), ...later };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const first = await planSet(undefined, input.seededOutputs);
|
|
336
|
+
const bound = await bindDoc(first.doc, gates, "change set");
|
|
337
|
+
if (typeof bound === "number") return bound;
|
|
338
|
+
for (const [component, refs] of Object.entries(first.unknownInputs ?? {})) {
|
|
339
|
+
console.error(formatInfo(`${component} reads ${refs.join(", ")}, known only once its upstream applies; it is planned again then, and needs its own approval if its plan moves`));
|
|
340
|
+
}
|
|
341
|
+
return { doc: first.doc, ...bound, reads: first.reads, ...(first.plannedOutputs ? { plannedOutputs: first.plannedOutputs } : {}), ...(first.unknownInputs ? { unknownInputs: first.unknownInputs } : {}), ...later };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** A later round's or wave's plan was refused (provisional or failed members); the refusal is already printed. */
|
|
345
|
+
class PromoteStageRefused extends Error {
|
|
346
|
+
constructor() {
|
|
347
|
+
super("a later round's plan was refused");
|
|
348
|
+
this.name = "PromoteStageRefused";
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The pull requests a release carries into `env` and how each was approved
|
|
354
|
+
* on merge (#4168 decision 2): shown beside the release gate, never counted
|
|
355
|
+
* by it. The commits are the release commit's first-parent history since
|
|
356
|
+
* the commits `env` runs, at most 200, read from the merge records.
|
|
357
|
+
*/
|
|
358
|
+
async function pullRequestApprovals(release: { commit: string }, since: readonly string[], cwd: string): Promise<Array<import("../../lifecycle/merge-record").MergeRecord>> {
|
|
359
|
+
const { execFileSync } = await import("node:child_process");
|
|
360
|
+
const { readMergeRecord } = await import("../../lifecycle/merge-record");
|
|
361
|
+
let commits: string[] = [];
|
|
362
|
+
try {
|
|
363
|
+
const out = execFileSync("git", ["rev-list", "--first-parent", "--max-count=200", release.commit, ...since.map((c) => `^${c}`)], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
364
|
+
commits = out.split("\n").filter(Boolean);
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
const records: Array<import("../../lifecycle/merge-record").MergeRecord> = [];
|
|
369
|
+
for (const commit of commits) {
|
|
370
|
+
const record = await readMergeRecord(commit, { cwd }).catch(() => null);
|
|
371
|
+
if (record) records.push(record);
|
|
372
|
+
}
|
|
373
|
+
return records;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** One line per merge record: where it came from and how its plan stood against its approval. */
|
|
377
|
+
function approvalLine(r: import("../../lifecycle/merge-record").MergeRecord): string {
|
|
378
|
+
const from = r.source.kind === "pull-request" ? `#${r.source.pr}` : r.source.kind === "work-item" ? r.source.records.map((x) => `${x.kind}:${x.id}`).join(", ") : "direct push";
|
|
379
|
+
const how =
|
|
380
|
+
r.plan === "approved"
|
|
381
|
+
? `approved by ${r.approvedBy.join(", ") || "nobody named"}${r.authorApproved?.length ? ` (by its author: ${r.authorApproved.join(", ")})` : ""}`
|
|
382
|
+
: r.plan === "changed"
|
|
383
|
+
? `its plan moved after the approval of ${r.approvedDigest ?? "another digest"}`
|
|
384
|
+
: r.plan;
|
|
385
|
+
return `${r.commit.slice(0, 12)} ${from}: ${how}`;
|
|
271
386
|
}
|
|
272
387
|
|
|
273
388
|
/**
|
|
@@ -290,17 +405,22 @@ async function deployPlan(
|
|
|
290
405
|
const prepared: DriverComponent[] = [];
|
|
291
406
|
const removed = new Map<string, string[]>();
|
|
292
407
|
const refusals: string[] = [];
|
|
408
|
+
// #4222: a component with no artifact, deployed from the templates the release records, and its record's digest.
|
|
409
|
+
const templateDigests = new Map<string, string>();
|
|
410
|
+
const { releaseTemplatesDigest } = await import("../../components/release-promote");
|
|
293
411
|
for (const item of plan.items) {
|
|
412
|
+
const templates = under ? releaseTemplatesDigest(under.target, item.component) : null;
|
|
294
413
|
// A rollback and a redeploy deploy a release this environment already
|
|
295
414
|
// received, so neither runs the publish step.
|
|
296
415
|
const result = withoutBuildSteps(
|
|
297
416
|
byName.get(item.component)!,
|
|
298
|
-
verb === "promote" ? {} : { pinDigest: item.digest, verb: `a ${verb}` },
|
|
417
|
+
{ ...(verb === "promote" ? {} : { pinDigest: item.digest, verb: `a ${verb}` }), templatesPinned: templates !== null },
|
|
299
418
|
);
|
|
300
419
|
if ("error" in result) refusals.push(result.error);
|
|
301
420
|
else {
|
|
302
421
|
prepared.push(result.component);
|
|
303
422
|
removed.set(item.component, result.removed);
|
|
423
|
+
if (result.templateOnly && templates !== null) templateDigests.set(item.component, templates);
|
|
304
424
|
}
|
|
305
425
|
}
|
|
306
426
|
if (refusals.length > 0) {
|
|
@@ -387,22 +507,50 @@ async function deployPlan(
|
|
|
387
507
|
// #4045: under a release, every gate binds the release's change set for this environment.
|
|
388
508
|
let releaseGate: ReleaseGateBinding | undefined;
|
|
389
509
|
let changeSetJson: { changeSet: string; empty: boolean } | undefined;
|
|
510
|
+
let bound: BoundReleaseGate | undefined;
|
|
511
|
+
// #4168 step 4: a promote that stopped at a round's or a wave's gate left a resume record.
|
|
512
|
+
const rounds = await import("../../components/promote-rounds");
|
|
513
|
+
const resumePath = resolve(project.projectPath, args.resume ?? join(".chant", "promote", `${plan.to}.resume.json`));
|
|
514
|
+
let resume: import("../../components/promote-rounds").PromoteResume | undefined;
|
|
515
|
+
if (under && existsSync(resumePath)) {
|
|
516
|
+
try {
|
|
517
|
+
const record = JSON.parse(readFileSync(resumePath, "utf8")) as import("../../components/promote-rounds").PromoteResume;
|
|
518
|
+
if (record.schema === rounds.PROMOTE_RESUME_SCHEMA && record.env === plan.to && record.releaseDigest === under.release.releaseDigest) resume = record;
|
|
519
|
+
else if (args.resume) console.error(formatWarning({ message: `--resume ${args.resume} is for another release or environment; planning from the start` }));
|
|
520
|
+
} catch (err) {
|
|
521
|
+
console.error(formatWarning({ message: `could not read the resume record ${resumePath}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
let pullRequests: Array<import("../../lifecycle/merge-record").MergeRecord> = [];
|
|
390
525
|
if (under) {
|
|
391
526
|
try {
|
|
392
|
-
const
|
|
393
|
-
if (typeof
|
|
527
|
+
const result = await bindReleaseGate({ plan, prepared, under, registry, port, seededOutputs, actor: actor!, projectPath: project.projectPath, head, ...(resume ? { resume } : {}) });
|
|
528
|
+
if (typeof result === "number") {
|
|
394
529
|
tidy();
|
|
395
|
-
return
|
|
530
|
+
return result;
|
|
396
531
|
}
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
532
|
+
if (result) {
|
|
533
|
+
bound = result;
|
|
534
|
+
releaseGate = result.binding;
|
|
535
|
+
changeSetJson = { changeSet: result.doc.digest, empty: result.empty };
|
|
536
|
+
// #4168 decision 2: the pull requests' own approvals, shown beside the gate.
|
|
537
|
+
const since = [...new Set((await readLedger(plan.to)).filter((r) => r.env === plan.to).map((r) => r.gitSha))];
|
|
538
|
+
pullRequests = await pullRequestApprovals(under.release, since, project.projectPath);
|
|
539
|
+
if (pullRequests.length > 0 && !args.json) {
|
|
540
|
+
console.error(formatInfo(`pull requests this release carries into "${plan.to}" (shown, not counted by the release gate):`));
|
|
541
|
+
for (const r of pullRequests) console.error(` ${approvalLine(r)}`);
|
|
542
|
+
}
|
|
400
543
|
}
|
|
401
544
|
} catch (err) {
|
|
402
545
|
tidy();
|
|
403
546
|
throw err;
|
|
404
547
|
}
|
|
405
548
|
}
|
|
549
|
+
if (args.waveGate && !bound) {
|
|
550
|
+
console.error(formatError({ message: "--wave-gate needs a promote under --release whose compositions declare a gate", hint: PROMOTE_USAGE }));
|
|
551
|
+
tidy();
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
406
554
|
// The run id the release records carry, chosen before the run so that
|
|
407
555
|
// `--events-json` (#4041) names the run by it too.
|
|
408
556
|
const { runId, runOrigin } = resolveRunId(args.runId);
|
|
@@ -413,23 +561,83 @@ async function deployPlan(
|
|
|
413
561
|
cwd: project.projectPath,
|
|
414
562
|
});
|
|
415
563
|
let run: Awaited<ReturnType<typeof runPromotion>>;
|
|
564
|
+
let staged: import("../../components/promote-rounds").PromoteRoundsResult | undefined;
|
|
565
|
+
// #4168 step 4: a wave at a time when a dependent read an upstream's plan,
|
|
566
|
+
// under --wave-gate, or on a resume; one run otherwise.
|
|
567
|
+
const inPlan = new Set(plan.items.map((i) => i.component));
|
|
568
|
+
const readsPlanned = bound?.plannedOutputs !== undefined && Object.keys(bound.plannedOutputs).some((d) => inPlan.has(d));
|
|
569
|
+
const inRounds = bound !== undefined && (readsPlanned || args.waveGate !== undefined || resume !== undefined);
|
|
416
570
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
571
|
+
if (inRounds && bound) {
|
|
572
|
+
staged = await rounds.runPromotionInRounds({
|
|
573
|
+
components: prepared,
|
|
574
|
+
outputs: { ...seededOutputs, ...(resume?.outputs ?? {}) },
|
|
575
|
+
binding: bound.binding,
|
|
576
|
+
approved: bound.doc,
|
|
577
|
+
reads: bound.reads,
|
|
578
|
+
...(resume ? { completed: resume.completed } : {}),
|
|
579
|
+
...(args.waveGate ? { waveGate: args.waveGate } : {}),
|
|
580
|
+
replan: bound.replan,
|
|
581
|
+
bind: bound.bind,
|
|
582
|
+
run: (components, outputs, binding) =>
|
|
583
|
+
runPromotion({
|
|
584
|
+
plan: { ...plan, items: plan.items.filter((i) => components.some((c) => c.name === i.component)) },
|
|
585
|
+
components,
|
|
586
|
+
registry,
|
|
587
|
+
componentOutputs: outputs,
|
|
588
|
+
gates: port,
|
|
589
|
+
releaseGate: binding,
|
|
590
|
+
...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
|
|
591
|
+
...(events ? { onRunEvent: events.emit } : {}),
|
|
592
|
+
}),
|
|
593
|
+
});
|
|
594
|
+
run = staged.run;
|
|
595
|
+
} else {
|
|
596
|
+
run = await runPromotion({
|
|
597
|
+
plan,
|
|
598
|
+
components: prepared,
|
|
599
|
+
registry,
|
|
600
|
+
componentOutputs: seededOutputs,
|
|
601
|
+
gates: port,
|
|
602
|
+
...(releaseGate ? { releaseGate } : {}),
|
|
603
|
+
...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
|
|
604
|
+
...(events ? { onRunEvent: events.emit } : {}),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
427
607
|
} catch (err) {
|
|
608
|
+
if (err instanceof PromoteStageRefused) {
|
|
609
|
+
events?.settle("failed", err.message);
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
428
612
|
events?.settle("failed", err instanceof Error ? err.message : String(err));
|
|
429
613
|
throw err;
|
|
430
614
|
} finally {
|
|
431
615
|
tidy();
|
|
432
616
|
}
|
|
617
|
+
if (staged && bound) {
|
|
618
|
+
for (const stage of staged.stages) {
|
|
619
|
+
const label = stage.kind === "round" ? `round ${stage.number}` : `wave ${stage.number}`;
|
|
620
|
+
console.error(formatInfo(`${label} (${stage.components.join(", ")}): planned again as ${stage.digest}, ${stage.status}`));
|
|
621
|
+
for (const line of stage.notCovered) console.error(` not covered by ${bound.doc.digest}: ${line}`);
|
|
622
|
+
if (stage.message) console.error(formatError({ message: stage.message }));
|
|
623
|
+
}
|
|
624
|
+
if (run.status === "ok") {
|
|
625
|
+
rmSync(resumePath, { force: true });
|
|
626
|
+
} else if (staged.completed.length > 0) {
|
|
627
|
+
const record: import("../../components/promote-rounds").PromoteResume = {
|
|
628
|
+
schema: rounds.PROMOTE_RESUME_SCHEMA,
|
|
629
|
+
env: plan.to,
|
|
630
|
+
releaseDigest: under!.release.releaseDigest,
|
|
631
|
+
approvedDigest: resume?.approvedDigest ?? bound.resumedFrom ?? bound.doc.digest,
|
|
632
|
+
completed: staged.completed,
|
|
633
|
+
outputs: staged.outputs,
|
|
634
|
+
at: new Date().toISOString(),
|
|
635
|
+
};
|
|
636
|
+
mkdirSync(dirname(resumePath), { recursive: true });
|
|
637
|
+
writeFileSync(resumePath, `${JSON.stringify(record, null, 2)}\n`);
|
|
638
|
+
console.error(formatInfo(`resume record: ${resumePath}; the same promote carries on from here`));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
433
641
|
// The driver settles the stream; this only ends one it left open.
|
|
434
642
|
events?.settle(run.status === "fail" ? "failed" : run.status);
|
|
435
643
|
if (!args.json) renderDriverHuman(run);
|
|
@@ -452,7 +660,8 @@ async function deployPlan(
|
|
|
452
660
|
const approver = gateApprover(result);
|
|
453
661
|
const admission = gateAdmission(result);
|
|
454
662
|
const input = record(item, plan.to, { runId, ...(runOrigin ? { runOrigin } : {}), actor: actor!, timestamp, ...(approver ? { approver } : {}), ...(admission ? { admission } : {}) });
|
|
455
|
-
const
|
|
663
|
+
const templates = templateDigests.get(item.component);
|
|
664
|
+
const { record: written } = await appendReleaseRecord(under ? { ...input, ...(templates ? { digest: templates } : {}), releaseDigest: under.release.releaseDigest } : input);
|
|
456
665
|
recorded.push(written);
|
|
457
666
|
}
|
|
458
667
|
if (outcome) await appendDeployOutcome(outcome);
|
|
@@ -464,7 +673,9 @@ async function deployPlan(
|
|
|
464
673
|
|
|
465
674
|
if (args.json) {
|
|
466
675
|
const withChangeSet = under && changeSetJson ? { release: { ...releaseJson.release!, ...changeSetJson } } : releaseJson;
|
|
467
|
-
|
|
676
|
+
const stagedJson = staged ? { stages: staged.stages, ...(bound?.plannedOutputs ? { plannedOutputs: bound.plannedOutputs } : {}), ...(bound?.unknownInputs ? { unknownInputs: bound.unknownInputs } : {}) } : {};
|
|
677
|
+
const prJson = pullRequests.length > 0 ? { pullRequests: pullRequests.map((r) => ({ commit: r.commit, source: r.source, plan: r.plan, digest: r.digest, approvedBy: r.approvedBy, ...(r.authorApproved ? { authorApproved: r.authorApproved } : {}) })) } : {};
|
|
678
|
+
console.log(JSON.stringify({ ...planJson(plan, removed), ...withChangeSet, ...stagedJson, ...prJson, run, recorded, ...(outcome ? { outcome } : {}) }, null, 2));
|
|
468
679
|
} else {
|
|
469
680
|
for (const r of recorded) {
|
|
470
681
|
console.error(formatSuccess(
|
|
@@ -675,7 +886,7 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
675
886
|
const who = actorOrRefuse(ctx);
|
|
676
887
|
if (typeof who === "number") return who;
|
|
677
888
|
|
|
678
|
-
const project = await loadProject(ctx, args.component ?? "all");
|
|
889
|
+
const project = await loadProject(ctx, args.component ?? "all", to);
|
|
679
890
|
if (typeof project === "number") return project;
|
|
680
891
|
|
|
681
892
|
// The member's `environments` array is its promotion order (#4030). It
|
|
@@ -761,7 +972,7 @@ export async function runComponentsRollback(ctx: CommandContext): Promise<number
|
|
|
761
972
|
if (typeof who === "number") return who;
|
|
762
973
|
if (args.release) return runReleaseRollback(ctx, env, args.release, who.actor);
|
|
763
974
|
|
|
764
|
-
const project = await loadProject(ctx, args.component
|
|
975
|
+
const project = await loadProject(ctx, args.component!, env);
|
|
765
976
|
if (typeof project === "number") return project;
|
|
766
977
|
|
|
767
978
|
await fetchLifecycle().catch(() => false);
|
|
@@ -791,7 +1002,7 @@ export async function runComponentsRollback(ctx: CommandContext): Promise<number
|
|
|
791
1002
|
* runs now, as `promote --release` binds them (#4045).
|
|
792
1003
|
*/
|
|
793
1004
|
async function runReleaseRollback(ctx: CommandContext, env: string, ref: string, actor: string | undefined): Promise<number> {
|
|
794
|
-
const project = await loadProject(ctx, "all");
|
|
1005
|
+
const project = await loadProject(ctx, "all", env);
|
|
795
1006
|
if (typeof project === "number") return project;
|
|
796
1007
|
await fetchLifecycle().catch(() => false);
|
|
797
1008
|
const records = await readLedger(env);
|
|
@@ -828,7 +1039,7 @@ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number
|
|
|
828
1039
|
const who = actorOrRefuse(ctx);
|
|
829
1040
|
if (typeof who === "number") return who;
|
|
830
1041
|
|
|
831
|
-
const project = await loadProject(ctx, args.component);
|
|
1042
|
+
const project = await loadProject(ctx, args.component, env);
|
|
832
1043
|
if (typeof project === "number") return project;
|
|
833
1044
|
|
|
834
1045
|
await fetchLifecycle().catch(() => false);
|