@continuous-excellence/ze-great-dashboard-aws 0.14.7 → 0.15.0
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/README.md +9 -0
- package/bootstrap/dashboard-bootstrap.example.json +7 -0
- package/dist/bootstrap-check.d.ts +3 -1
- package/dist/bootstrap.d.ts +25 -0
- package/dist/cli.js +287 -58
- package/dist/guided.d.ts +12 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +236 -31
- package/dist/remediation.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,15 @@ bootstrap initialization; routine packaging and diagnostics then select the matc
|
|
|
10
10
|
An explicit mode fails if it disagrees with persisted configuration, so changing mode requires
|
|
11
11
|
regenerating the reviewed bootstrap and parameter artifacts.
|
|
12
12
|
|
|
13
|
+
`dashboard-bootstrap.json` is checked-in desired state, not a capture of AWS. If an intentionally
|
|
14
|
+
upgraded package changes a bootstrap template contract or revision, run `bootstrap upgrade
|
|
15
|
+
--config dashboard-bootstrap.json`, review and commit only that metadata change, then have the
|
|
16
|
+
consumer's approved deployment automation preview and execute CloudFormation UPDATE change sets.
|
|
17
|
+
Package upgrades that do not change bootstrap template identity do not require a manifest update or
|
|
18
|
+
bootstrap redeploy. Captures and generated parameter files are disposable deployment artifacts;
|
|
19
|
+
the package owns template contents and the manifest records the intended package/template identity.
|
|
20
|
+
No credentials belong in the manifest.
|
|
21
|
+
|
|
13
22
|
Consumer ECS deployments use the long-lived service template and provide their own subnets and
|
|
14
23
|
security groups.
|
|
15
24
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"mode": "lambda",
|
|
3
|
+
"desiredState": {
|
|
4
|
+
"packageVersion": "0.0.0-dev",
|
|
5
|
+
"templates": {
|
|
6
|
+
"core": { "contractVersion": "1", "templateRevision": "1.3" },
|
|
7
|
+
"githubOidc": { "contractVersion": "2", "templateRevision": "2.3" }
|
|
8
|
+
}
|
|
9
|
+
},
|
|
3
10
|
"region": "us-east-1",
|
|
4
11
|
"core": {
|
|
5
12
|
"stackName": "team-dashboard-bootstrap",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind } from './bootstrap.js';
|
|
1
|
+
import { type BootstrapConfig, type BootstrapConsistency, type BootstrapDesiredStateComparison, type BootstrapKind } from './bootstrap.js';
|
|
2
2
|
import { type BootstrapRemediation } from './remediation.js';
|
|
3
3
|
export type BootstrapResourceDifference = {
|
|
4
4
|
path?: string;
|
|
@@ -33,6 +33,7 @@ export type BootstrapCheck = {
|
|
|
33
33
|
ok: boolean;
|
|
34
34
|
packageVersion: string;
|
|
35
35
|
stacks: BootstrapStackCheck[];
|
|
36
|
+
desiredState: BootstrapDesiredStateComparison;
|
|
36
37
|
remediation: BootstrapRemediation;
|
|
37
38
|
};
|
|
38
39
|
export type BootstrapCheckDependencies = {
|
|
@@ -44,4 +45,5 @@ export declare function formatBootstrapCheckText(result: BootstrapCheck): string
|
|
|
44
45
|
/** Runs the explicitly named live bootstrap diagnostic; it never changes stack resources. */
|
|
45
46
|
export declare function checkBootstrap(config: BootstrapConfig, options: {
|
|
46
47
|
resourceDrift?: boolean;
|
|
48
|
+
configPath?: string;
|
|
47
49
|
}, dependencies: BootstrapCheckDependencies): Promise<BootstrapCheck>;
|
package/dist/bootstrap.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type DeployedBootstrapStack = {
|
|
|
18
18
|
}[];
|
|
19
19
|
};
|
|
20
20
|
export type BootstrapConfig = {
|
|
21
|
+
desiredState?: BootstrapDesiredState;
|
|
21
22
|
/** Omitted in older manifests; Lambda is the compatibility default. */
|
|
22
23
|
mode?: ComputeMode;
|
|
23
24
|
region?: string;
|
|
@@ -39,6 +40,19 @@ export type BootstrapConfig = {
|
|
|
39
40
|
consumerGatewayStackName?: string;
|
|
40
41
|
};
|
|
41
42
|
};
|
|
43
|
+
export type BootstrapDesiredState = {
|
|
44
|
+
packageVersion: string;
|
|
45
|
+
templates: {
|
|
46
|
+
core: {
|
|
47
|
+
contractVersion: string;
|
|
48
|
+
templateRevision: string;
|
|
49
|
+
};
|
|
50
|
+
githubOidc: {
|
|
51
|
+
contractVersion: string;
|
|
52
|
+
templateRevision: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
42
56
|
export type BootstrapTemplateInspection = {
|
|
43
57
|
kind: BootstrapKind;
|
|
44
58
|
path: string;
|
|
@@ -55,9 +69,18 @@ export type BootstrapPlan = {
|
|
|
55
69
|
packageVersion: string;
|
|
56
70
|
packageTemplates: BootstrapTemplateInspection[];
|
|
57
71
|
configuration: BootstrapConfig;
|
|
72
|
+
desiredState: BootstrapDesiredStateComparison;
|
|
58
73
|
notes: string[];
|
|
59
74
|
remediation: BootstrapRemediation;
|
|
60
75
|
};
|
|
76
|
+
export type BootstrapDesiredStateComparison = {
|
|
77
|
+
manifest?: BootstrapDesiredState;
|
|
78
|
+
installed: BootstrapDesiredState;
|
|
79
|
+
matches: boolean;
|
|
80
|
+
packageVersionMatches: boolean;
|
|
81
|
+
ok: boolean;
|
|
82
|
+
mismatches: string[];
|
|
83
|
+
};
|
|
61
84
|
export type BootstrapConsistency = {
|
|
62
85
|
ok: boolean;
|
|
63
86
|
mismatches: string[];
|
|
@@ -65,6 +88,8 @@ export type BootstrapConsistency = {
|
|
|
65
88
|
export declare function requireComputeMode(persisted: Pick<BootstrapConfig, 'mode'>, explicit?: string): ComputeMode;
|
|
66
89
|
export declare function bootstrapTemplatePath(kind: BootstrapKind, mode?: ComputeMode): Promise<string>;
|
|
67
90
|
export declare function bootstrapTemplate(kind: BootstrapKind, mode?: ComputeMode): Promise<string>;
|
|
91
|
+
/** Resolves the package-owned identity intended for a manifest at the selected compute mode. */
|
|
92
|
+
export declare function installedBootstrapDesiredState(config?: Pick<BootstrapConfig, 'mode'>): Promise<BootstrapDesiredState>;
|
|
68
93
|
export declare function bootstrapContractVersion(template: string): string;
|
|
69
94
|
export declare function bootstrapTemplateRevision(template: string): string;
|
|
70
95
|
/** Returns installed templates and a reviewable, non-secret plan without external calls. */
|
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// packages/aws/src/cli.ts
|
|
4
4
|
import { execFile as execFile3 } from "node:child_process";
|
|
5
|
-
import { readFile as
|
|
5
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7
7
|
import { promisify as promisify3 } from "node:util";
|
|
8
8
|
import { parse as parse3 } from "yaml";
|
|
9
9
|
|
|
10
10
|
// packages/aws/src/doctor.ts
|
|
11
11
|
import { execFile as execFile2 } from "node:child_process";
|
|
12
|
-
import { readFile as
|
|
12
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
13
13
|
import { promisify as promisify2 } from "node:util";
|
|
14
14
|
import { parse as parse2 } from "yaml";
|
|
15
15
|
|
|
@@ -169,7 +169,34 @@ function bootstrapRemediation(config, input = {}) {
|
|
|
169
169
|
["github-oidc", config.githubOidc?.stackName]
|
|
170
170
|
].filter((entry) => Boolean(entry[1])).map(([kind, stackName2]) => ({ kind, stackName: stackName2 }));
|
|
171
171
|
const configPath = input.configPath ?? "manifest.json";
|
|
172
|
-
const checkCommand = `ze-great-dashboard-aws bootstrap check --config ${configPath} --format
|
|
172
|
+
const checkCommand = `npm exec -- ze-great-dashboard-aws bootstrap check --config ${configPath} --format text`;
|
|
173
|
+
const desiredStateUpdateCommand = `npm exec -- ze-great-dashboard-aws bootstrap upgrade --config ${configPath}`;
|
|
174
|
+
const desiredStateUpdateRequired = input.desiredStateUpdateRequired ?? false;
|
|
175
|
+
const jsonCheckCommand = `npm exec -- ze-great-dashboard-aws bootstrap check --config ${configPath} --format json`;
|
|
176
|
+
const region2 = config.region ?? "<region>";
|
|
177
|
+
const recoveryStacks = [
|
|
178
|
+
["core", config.core?.stackName],
|
|
179
|
+
["github-oidc", config.githubOidc?.stackName]
|
|
180
|
+
].filter((entry) => Boolean(entry[1]));
|
|
181
|
+
const captures = new Map(
|
|
182
|
+
recoveryStacks.map(([kind]) => [kind, `.bootstrap-work/${kind}-deployed-stack.json`])
|
|
183
|
+
);
|
|
184
|
+
const recoveryCommands = recoveryStacks.map(([kind, stackName2]) => ({
|
|
185
|
+
name: `capture-${kind}-stack`,
|
|
186
|
+
command: `mkdir -p .bootstrap-work && aws cloudformation describe-stacks --stack-name ${stackName2} --region ${region2} --output json --no-cli-pager > ${captures.get(kind)}`
|
|
187
|
+
}));
|
|
188
|
+
for (const [kind, stackName2] of recoveryStacks) {
|
|
189
|
+
const parameters = `.bootstrap-work/${kind}-bootstrap-parameters.json`;
|
|
190
|
+
recoveryCommands.push({
|
|
191
|
+
name: `preserve-${kind}-parameters`,
|
|
192
|
+
command: `npm exec -- ze-great-dashboard-aws bootstrap parameters --kind ${kind} --config ${configPath} --deployed-stack-json ${captures.get(kind)}${kind === "github-oidc" && captures.has("core") ? ` --core-stack-json ${captures.get("core")}` : ""} --output ${parameters}`
|
|
193
|
+
});
|
|
194
|
+
recoveryCommands.push({
|
|
195
|
+
name: `generate-${kind}-update-change-set`,
|
|
196
|
+
command: `npm exec -- ze-great-dashboard-aws bootstrap change-set --kind ${kind} --config ${configPath} --stack-name ${stackName2} --change-set-name repair-${kind} --change-set-type UPDATE --parameters ${parameters}`
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
recoveryCommands.push({ name: "revalidate-bootstrap", command: checkCommand });
|
|
173
200
|
const issues = input.issues ?? [];
|
|
174
201
|
const summary = input.summary ?? (issues.length ? `Bootstrap validation found ${issues.length} issue${issues.length === 1 ? "" : "s"}.` : "Bootstrap validation is ready for the next reviewed operation.");
|
|
175
202
|
return {
|
|
@@ -181,10 +208,22 @@ function bootstrapRemediation(config, input = {}) {
|
|
|
181
208
|
"Capture the resulting CloudFormation stack output for the next validation step."
|
|
182
209
|
],
|
|
183
210
|
upgradeSteps: [
|
|
211
|
+
...desiredStateUpdateRequired ? [`Bootstrap template identity changed; review and run: ${desiredStateUpdateCommand}`] : [],
|
|
184
212
|
"If the contract or template revision is stale, generate parameters from this installed package and create a reviewed UPDATE change set for each affected stack.",
|
|
185
213
|
"Review IAM actions, retained resources, parameters, and the change set before an administrator executes it."
|
|
186
214
|
],
|
|
187
|
-
|
|
215
|
+
recoveryCommands,
|
|
216
|
+
...desiredStateUpdateRequired ? {
|
|
217
|
+
desiredStateUpdateCommand,
|
|
218
|
+
desiredStateCheckpoint: "Review and commit the desired-state manifest mutation before the administrator deployment process consumes it; never copy deployed AWS values into desired state."
|
|
219
|
+
} : {},
|
|
220
|
+
reviewCheckpoints: [
|
|
221
|
+
"Review the captured stack JSON and preserved parameter files before generating any change set.",
|
|
222
|
+
"Inspect every generated UPDATE change set in CloudFormation; verify IAM actions, retained resources, and parameters before executing it.",
|
|
223
|
+
"Execute change sets only as an administrator, one affected stack at a time, then capture both stacks again."
|
|
224
|
+
],
|
|
225
|
+
runbookTarget: "docs/aws-bootstrap-upgrade.md",
|
|
226
|
+
revalidateCommand: jsonCheckCommand,
|
|
188
227
|
safetyNote
|
|
189
228
|
};
|
|
190
229
|
}
|
|
@@ -196,6 +235,17 @@ function formatBootstrapRemediationText(remediation) {
|
|
|
196
235
|
...remediation.immediateSteps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
197
236
|
"Upgrade steps:",
|
|
198
237
|
...remediation.upgradeSteps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
238
|
+
...remediation.desiredStateUpdateCommand ? [
|
|
239
|
+
`Desired-state update (repository mutation; review and commit):
|
|
240
|
+
$ ${remediation.desiredStateUpdateCommand}`,
|
|
241
|
+
`Desired-state checkpoint: ${remediation.desiredStateCheckpoint}`
|
|
242
|
+
] : [],
|
|
243
|
+
"Copy/paste recovery commands (review before execute):",
|
|
244
|
+
...remediation.recoveryCommands.map(({ name, command }) => ` ${name}:
|
|
245
|
+
$ ${command}`),
|
|
246
|
+
"Review checkpoints:",
|
|
247
|
+
...remediation.reviewCheckpoints.map((checkpoint, index) => ` ${index + 1}. ${checkpoint}`),
|
|
248
|
+
`Runbook: ${remediation.runbookTarget}`,
|
|
199
249
|
`Revalidate: ${remediation.revalidateCommand}`,
|
|
200
250
|
`Safety: ${remediation.safetyNote}`
|
|
201
251
|
];
|
|
@@ -222,6 +272,47 @@ async function bootstrapTemplatePath(kind, mode = "lambda") {
|
|
|
222
272
|
async function bootstrapTemplate(kind, mode = "lambda") {
|
|
223
273
|
return readFile2(await bootstrapTemplatePath(kind, mode), "utf8");
|
|
224
274
|
}
|
|
275
|
+
async function installedBootstrapDesiredState(config = {}) {
|
|
276
|
+
const packageManifest = JSON.parse(
|
|
277
|
+
await readFile2(new URL("../package.json", import.meta.url), "utf8")
|
|
278
|
+
);
|
|
279
|
+
return desiredStateFromTemplates(
|
|
280
|
+
packageVersion(packageManifest),
|
|
281
|
+
await installedTemplateInspections(computeMode(config))
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function packageVersion(packageManifest) {
|
|
285
|
+
return typeof packageManifest.version === "string" ? packageManifest.version : "unknown";
|
|
286
|
+
}
|
|
287
|
+
function desiredStateFromTemplates(version, packageTemplates) {
|
|
288
|
+
const template = (kind) => packageTemplates.find((entry) => entry.kind === kind);
|
|
289
|
+
const core = template("core");
|
|
290
|
+
const githubOidc = template("github-oidc");
|
|
291
|
+
if (!core || !githubOidc) throw new Error("Installed package is missing a bootstrap template");
|
|
292
|
+
if (!core.templateRevision || !githubOidc.templateRevision)
|
|
293
|
+
throw new Error("Installed bootstrap template has no template revision");
|
|
294
|
+
return {
|
|
295
|
+
packageVersion: version,
|
|
296
|
+
templates: {
|
|
297
|
+
core: { contractVersion: core.contractVersion, templateRevision: core.templateRevision },
|
|
298
|
+
githubOidc: {
|
|
299
|
+
contractVersion: githubOidc.contractVersion,
|
|
300
|
+
templateRevision: githubOidc.templateRevision
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async function installedTemplateInspections(mode) {
|
|
306
|
+
return Promise.all(
|
|
307
|
+
["core", "github-oidc"].map(async (kind) => {
|
|
308
|
+
const [path, template] = await Promise.all([
|
|
309
|
+
bootstrapTemplatePath(kind, mode),
|
|
310
|
+
bootstrapTemplate(kind, mode)
|
|
311
|
+
]);
|
|
312
|
+
return inspectTemplate(kind, path, template);
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
}
|
|
225
316
|
function bootstrapContractVersion(template) {
|
|
226
317
|
const version = template.match(/BootstrapContractVersion:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
|
|
227
318
|
if (!version) throw new Error("Bootstrap template has no BootstrapContractVersion output");
|
|
@@ -255,26 +346,36 @@ async function bootstrapPlan(config) {
|
|
|
255
346
|
const packageManifest = JSON.parse(
|
|
256
347
|
await readFile2(new URL("../package.json", import.meta.url), "utf8")
|
|
257
348
|
);
|
|
258
|
-
const packageTemplates = await
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
bootstrapTemplatePath(kind, mode),
|
|
263
|
-
bootstrapTemplate(kind, mode)
|
|
264
|
-
]);
|
|
265
|
-
return inspectTemplate(kind, path, template);
|
|
266
|
-
})
|
|
349
|
+
const packageTemplates = await installedTemplateInspections(computeMode(config));
|
|
350
|
+
const installedDesiredState = desiredStateFromTemplates(
|
|
351
|
+
packageVersion(packageManifest),
|
|
352
|
+
packageTemplates
|
|
267
353
|
);
|
|
354
|
+
const desiredStateMatches = Boolean(config.desiredState) && ["core", "githubOidc"].every((kind) => {
|
|
355
|
+
const manifestTemplate = config.desiredState?.templates[kind];
|
|
356
|
+
const installedTemplate = installedDesiredState.templates[kind];
|
|
357
|
+
return manifestTemplate?.contractVersion === installedTemplate.contractVersion && manifestTemplate?.templateRevision === installedTemplate.templateRevision;
|
|
358
|
+
});
|
|
359
|
+
const packageVersionMatches = config.desiredState?.packageVersion === installedDesiredState.packageVersion;
|
|
268
360
|
return {
|
|
269
|
-
packageVersion:
|
|
361
|
+
packageVersion: installedDesiredState.packageVersion,
|
|
270
362
|
packageTemplates,
|
|
271
363
|
configuration: config,
|
|
364
|
+
desiredState: {
|
|
365
|
+
...config.desiredState ? { manifest: config.desiredState } : {},
|
|
366
|
+
installed: installedDesiredState,
|
|
367
|
+
matches: desiredStateMatches,
|
|
368
|
+
packageVersionMatches,
|
|
369
|
+
ok: desiredStateMatches,
|
|
370
|
+
mismatches: desiredStateMatches ? [] : ["manifest bootstrap template identity differs from installed package"]
|
|
371
|
+
},
|
|
272
372
|
notes: [
|
|
273
373
|
"Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
|
|
274
374
|
"Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
|
|
275
375
|
"This plan performs no AWS or GitHub mutations."
|
|
276
376
|
],
|
|
277
377
|
remediation: bootstrapRemediation(config, {
|
|
378
|
+
desiredStateUpdateRequired: !desiredStateMatches,
|
|
278
379
|
nextOperation: "Review the installed templates, then run bootstrap preflight before creating a change set."
|
|
279
380
|
})
|
|
280
381
|
};
|
|
@@ -417,7 +518,7 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
417
518
|
|
|
418
519
|
// packages/aws/src/index.ts
|
|
419
520
|
import { execFile } from "node:child_process";
|
|
420
|
-
import { cp, mkdir as mkdir2, readFile as
|
|
521
|
+
import { cp, mkdir as mkdir2, readFile as readFile4, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
421
522
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
422
523
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
423
524
|
import { promisify } from "node:util";
|
|
@@ -1143,7 +1244,15 @@ import { parseDocument, Scalar } from "yaml";
|
|
|
1143
1244
|
|
|
1144
1245
|
// packages/aws/src/bootstrap-check.ts
|
|
1145
1246
|
function formatBootstrapCheckText(result) {
|
|
1146
|
-
const lines = [
|
|
1247
|
+
const lines = [
|
|
1248
|
+
`AWS bootstrap consistency (${result.packageVersion})`,
|
|
1249
|
+
`Desired state: ${result.desiredState.ok ? "PASS" : "FAIL"}`,
|
|
1250
|
+
`Package provenance: ${result.desiredState.packageVersionMatches ? "matches installed package" : `manifest ${result.desiredState.manifest?.packageVersion ?? "missing"}, installed ${result.desiredState.installed.packageVersion} (informational)`}`,
|
|
1251
|
+
...(result.desiredState.mismatches ?? []).map((mismatch) => ` mismatch: ${mismatch}`),
|
|
1252
|
+
...result.remediation.desiredStateUpdateCommand ? [
|
|
1253
|
+
`Desired-state update command (review, commit, then deploy): ${result.remediation.desiredStateUpdateCommand}`
|
|
1254
|
+
] : []
|
|
1255
|
+
];
|
|
1147
1256
|
for (const stack of result.stacks) {
|
|
1148
1257
|
lines.push(
|
|
1149
1258
|
`${stack.consistency.ok && (!stack.resourceDrift || stack.resourceDrift.ok) ? "PASS" : "FAIL"} ${stack.kind}: ${stack.stackName} (${stack.stackStatus ?? "unavailable"})`,
|
|
@@ -1193,6 +1302,17 @@ function outputs(stack) {
|
|
|
1193
1302
|
).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
|
|
1194
1303
|
);
|
|
1195
1304
|
}
|
|
1305
|
+
function desiredStateShape(value2) {
|
|
1306
|
+
if (!value2 || typeof value2 !== "object") return false;
|
|
1307
|
+
const state = value2;
|
|
1308
|
+
const templates2 = state.templates;
|
|
1309
|
+
if (typeof state.packageVersion !== "string" || !templates2 || typeof templates2 !== "object")
|
|
1310
|
+
return false;
|
|
1311
|
+
return ["core", "githubOidc"].every((kind) => {
|
|
1312
|
+
const template = templates2[kind];
|
|
1313
|
+
return Boolean(template) && typeof template === "object" && typeof template.contractVersion === "string" && typeof template.templateRevision === "string";
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1196
1316
|
async function resourceDrift(stackName2, region2, dependencies) {
|
|
1197
1317
|
try {
|
|
1198
1318
|
const detected = parseJson(
|
|
@@ -1326,6 +1446,26 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1326
1446
|
"github-oidc": config.githubOidc?.stackName
|
|
1327
1447
|
};
|
|
1328
1448
|
const plan = await bootstrapPlan(config);
|
|
1449
|
+
const desiredStateMismatches = [];
|
|
1450
|
+
const declared = config.desiredState;
|
|
1451
|
+
if (!desiredStateShape(declared))
|
|
1452
|
+
desiredStateMismatches.push(
|
|
1453
|
+
`manifest desiredState metadata is ${declared ? "malformed" : "missing"}; run the explicit bootstrap upgrade command`
|
|
1454
|
+
);
|
|
1455
|
+
else {
|
|
1456
|
+
for (const kind of ["core", "githubOidc"]) {
|
|
1457
|
+
const wanted = declared.templates[kind];
|
|
1458
|
+
const installed = plan.desiredState.installed.templates[kind];
|
|
1459
|
+
if (!wanted || wanted.contractVersion !== installed.contractVersion)
|
|
1460
|
+
desiredStateMismatches.push(
|
|
1461
|
+
`${kind} contract is ${wanted?.contractVersion ?? "missing"}; installed package has ${installed.contractVersion}`
|
|
1462
|
+
);
|
|
1463
|
+
if (!wanted || wanted.templateRevision !== installed.templateRevision)
|
|
1464
|
+
desiredStateMismatches.push(
|
|
1465
|
+
`${kind} template revision is ${wanted?.templateRevision ?? "missing"}; installed package has ${installed.templateRevision}`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1329
1469
|
const responses = await Promise.all(
|
|
1330
1470
|
["core", "github-oidc"].map(async (kind) => {
|
|
1331
1471
|
const stackName2 = stackNames[kind];
|
|
@@ -1361,8 +1501,8 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1361
1501
|
kind,
|
|
1362
1502
|
config,
|
|
1363
1503
|
response,
|
|
1364
|
-
template.contractVersion,
|
|
1365
|
-
template.templateRevision,
|
|
1504
|
+
(desiredStateShape(declared) ? declared.templates[kind === "core" ? "core" : "githubOidc"].contractVersion : void 0) ?? template.contractVersion,
|
|
1505
|
+
(desiredStateShape(declared) ? declared.templates[kind === "core" ? "core" : "githubOidc"].templateRevision : void 0) ?? template.templateRevision,
|
|
1366
1506
|
coreOutputValues
|
|
1367
1507
|
);
|
|
1368
1508
|
const checked = {
|
|
@@ -1387,12 +1527,22 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1387
1527
|
({ consistency, resourceDrift: resourceDrift2 }) => !consistency.ok || Boolean(resourceDrift2 && !resourceDrift2.ok)
|
|
1388
1528
|
);
|
|
1389
1529
|
return {
|
|
1390
|
-
ok: stacks.every(
|
|
1530
|
+
ok: desiredStateMismatches.length === 0 && stacks.every(
|
|
1391
1531
|
({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
|
|
1392
1532
|
),
|
|
1393
1533
|
packageVersion: plan.packageVersion,
|
|
1394
1534
|
stacks,
|
|
1535
|
+
desiredState: {
|
|
1536
|
+
ok: desiredStateMismatches.length === 0,
|
|
1537
|
+
mismatches: desiredStateMismatches,
|
|
1538
|
+
...desiredStateShape(declared) ? { manifest: declared } : {},
|
|
1539
|
+
installed: plan.desiredState.installed,
|
|
1540
|
+
matches: desiredStateMismatches.length === 0,
|
|
1541
|
+
packageVersionMatches: declared?.packageVersion === plan.desiredState.installed.packageVersion
|
|
1542
|
+
},
|
|
1395
1543
|
remediation: bootstrapRemediation(config, {
|
|
1544
|
+
configPath: options.configPath,
|
|
1545
|
+
desiredStateUpdateRequired: desiredStateMismatches.length > 0,
|
|
1396
1546
|
summary: failed.length ? `Bootstrap validation failed for ${failed.length} stack${failed.length === 1 ? "" : "s"}.` : "Bootstrap stacks are consistent and ready for the deployment check.",
|
|
1397
1547
|
affectedStacks: (failed.length ? failed : stacks).map(
|
|
1398
1548
|
({ kind, stackName: stackName2, consistency, resourceDrift: resourceDrift2 }) => ({
|
|
@@ -1410,6 +1560,9 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1410
1560
|
};
|
|
1411
1561
|
}
|
|
1412
1562
|
|
|
1563
|
+
// packages/aws/src/guided.ts
|
|
1564
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1565
|
+
|
|
1413
1566
|
// packages/aws/src/handoff.ts
|
|
1414
1567
|
var githubSubjectKeys = [
|
|
1415
1568
|
"repository_owner",
|
|
@@ -1902,6 +2055,7 @@ async function scaffoldBootstrapManifest(input) {
|
|
|
1902
2055
|
if (!repositoryId)
|
|
1903
2056
|
throw new Error("--github-repository-id is required when GitHub is unavailable");
|
|
1904
2057
|
return {
|
|
2058
|
+
desiredState: await installedBootstrapDesiredState({ mode: input.mode ?? "lambda" }),
|
|
1905
2059
|
region: region2,
|
|
1906
2060
|
core: {
|
|
1907
2061
|
stackName: `${slug}-bootstrap`,
|
|
@@ -1920,6 +2074,55 @@ async function scaffoldBootstrapManifest(input) {
|
|
|
1920
2074
|
}
|
|
1921
2075
|
};
|
|
1922
2076
|
}
|
|
2077
|
+
async function upgradeBootstrapManifest(path) {
|
|
2078
|
+
let parsed;
|
|
2079
|
+
try {
|
|
2080
|
+
parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
throw new Error(
|
|
2083
|
+
`Unable to read bootstrap manifest ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2087
|
+
throw new Error(`Bootstrap manifest ${path} must contain a JSON object`);
|
|
2088
|
+
const existing = parsed;
|
|
2089
|
+
const missing = incomplete(existing);
|
|
2090
|
+
if (missing.length) throw new Error(`Bootstrap manifest is incomplete: ${missing.join(", ")}`);
|
|
2091
|
+
if (existing.mode && existing.mode !== "lambda" && existing.mode !== "ecs")
|
|
2092
|
+
throw new Error("Bootstrap manifest has invalid mode; expected lambda or ecs");
|
|
2093
|
+
const desiredState = await installedBootstrapDesiredState({ mode: existing.mode ?? "lambda" });
|
|
2094
|
+
const previous = existing.desiredState;
|
|
2095
|
+
const metadataChanges = [
|
|
2096
|
+
...previous?.packageVersion !== desiredState.packageVersion ? [
|
|
2097
|
+
{
|
|
2098
|
+
path: "packageVersion",
|
|
2099
|
+
before: previous?.packageVersion,
|
|
2100
|
+
after: desiredState.packageVersion
|
|
2101
|
+
}
|
|
2102
|
+
] : [],
|
|
2103
|
+
...["core", "githubOidc"].flatMap((kind) => [
|
|
2104
|
+
...previous?.templates?.[kind]?.contractVersion !== desiredState.templates[kind].contractVersion ? [
|
|
2105
|
+
{
|
|
2106
|
+
path: `templates.${kind}.contractVersion`,
|
|
2107
|
+
before: previous?.templates?.[kind]?.contractVersion,
|
|
2108
|
+
after: desiredState.templates[kind].contractVersion
|
|
2109
|
+
}
|
|
2110
|
+
] : [],
|
|
2111
|
+
...previous?.templates?.[kind]?.templateRevision !== desiredState.templates[kind].templateRevision ? [
|
|
2112
|
+
{
|
|
2113
|
+
path: `templates.${kind}.templateRevision`,
|
|
2114
|
+
before: previous?.templates?.[kind]?.templateRevision,
|
|
2115
|
+
after: desiredState.templates[kind].templateRevision
|
|
2116
|
+
}
|
|
2117
|
+
] : []
|
|
2118
|
+
])
|
|
2119
|
+
];
|
|
2120
|
+
const changed = metadataChanges.map(({ path: path2 }) => path2);
|
|
2121
|
+
const manifest = { ...existing, desiredState };
|
|
2122
|
+
await writeFile2(path, `${JSON.stringify(manifest, null, 2)}
|
|
2123
|
+
`);
|
|
2124
|
+
return { manifest, desiredState, changed, metadataChanges };
|
|
2125
|
+
}
|
|
1923
2126
|
function incomplete(config) {
|
|
1924
2127
|
const fields = [
|
|
1925
2128
|
["region", config.region],
|
|
@@ -2189,13 +2392,13 @@ async function packageLambda(options) {
|
|
|
2189
2392
|
computeMode: "lambda",
|
|
2190
2393
|
artifactChecksums: {
|
|
2191
2394
|
...release.metadata.artifactChecksums,
|
|
2192
|
-
"index.mjs": sha256(await
|
|
2395
|
+
"index.mjs": sha256(await readFile4(join2(runtimeDir, "index.mjs")))
|
|
2193
2396
|
}
|
|
2194
2397
|
};
|
|
2195
|
-
await
|
|
2398
|
+
await writeFile3(join2(runtimeDir, "release.json"), `${JSON.stringify(runtimeMetadata, null, 2)}
|
|
2196
2399
|
`);
|
|
2197
2400
|
const sums = Object.entries(runtimeMetadata.artifactChecksums).map(([name, digest]) => `${digest} ${name}`).join("\n");
|
|
2198
|
-
await
|
|
2401
|
+
await writeFile3(join2(runtimeDir, "SHA256SUMS"), `${sums}
|
|
2199
2402
|
`);
|
|
2200
2403
|
const lambdaPath = join2(outputDir, "lambda.zip");
|
|
2201
2404
|
const archiveFiles = ["SHA256SUMS", "board.yaml", "index.mjs", "release.json"].sort();
|
|
@@ -2204,24 +2407,24 @@ async function packageLambda(options) {
|
|
|
2204
2407
|
archiveFiles.map(async (name) => [
|
|
2205
2408
|
name,
|
|
2206
2409
|
[
|
|
2207
|
-
strToU8(await
|
|
2410
|
+
strToU8(await readFile4(join2(runtimeDir, name), "utf8")),
|
|
2208
2411
|
{ mtime: new Date(1980, 0, 1, 0, 0, 0), level: 9 }
|
|
2209
2412
|
]
|
|
2210
2413
|
])
|
|
2211
2414
|
)
|
|
2212
2415
|
);
|
|
2213
|
-
await
|
|
2416
|
+
await writeFile3(lambdaPath, zipSync(archive));
|
|
2214
2417
|
await rm(runtimeDir, { recursive: true, force: true });
|
|
2215
|
-
const lambdaChecksum = sha256(await
|
|
2418
|
+
const lambdaChecksum = sha256(await readFile4(lambdaPath));
|
|
2216
2419
|
const deploymentChecksum = sha256(JSON.stringify(runtimeMetadata));
|
|
2217
2420
|
const packagedRelease = {
|
|
2218
2421
|
...runtimeMetadata,
|
|
2219
2422
|
artifactChecksums: { ...runtimeMetadata.artifactChecksums, "lambda.zip": lambdaChecksum },
|
|
2220
2423
|
artifactKey: `lambda/${deploymentChecksum}.zip`
|
|
2221
2424
|
};
|
|
2222
|
-
await
|
|
2425
|
+
await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
|
|
2223
2426
|
`);
|
|
2224
|
-
await
|
|
2427
|
+
await writeFile3(
|
|
2225
2428
|
join2(outputDir, "template.yml"),
|
|
2226
2429
|
deploymentTemplate(await cloudFormationTemplate("lambda"), {
|
|
2227
2430
|
ComputeMode: "lambda",
|
|
@@ -2249,9 +2452,9 @@ async function packageEcs(options) {
|
|
|
2249
2452
|
image: imageReference,
|
|
2250
2453
|
artifactKey: `ecs/${sha256(JSON.stringify({ ...release.metadata, image: imageReference }))}`
|
|
2251
2454
|
};
|
|
2252
|
-
await
|
|
2455
|
+
await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
|
|
2253
2456
|
`);
|
|
2254
|
-
await
|
|
2457
|
+
await writeFile3(
|
|
2255
2458
|
join2(outputDir, "template.yml"),
|
|
2256
2459
|
deploymentTemplate(await cloudFormationTemplate("ecs"), {
|
|
2257
2460
|
ComputeMode: "ecs",
|
|
@@ -2259,16 +2462,16 @@ async function packageEcs(options) {
|
|
|
2259
2462
|
DashboardVersion: packagedRelease.dashboardVersion
|
|
2260
2463
|
})
|
|
2261
2464
|
);
|
|
2262
|
-
await
|
|
2465
|
+
await writeFile3(
|
|
2263
2466
|
join2(outputDir, "SHA256SUMS"),
|
|
2264
|
-
`${sha256(await
|
|
2467
|
+
`${sha256(await readFile4(join2(outputDir, "release.json")))} release.json
|
|
2265
2468
|
`
|
|
2266
2469
|
);
|
|
2267
2470
|
return packagedRelease;
|
|
2268
2471
|
}
|
|
2269
2472
|
async function publishClientAssets(options) {
|
|
2270
2473
|
const assetsDir = resolve2(options.assetsDir);
|
|
2271
|
-
await
|
|
2474
|
+
await readFile4(join2(assetsDir, "index.html"));
|
|
2272
2475
|
const assetPath = `${options.assetsBaseUrl.replace(/\/+$/, "")}/dashboard/${options.version}`;
|
|
2273
2476
|
await run("aws", [
|
|
2274
2477
|
"s3",
|
|
@@ -2291,7 +2494,7 @@ async function publishClientAssets(options) {
|
|
|
2291
2494
|
return assetPath;
|
|
2292
2495
|
}
|
|
2293
2496
|
async function cloudFormationTemplate(mode = "lambda") {
|
|
2294
|
-
return
|
|
2497
|
+
return readFile4(
|
|
2295
2498
|
fileURLToPath2(
|
|
2296
2499
|
new URL(mode === "ecs" ? "../template-ecs.yml" : "../template.yml", import.meta.url)
|
|
2297
2500
|
),
|
|
@@ -2396,7 +2599,7 @@ async function runDoctor(options, dependencies = actualDependencies) {
|
|
|
2396
2599
|
let parameterData;
|
|
2397
2600
|
await check("Parameters/template", async () => {
|
|
2398
2601
|
const parameters = readParameterValues(
|
|
2399
|
-
JSON.parse(await
|
|
2602
|
+
JSON.parse(await readFile5(options.parametersPath, "utf8"))
|
|
2400
2603
|
);
|
|
2401
2604
|
parameterData = await templateContract(parameters);
|
|
2402
2605
|
return `${options.parametersPath} is compatible (${computeMode({ mode: parameterData.values.ComputeMode })})`;
|
|
@@ -2438,7 +2641,7 @@ async function runDoctor(options, dependencies = actualDependencies) {
|
|
|
2438
2641
|
const template = await bootstrapTemplate("github-oidc");
|
|
2439
2642
|
const expectedRevision = bootstrapTemplateRevision(template);
|
|
2440
2643
|
await check("Bootstrap template", async () => {
|
|
2441
|
-
const raw = JSON.parse(await
|
|
2644
|
+
const raw = JSON.parse(await readFile5(stackPath, "utf8"));
|
|
2442
2645
|
const stack = raw && typeof raw === "object" && "Stacks" in raw ? raw.Stacks?.[0] : raw;
|
|
2443
2646
|
if (!stack || typeof stack !== "object")
|
|
2444
2647
|
throw new Error("captured stack JSON contains no stack");
|
|
@@ -2476,7 +2679,7 @@ var option = (name, fallback) => {
|
|
|
2476
2679
|
};
|
|
2477
2680
|
async function installedPackageVersion() {
|
|
2478
2681
|
const packageManifest = JSON.parse(
|
|
2479
|
-
await
|
|
2682
|
+
await readFile6(new URL("../package.json", import.meta.url), "utf8")
|
|
2480
2683
|
);
|
|
2481
2684
|
return typeof packageManifest.version === "string" ? packageManifest.version : "";
|
|
2482
2685
|
}
|
|
@@ -2490,7 +2693,7 @@ function parameter(key, value2) {
|
|
|
2490
2693
|
}
|
|
2491
2694
|
async function existingParameters(path, allowMissing = false) {
|
|
2492
2695
|
try {
|
|
2493
|
-
const parsed = JSON.parse(await
|
|
2696
|
+
const parsed = JSON.parse(await readFile6(path, "utf8"));
|
|
2494
2697
|
if (!Array.isArray(parsed)) throw new Error(`${path} must contain a JSON parameter array`);
|
|
2495
2698
|
const values = parsed.map((value2) => {
|
|
2496
2699
|
if (!value2 || typeof value2 !== "object" || typeof value2.ParameterKey !== "string" || typeof value2.ParameterValue !== "string")
|
|
@@ -2514,7 +2717,7 @@ function bootstrapKind() {
|
|
|
2514
2717
|
throw new Error("--kind must be core or github-oidc");
|
|
2515
2718
|
}
|
|
2516
2719
|
async function readBootstrapConfig(path) {
|
|
2517
|
-
const parsed = JSON.parse(await
|
|
2720
|
+
const parsed = JSON.parse(await readFile6(path, "utf8"));
|
|
2518
2721
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2519
2722
|
throw new Error("--config must contain a JSON object");
|
|
2520
2723
|
return parsed;
|
|
@@ -2693,10 +2896,10 @@ function copyableCommand(command) {
|
|
|
2693
2896
|
).join(" ");
|
|
2694
2897
|
}
|
|
2695
2898
|
try {
|
|
2696
|
-
const
|
|
2899
|
+
const packageVersion2 = await installedPackageVersion();
|
|
2697
2900
|
const bundledAssets = fileURLToPath3(new URL("../client", import.meta.url));
|
|
2698
2901
|
if (args[0] === "publish-assets") {
|
|
2699
|
-
const version = requiredOption("--version",
|
|
2902
|
+
const version = requiredOption("--version", packageVersion2);
|
|
2700
2903
|
const assetPath = await publishClientAssets({
|
|
2701
2904
|
assetsDir: option("--assets-dir", bundledAssets) ?? bundledAssets,
|
|
2702
2905
|
assetsBucket: requiredOption("--assets-bucket"),
|
|
@@ -2719,7 +2922,7 @@ try {
|
|
|
2719
2922
|
return fetch(url);
|
|
2720
2923
|
},
|
|
2721
2924
|
nodeVersion: process.versions.node,
|
|
2722
|
-
packageVersion
|
|
2925
|
+
packageVersion: packageVersion2
|
|
2723
2926
|
}
|
|
2724
2927
|
);
|
|
2725
2928
|
const remediation = checks[0]?.remediation;
|
|
@@ -2774,7 +2977,7 @@ try {
|
|
|
2774
2977
|
if (value2 === void 0) throw new Error(`No value for CloudFormation parameter ${key}`);
|
|
2775
2978
|
return parameter(key, String(value2));
|
|
2776
2979
|
});
|
|
2777
|
-
await
|
|
2980
|
+
await writeFile4(output, `${JSON.stringify(parameters, null, 2)}
|
|
2778
2981
|
`);
|
|
2779
2982
|
printBootstrap({ output }, `Wrote ${output}. Next: run the generated deployment handoff.`);
|
|
2780
2983
|
} else if (args[0] === "bootstrap") {
|
|
@@ -2782,7 +2985,7 @@ try {
|
|
|
2782
2985
|
if (action === "init") {
|
|
2783
2986
|
const output = requiredOption("--output");
|
|
2784
2987
|
try {
|
|
2785
|
-
await
|
|
2988
|
+
await readFile6(output, "utf8");
|
|
2786
2989
|
throw new Error(`Refusing to overwrite existing manifest: ${output}`);
|
|
2787
2990
|
} catch (error) {
|
|
2788
2991
|
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
@@ -2801,12 +3004,21 @@ try {
|
|
|
2801
3004
|
mode: option("--mode", "lambda") ?? "lambda",
|
|
2802
3005
|
runner
|
|
2803
3006
|
});
|
|
2804
|
-
await
|
|
3007
|
+
await writeFile4(output, `${JSON.stringify(manifest, null, 2)}
|
|
2805
3008
|
`, { flag: "wx" });
|
|
2806
3009
|
printBootstrap(
|
|
2807
3010
|
{ output, manifest, remediation: (await bootstrapPlan(manifest)).remediation },
|
|
2808
3011
|
`Wrote ${output}. Next: run bootstrap preflight, then bootstrap handoff.`
|
|
2809
3012
|
);
|
|
3013
|
+
} else if (action === "upgrade") {
|
|
3014
|
+
const configPath = requiredOption("--config");
|
|
3015
|
+
const result = await upgradeBootstrapManifest(configPath);
|
|
3016
|
+
printBootstrap(
|
|
3017
|
+
result,
|
|
3018
|
+
`Updated desired-state metadata in ${configPath}.
|
|
3019
|
+
Changed: ${result.metadataChanges.length ? result.metadataChanges.map(({ path, before, after }) => `${path}: ${before ?? "missing"} -> ${after}`).join(", ") : "none (already current)"}.
|
|
3020
|
+
Review and commit this manifest before deployment.`
|
|
3021
|
+
);
|
|
2810
3022
|
} else {
|
|
2811
3023
|
const config = await bootstrapConfig();
|
|
2812
3024
|
if (action === "preflight") {
|
|
@@ -2831,9 +3043,9 @@ try {
|
|
|
2831
3043
|
config,
|
|
2832
3044
|
configPath,
|
|
2833
3045
|
workDir: option("--work-dir"),
|
|
2834
|
-
coreStack: coreStackPath ? JSON.parse(await
|
|
3046
|
+
coreStack: coreStackPath ? JSON.parse(await readFile6(coreStackPath, "utf8")) : void 0,
|
|
2835
3047
|
coreStackPath,
|
|
2836
|
-
githubOidcStack: githubStackPath ? JSON.parse(await
|
|
3048
|
+
githubOidcStack: githubStackPath ? JSON.parse(await readFile6(githubStackPath, "utf8")) : void 0,
|
|
2837
3049
|
githubOidcStackPath: githubStackPath,
|
|
2838
3050
|
runner
|
|
2839
3051
|
});
|
|
@@ -2855,9 +3067,9 @@ try {
|
|
|
2855
3067
|
config,
|
|
2856
3068
|
configPath,
|
|
2857
3069
|
workDir: option("--work-dir"),
|
|
2858
|
-
coreStack: coreStackPath ? JSON.parse(await
|
|
3070
|
+
coreStack: coreStackPath ? JSON.parse(await readFile6(coreStackPath, "utf8")) : void 0,
|
|
2859
3071
|
coreStackPath,
|
|
2860
|
-
githubOidcStack: githubStackPath ? JSON.parse(await
|
|
3072
|
+
githubOidcStack: githubStackPath ? JSON.parse(await readFile6(githubStackPath, "utf8")) : void 0,
|
|
2861
3073
|
githubOidcStackPath: githubStackPath,
|
|
2862
3074
|
runner
|
|
2863
3075
|
});
|
|
@@ -2872,8 +3084,8 @@ ${formatBootstrapRemediationText(handoff.remediation).join("\n")}`
|
|
|
2872
3084
|
const githubStackPath = requiredOption("--github-oidc-stack-json");
|
|
2873
3085
|
const verified = await verifyBootstrap({
|
|
2874
3086
|
config,
|
|
2875
|
-
coreStack: JSON.parse(await
|
|
2876
|
-
githubOidcStack: JSON.parse(await
|
|
3087
|
+
coreStack: JSON.parse(await readFile6(coreStackPath, "utf8")),
|
|
3088
|
+
githubOidcStack: JSON.parse(await readFile6(githubStackPath, "utf8"))
|
|
2877
3089
|
});
|
|
2878
3090
|
printBootstrap(
|
|
2879
3091
|
verified,
|
|
@@ -2904,6 +3116,20 @@ ${formatBootstrapRemediationText(template.remediation).join("\n")}`
|
|
|
2904
3116
|
if (outputFormat() === "text") {
|
|
2905
3117
|
console.log("AWS bootstrap plan (read-only)");
|
|
2906
3118
|
console.log(`Package version: ${plan.packageVersion}`);
|
|
3119
|
+
console.log(
|
|
3120
|
+
`Manifest desired state: ${plan.desiredState.manifest ? JSON.stringify(plan.desiredState.manifest) : "missing"}`
|
|
3121
|
+
);
|
|
3122
|
+
console.log(`Installed desired state: ${JSON.stringify(plan.desiredState.installed)}`);
|
|
3123
|
+
console.log(
|
|
3124
|
+
`Bootstrap template identity matches installed package: ${plan.desiredState.matches ? "yes" : "no"}`
|
|
3125
|
+
);
|
|
3126
|
+
console.log(
|
|
3127
|
+
`Package version provenance matches installed package: ${plan.desiredState.packageVersionMatches ? "yes" : "no (informational)"}`
|
|
3128
|
+
);
|
|
3129
|
+
if (!plan.desiredState.matches)
|
|
3130
|
+
console.log(
|
|
3131
|
+
`Run: npm exec -- ze-great-dashboard-aws bootstrap upgrade --config ${requiredOption("--config")}`
|
|
3132
|
+
);
|
|
2907
3133
|
for (const template of plan.packageTemplates) {
|
|
2908
3134
|
console.log(`
|
|
2909
3135
|
${template.kind}: ${template.path}`);
|
|
@@ -2923,7 +3149,10 @@ ${plan.notes.join("\n")}`);
|
|
|
2923
3149
|
requiredOption("--config");
|
|
2924
3150
|
const result = await checkBootstrap(
|
|
2925
3151
|
config,
|
|
2926
|
-
{
|
|
3152
|
+
{
|
|
3153
|
+
resourceDrift: args.includes("--resource-drift"),
|
|
3154
|
+
configPath: requiredOption("--config")
|
|
3155
|
+
},
|
|
2927
3156
|
{
|
|
2928
3157
|
execute: runner.execute
|
|
2929
3158
|
}
|
|
@@ -2940,7 +3169,7 @@ ${plan.notes.join("\n")}`);
|
|
|
2940
3169
|
if (kind === "github-oidc" && coreStackPath) {
|
|
2941
3170
|
coreOutputs = coreBootstrapOutputs(
|
|
2942
3171
|
deployedBootstrapStack(
|
|
2943
|
-
JSON.parse(await
|
|
3172
|
+
JSON.parse(await readFile6(coreStackPath, "utf8")),
|
|
2944
3173
|
bootstrapContractVersion(await bootstrapTemplate("core", mode))
|
|
2945
3174
|
)
|
|
2946
3175
|
);
|
|
@@ -2953,7 +3182,7 @@ ${plan.notes.join("\n")}`);
|
|
|
2953
3182
|
const deployedStackPath = option("--deployed-stack-json");
|
|
2954
3183
|
if (deployedStackPath) {
|
|
2955
3184
|
const stack = deployedBootstrapStack(
|
|
2956
|
-
JSON.parse(await
|
|
3185
|
+
JSON.parse(await readFile6(deployedStackPath, "utf8")),
|
|
2957
3186
|
bootstrapContractVersion(await bootstrapTemplate(kind, mode))
|
|
2958
3187
|
);
|
|
2959
3188
|
if (kind === "github-oidc" && !(stack.Parameters ?? []).some(
|
|
@@ -2964,7 +3193,7 @@ ${plan.notes.join("\n")}`);
|
|
|
2964
3193
|
);
|
|
2965
3194
|
parameters = mergeBootstrapParameters(supplied, stack.Parameters ?? []);
|
|
2966
3195
|
}
|
|
2967
|
-
await
|
|
3196
|
+
await writeFile4(output, `${JSON.stringify(parameters, null, 2)}
|
|
2968
3197
|
`);
|
|
2969
3198
|
const remediation = (await bootstrapPlan(config)).remediation;
|
|
2970
3199
|
printBootstrap(
|
|
@@ -3024,7 +3253,7 @@ ${shellCommand(awsCommand)}`,
|
|
|
3024
3253
|
);
|
|
3025
3254
|
} else {
|
|
3026
3255
|
throw new Error(
|
|
3027
|
-
"Usage: ze-great-dashboard-aws bootstrap init|preflight|plan|check|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|change-set --kind core|github-oidc [options]"
|
|
3256
|
+
"Usage: ze-great-dashboard-aws bootstrap init|upgrade|preflight|plan|check|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|change-set --kind core|github-oidc [options]"
|
|
3028
3257
|
);
|
|
3029
3258
|
}
|
|
3030
3259
|
}
|
|
@@ -3034,7 +3263,7 @@ ${shellCommand(awsCommand)}`,
|
|
|
3034
3263
|
);
|
|
3035
3264
|
else {
|
|
3036
3265
|
const boardConfig = option("--board-config");
|
|
3037
|
-
const version = option("--version", process.env.DASHBOARD_VERSION ??
|
|
3266
|
+
const version = option("--version", process.env.DASHBOARD_VERSION ?? packageVersion2);
|
|
3038
3267
|
if (!boardConfig) throw new Error("--board-config is required");
|
|
3039
3268
|
if (!version)
|
|
3040
3269
|
throw new Error("Unable to determine the package version; pass --version explicitly");
|
|
@@ -3073,7 +3302,7 @@ ${shellCommand(awsCommand)}`,
|
|
|
3073
3302
|
resolvedParameters.findIndex(({ ParameterKey }) => ParameterKey === "ComputeMode"),
|
|
3074
3303
|
1
|
|
3075
3304
|
);
|
|
3076
|
-
await
|
|
3305
|
+
await writeFile4(
|
|
3077
3306
|
`${outputDir}/parameters.json`,
|
|
3078
3307
|
`${JSON.stringify(resolvedParameters, null, 2)}
|
|
3079
3308
|
`
|
|
@@ -3084,7 +3313,7 @@ ${shellCommand(awsCommand)}`,
|
|
|
3084
3313
|
artifactKey: metadata.artifactKey,
|
|
3085
3314
|
release: metadata
|
|
3086
3315
|
});
|
|
3087
|
-
await
|
|
3316
|
+
await writeFile4(`${outputDir}/deployment.json`, `${JSON.stringify(handoff, null, 2)}
|
|
3088
3317
|
`);
|
|
3089
3318
|
console.log(
|
|
3090
3319
|
`Packaged ${mode === "lambda" ? `${outputDir}/lambda.zip` : metadata.image} and ${outputDir}/template.yml`
|
package/dist/guided.d.ts
CHANGED
|
@@ -30,8 +30,20 @@ export type BootstrapInitInput = {
|
|
|
30
30
|
consumerGatewayStackName?: string;
|
|
31
31
|
runner?: CommandRunner;
|
|
32
32
|
};
|
|
33
|
+
export type BootstrapManifestUpgrade = {
|
|
34
|
+
manifest: BootstrapConfig;
|
|
35
|
+
desiredState: NonNullable<BootstrapConfig['desiredState']>;
|
|
36
|
+
changed: string[];
|
|
37
|
+
metadataChanges: {
|
|
38
|
+
path: string;
|
|
39
|
+
before?: string;
|
|
40
|
+
after: string;
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
33
43
|
/** Creates the non-secret manifest data; callers control where (and whether) it is written. */
|
|
34
44
|
export declare function scaffoldBootstrapManifest(input: BootstrapInitInput): Promise<BootstrapConfig>;
|
|
45
|
+
/** Explicitly updates only package-owned desired-state metadata; it never reads AWS. */
|
|
46
|
+
export declare function upgradeBootstrapManifest(path: string): Promise<BootstrapManifestUpgrade>;
|
|
35
47
|
/** Runs only named read-only discovery checks. Network/auth failures remain usable offline. */
|
|
36
48
|
export declare function bootstrapPreflight(input: {
|
|
37
49
|
config: BootstrapConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ComputeMode } from './bootstrap.js';
|
|
2
|
-
export { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind, type BootstrapPlan, type BootstrapTemplateInspection, bootstrapConsistency, bootstrapContractVersion, bootstrapPlan, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, type ComputeMode, computeMode, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requireComputeMode, requiredBootstrapParameters, resolveComputeMode, } from './bootstrap.js';
|
|
2
|
+
export { type BootstrapConfig, type BootstrapConsistency, type BootstrapDesiredState, type BootstrapDesiredStateComparison, type BootstrapKind, type BootstrapPlan, type BootstrapTemplateInspection, bootstrapConsistency, bootstrapContractVersion, bootstrapPlan, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, type ComputeMode, computeMode, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, installedBootstrapDesiredState, mergeBootstrapParameters, requireComputeMode, requiredBootstrapParameters, resolveComputeMode, } from './bootstrap.js';
|
|
3
3
|
export { type BootstrapCheck, type BootstrapCheckDependencies, type BootstrapResourceDifference, type BootstrapResourceDrift, type BootstrapResourceDriftResult, type BootstrapStackCheck, checkBootstrap, formatBootstrapCheckText, } from './bootstrap-check.js';
|
|
4
|
-
export { type BootstrapCheckStatus, type BootstrapGuideReport, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapGuideReport, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
|
|
4
|
+
export { type BootstrapCheckStatus, type BootstrapGuideReport, type BootstrapInitInput, type BootstrapManifestUpgrade, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapGuideReport, bootstrapPreflight, scaffoldBootstrapManifest, upgradeBootstrapManifest, } from './guided.js';
|
|
5
5
|
export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
|
|
6
|
-
export { type BootstrapAffectedStack, type BootstrapRemediation, type BootstrapRemediationInput, bootstrapRemediation, formatBootstrapRemediationText, } from './remediation.js';
|
|
6
|
+
export { type BootstrapAffectedStack, type BootstrapRecoveryCommand, type BootstrapRemediation, type BootstrapRemediationInput, bootstrapRemediation, formatBootstrapRemediationText, } from './remediation.js';
|
|
7
7
|
export type ReleaseMetadata = {
|
|
8
8
|
computeMode: ComputeMode;
|
|
9
9
|
dashboardVersion: string;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/aws/src/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { cp, mkdir as mkdir2, readFile as
|
|
3
|
+
import { cp, mkdir as mkdir2, readFile as readFile4, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
4
4
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -880,7 +880,34 @@ function bootstrapRemediation(config, input = {}) {
|
|
|
880
880
|
["github-oidc", config.githubOidc?.stackName]
|
|
881
881
|
].filter((entry) => Boolean(entry[1])).map(([kind, stackName2]) => ({ kind, stackName: stackName2 }));
|
|
882
882
|
const configPath = input.configPath ?? "manifest.json";
|
|
883
|
-
const checkCommand = `ze-great-dashboard-aws bootstrap check --config ${configPath} --format
|
|
883
|
+
const checkCommand = `npm exec -- ze-great-dashboard-aws bootstrap check --config ${configPath} --format text`;
|
|
884
|
+
const desiredStateUpdateCommand = `npm exec -- ze-great-dashboard-aws bootstrap upgrade --config ${configPath}`;
|
|
885
|
+
const desiredStateUpdateRequired = input.desiredStateUpdateRequired ?? false;
|
|
886
|
+
const jsonCheckCommand = `npm exec -- ze-great-dashboard-aws bootstrap check --config ${configPath} --format json`;
|
|
887
|
+
const region2 = config.region ?? "<region>";
|
|
888
|
+
const recoveryStacks = [
|
|
889
|
+
["core", config.core?.stackName],
|
|
890
|
+
["github-oidc", config.githubOidc?.stackName]
|
|
891
|
+
].filter((entry) => Boolean(entry[1]));
|
|
892
|
+
const captures = new Map(
|
|
893
|
+
recoveryStacks.map(([kind]) => [kind, `.bootstrap-work/${kind}-deployed-stack.json`])
|
|
894
|
+
);
|
|
895
|
+
const recoveryCommands = recoveryStacks.map(([kind, stackName2]) => ({
|
|
896
|
+
name: `capture-${kind}-stack`,
|
|
897
|
+
command: `mkdir -p .bootstrap-work && aws cloudformation describe-stacks --stack-name ${stackName2} --region ${region2} --output json --no-cli-pager > ${captures.get(kind)}`
|
|
898
|
+
}));
|
|
899
|
+
for (const [kind, stackName2] of recoveryStacks) {
|
|
900
|
+
const parameters = `.bootstrap-work/${kind}-bootstrap-parameters.json`;
|
|
901
|
+
recoveryCommands.push({
|
|
902
|
+
name: `preserve-${kind}-parameters`,
|
|
903
|
+
command: `npm exec -- ze-great-dashboard-aws bootstrap parameters --kind ${kind} --config ${configPath} --deployed-stack-json ${captures.get(kind)}${kind === "github-oidc" && captures.has("core") ? ` --core-stack-json ${captures.get("core")}` : ""} --output ${parameters}`
|
|
904
|
+
});
|
|
905
|
+
recoveryCommands.push({
|
|
906
|
+
name: `generate-${kind}-update-change-set`,
|
|
907
|
+
command: `npm exec -- ze-great-dashboard-aws bootstrap change-set --kind ${kind} --config ${configPath} --stack-name ${stackName2} --change-set-name repair-${kind} --change-set-type UPDATE --parameters ${parameters}`
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
recoveryCommands.push({ name: "revalidate-bootstrap", command: checkCommand });
|
|
884
911
|
const issues = input.issues ?? [];
|
|
885
912
|
const summary = input.summary ?? (issues.length ? `Bootstrap validation found ${issues.length} issue${issues.length === 1 ? "" : "s"}.` : "Bootstrap validation is ready for the next reviewed operation.");
|
|
886
913
|
return {
|
|
@@ -892,10 +919,22 @@ function bootstrapRemediation(config, input = {}) {
|
|
|
892
919
|
"Capture the resulting CloudFormation stack output for the next validation step."
|
|
893
920
|
],
|
|
894
921
|
upgradeSteps: [
|
|
922
|
+
...desiredStateUpdateRequired ? [`Bootstrap template identity changed; review and run: ${desiredStateUpdateCommand}`] : [],
|
|
895
923
|
"If the contract or template revision is stale, generate parameters from this installed package and create a reviewed UPDATE change set for each affected stack.",
|
|
896
924
|
"Review IAM actions, retained resources, parameters, and the change set before an administrator executes it."
|
|
897
925
|
],
|
|
898
|
-
|
|
926
|
+
recoveryCommands,
|
|
927
|
+
...desiredStateUpdateRequired ? {
|
|
928
|
+
desiredStateUpdateCommand,
|
|
929
|
+
desiredStateCheckpoint: "Review and commit the desired-state manifest mutation before the administrator deployment process consumes it; never copy deployed AWS values into desired state."
|
|
930
|
+
} : {},
|
|
931
|
+
reviewCheckpoints: [
|
|
932
|
+
"Review the captured stack JSON and preserved parameter files before generating any change set.",
|
|
933
|
+
"Inspect every generated UPDATE change set in CloudFormation; verify IAM actions, retained resources, and parameters before executing it.",
|
|
934
|
+
"Execute change sets only as an administrator, one affected stack at a time, then capture both stacks again."
|
|
935
|
+
],
|
|
936
|
+
runbookTarget: "docs/aws-bootstrap-upgrade.md",
|
|
937
|
+
revalidateCommand: jsonCheckCommand,
|
|
899
938
|
safetyNote
|
|
900
939
|
};
|
|
901
940
|
}
|
|
@@ -907,6 +946,17 @@ function formatBootstrapRemediationText(remediation) {
|
|
|
907
946
|
...remediation.immediateSteps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
908
947
|
"Upgrade steps:",
|
|
909
948
|
...remediation.upgradeSteps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
949
|
+
...remediation.desiredStateUpdateCommand ? [
|
|
950
|
+
`Desired-state update (repository mutation; review and commit):
|
|
951
|
+
$ ${remediation.desiredStateUpdateCommand}`,
|
|
952
|
+
`Desired-state checkpoint: ${remediation.desiredStateCheckpoint}`
|
|
953
|
+
] : [],
|
|
954
|
+
"Copy/paste recovery commands (review before execute):",
|
|
955
|
+
...remediation.recoveryCommands.map(({ name, command }) => ` ${name}:
|
|
956
|
+
$ ${command}`),
|
|
957
|
+
"Review checkpoints:",
|
|
958
|
+
...remediation.reviewCheckpoints.map((checkpoint, index) => ` ${index + 1}. ${checkpoint}`),
|
|
959
|
+
`Runbook: ${remediation.runbookTarget}`,
|
|
910
960
|
`Revalidate: ${remediation.revalidateCommand}`,
|
|
911
961
|
`Safety: ${remediation.safetyNote}`
|
|
912
962
|
];
|
|
@@ -933,6 +983,47 @@ async function bootstrapTemplatePath(kind, mode = "lambda") {
|
|
|
933
983
|
async function bootstrapTemplate(kind, mode = "lambda") {
|
|
934
984
|
return readFile2(await bootstrapTemplatePath(kind, mode), "utf8");
|
|
935
985
|
}
|
|
986
|
+
async function installedBootstrapDesiredState(config = {}) {
|
|
987
|
+
const packageManifest = JSON.parse(
|
|
988
|
+
await readFile2(new URL("../package.json", import.meta.url), "utf8")
|
|
989
|
+
);
|
|
990
|
+
return desiredStateFromTemplates(
|
|
991
|
+
packageVersion(packageManifest),
|
|
992
|
+
await installedTemplateInspections(computeMode(config))
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
function packageVersion(packageManifest) {
|
|
996
|
+
return typeof packageManifest.version === "string" ? packageManifest.version : "unknown";
|
|
997
|
+
}
|
|
998
|
+
function desiredStateFromTemplates(version, packageTemplates) {
|
|
999
|
+
const template = (kind) => packageTemplates.find((entry) => entry.kind === kind);
|
|
1000
|
+
const core = template("core");
|
|
1001
|
+
const githubOidc = template("github-oidc");
|
|
1002
|
+
if (!core || !githubOidc) throw new Error("Installed package is missing a bootstrap template");
|
|
1003
|
+
if (!core.templateRevision || !githubOidc.templateRevision)
|
|
1004
|
+
throw new Error("Installed bootstrap template has no template revision");
|
|
1005
|
+
return {
|
|
1006
|
+
packageVersion: version,
|
|
1007
|
+
templates: {
|
|
1008
|
+
core: { contractVersion: core.contractVersion, templateRevision: core.templateRevision },
|
|
1009
|
+
githubOidc: {
|
|
1010
|
+
contractVersion: githubOidc.contractVersion,
|
|
1011
|
+
templateRevision: githubOidc.templateRevision
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
async function installedTemplateInspections(mode) {
|
|
1017
|
+
return Promise.all(
|
|
1018
|
+
["core", "github-oidc"].map(async (kind) => {
|
|
1019
|
+
const [path, template] = await Promise.all([
|
|
1020
|
+
bootstrapTemplatePath(kind, mode),
|
|
1021
|
+
bootstrapTemplate(kind, mode)
|
|
1022
|
+
]);
|
|
1023
|
+
return inspectTemplate(kind, path, template);
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
936
1027
|
function bootstrapContractVersion(template) {
|
|
937
1028
|
const version = template.match(/BootstrapContractVersion:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
|
|
938
1029
|
if (!version) throw new Error("Bootstrap template has no BootstrapContractVersion output");
|
|
@@ -966,26 +1057,36 @@ async function bootstrapPlan(config) {
|
|
|
966
1057
|
const packageManifest = JSON.parse(
|
|
967
1058
|
await readFile2(new URL("../package.json", import.meta.url), "utf8")
|
|
968
1059
|
);
|
|
969
|
-
const packageTemplates = await
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
bootstrapTemplatePath(kind, mode),
|
|
974
|
-
bootstrapTemplate(kind, mode)
|
|
975
|
-
]);
|
|
976
|
-
return inspectTemplate(kind, path, template);
|
|
977
|
-
})
|
|
1060
|
+
const packageTemplates = await installedTemplateInspections(computeMode(config));
|
|
1061
|
+
const installedDesiredState = desiredStateFromTemplates(
|
|
1062
|
+
packageVersion(packageManifest),
|
|
1063
|
+
packageTemplates
|
|
978
1064
|
);
|
|
1065
|
+
const desiredStateMatches = Boolean(config.desiredState) && ["core", "githubOidc"].every((kind) => {
|
|
1066
|
+
const manifestTemplate = config.desiredState?.templates[kind];
|
|
1067
|
+
const installedTemplate = installedDesiredState.templates[kind];
|
|
1068
|
+
return manifestTemplate?.contractVersion === installedTemplate.contractVersion && manifestTemplate?.templateRevision === installedTemplate.templateRevision;
|
|
1069
|
+
});
|
|
1070
|
+
const packageVersionMatches = config.desiredState?.packageVersion === installedDesiredState.packageVersion;
|
|
979
1071
|
return {
|
|
980
|
-
packageVersion:
|
|
1072
|
+
packageVersion: installedDesiredState.packageVersion,
|
|
981
1073
|
packageTemplates,
|
|
982
1074
|
configuration: config,
|
|
1075
|
+
desiredState: {
|
|
1076
|
+
...config.desiredState ? { manifest: config.desiredState } : {},
|
|
1077
|
+
installed: installedDesiredState,
|
|
1078
|
+
matches: desiredStateMatches,
|
|
1079
|
+
packageVersionMatches,
|
|
1080
|
+
ok: desiredStateMatches,
|
|
1081
|
+
mismatches: desiredStateMatches ? [] : ["manifest bootstrap template identity differs from installed package"]
|
|
1082
|
+
},
|
|
983
1083
|
notes: [
|
|
984
1084
|
"Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
|
|
985
1085
|
"Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
|
|
986
1086
|
"This plan performs no AWS or GitHub mutations."
|
|
987
1087
|
],
|
|
988
1088
|
remediation: bootstrapRemediation(config, {
|
|
1089
|
+
desiredStateUpdateRequired: !desiredStateMatches,
|
|
989
1090
|
nextOperation: "Review the installed templates, then run bootstrap preflight before creating a change set."
|
|
990
1091
|
})
|
|
991
1092
|
};
|
|
@@ -1128,7 +1229,15 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
1128
1229
|
|
|
1129
1230
|
// packages/aws/src/bootstrap-check.ts
|
|
1130
1231
|
function formatBootstrapCheckText(result) {
|
|
1131
|
-
const lines = [
|
|
1232
|
+
const lines = [
|
|
1233
|
+
`AWS bootstrap consistency (${result.packageVersion})`,
|
|
1234
|
+
`Desired state: ${result.desiredState.ok ? "PASS" : "FAIL"}`,
|
|
1235
|
+
`Package provenance: ${result.desiredState.packageVersionMatches ? "matches installed package" : `manifest ${result.desiredState.manifest?.packageVersion ?? "missing"}, installed ${result.desiredState.installed.packageVersion} (informational)`}`,
|
|
1236
|
+
...(result.desiredState.mismatches ?? []).map((mismatch) => ` mismatch: ${mismatch}`),
|
|
1237
|
+
...result.remediation.desiredStateUpdateCommand ? [
|
|
1238
|
+
`Desired-state update command (review, commit, then deploy): ${result.remediation.desiredStateUpdateCommand}`
|
|
1239
|
+
] : []
|
|
1240
|
+
];
|
|
1132
1241
|
for (const stack of result.stacks) {
|
|
1133
1242
|
lines.push(
|
|
1134
1243
|
`${stack.consistency.ok && (!stack.resourceDrift || stack.resourceDrift.ok) ? "PASS" : "FAIL"} ${stack.kind}: ${stack.stackName} (${stack.stackStatus ?? "unavailable"})`,
|
|
@@ -1178,6 +1287,17 @@ function outputs(stack) {
|
|
|
1178
1287
|
).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
|
|
1179
1288
|
);
|
|
1180
1289
|
}
|
|
1290
|
+
function desiredStateShape(value2) {
|
|
1291
|
+
if (!value2 || typeof value2 !== "object") return false;
|
|
1292
|
+
const state = value2;
|
|
1293
|
+
const templates2 = state.templates;
|
|
1294
|
+
if (typeof state.packageVersion !== "string" || !templates2 || typeof templates2 !== "object")
|
|
1295
|
+
return false;
|
|
1296
|
+
return ["core", "githubOidc"].every((kind) => {
|
|
1297
|
+
const template = templates2[kind];
|
|
1298
|
+
return Boolean(template) && typeof template === "object" && typeof template.contractVersion === "string" && typeof template.templateRevision === "string";
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1181
1301
|
async function resourceDrift(stackName2, region2, dependencies) {
|
|
1182
1302
|
try {
|
|
1183
1303
|
const detected = parseJson(
|
|
@@ -1311,6 +1431,26 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1311
1431
|
"github-oidc": config.githubOidc?.stackName
|
|
1312
1432
|
};
|
|
1313
1433
|
const plan = await bootstrapPlan(config);
|
|
1434
|
+
const desiredStateMismatches = [];
|
|
1435
|
+
const declared = config.desiredState;
|
|
1436
|
+
if (!desiredStateShape(declared))
|
|
1437
|
+
desiredStateMismatches.push(
|
|
1438
|
+
`manifest desiredState metadata is ${declared ? "malformed" : "missing"}; run the explicit bootstrap upgrade command`
|
|
1439
|
+
);
|
|
1440
|
+
else {
|
|
1441
|
+
for (const kind of ["core", "githubOidc"]) {
|
|
1442
|
+
const wanted = declared.templates[kind];
|
|
1443
|
+
const installed = plan.desiredState.installed.templates[kind];
|
|
1444
|
+
if (!wanted || wanted.contractVersion !== installed.contractVersion)
|
|
1445
|
+
desiredStateMismatches.push(
|
|
1446
|
+
`${kind} contract is ${wanted?.contractVersion ?? "missing"}; installed package has ${installed.contractVersion}`
|
|
1447
|
+
);
|
|
1448
|
+
if (!wanted || wanted.templateRevision !== installed.templateRevision)
|
|
1449
|
+
desiredStateMismatches.push(
|
|
1450
|
+
`${kind} template revision is ${wanted?.templateRevision ?? "missing"}; installed package has ${installed.templateRevision}`
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1314
1454
|
const responses = await Promise.all(
|
|
1315
1455
|
["core", "github-oidc"].map(async (kind) => {
|
|
1316
1456
|
const stackName2 = stackNames[kind];
|
|
@@ -1346,8 +1486,8 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1346
1486
|
kind,
|
|
1347
1487
|
config,
|
|
1348
1488
|
response,
|
|
1349
|
-
template.contractVersion,
|
|
1350
|
-
template.templateRevision,
|
|
1489
|
+
(desiredStateShape(declared) ? declared.templates[kind === "core" ? "core" : "githubOidc"].contractVersion : void 0) ?? template.contractVersion,
|
|
1490
|
+
(desiredStateShape(declared) ? declared.templates[kind === "core" ? "core" : "githubOidc"].templateRevision : void 0) ?? template.templateRevision,
|
|
1351
1491
|
coreOutputValues
|
|
1352
1492
|
);
|
|
1353
1493
|
const checked = {
|
|
@@ -1372,12 +1512,22 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1372
1512
|
({ consistency, resourceDrift: resourceDrift2 }) => !consistency.ok || Boolean(resourceDrift2 && !resourceDrift2.ok)
|
|
1373
1513
|
);
|
|
1374
1514
|
return {
|
|
1375
|
-
ok: stacks.every(
|
|
1515
|
+
ok: desiredStateMismatches.length === 0 && stacks.every(
|
|
1376
1516
|
({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
|
|
1377
1517
|
),
|
|
1378
1518
|
packageVersion: plan.packageVersion,
|
|
1379
1519
|
stacks,
|
|
1520
|
+
desiredState: {
|
|
1521
|
+
ok: desiredStateMismatches.length === 0,
|
|
1522
|
+
mismatches: desiredStateMismatches,
|
|
1523
|
+
...desiredStateShape(declared) ? { manifest: declared } : {},
|
|
1524
|
+
installed: plan.desiredState.installed,
|
|
1525
|
+
matches: desiredStateMismatches.length === 0,
|
|
1526
|
+
packageVersionMatches: declared?.packageVersion === plan.desiredState.installed.packageVersion
|
|
1527
|
+
},
|
|
1380
1528
|
remediation: bootstrapRemediation(config, {
|
|
1529
|
+
configPath: options.configPath,
|
|
1530
|
+
desiredStateUpdateRequired: desiredStateMismatches.length > 0,
|
|
1381
1531
|
summary: failed.length ? `Bootstrap validation failed for ${failed.length} stack${failed.length === 1 ? "" : "s"}.` : "Bootstrap stacks are consistent and ready for the deployment check.",
|
|
1382
1532
|
affectedStacks: (failed.length ? failed : stacks).map(
|
|
1383
1533
|
({ kind, stackName: stackName2, consistency, resourceDrift: resourceDrift2 }) => ({
|
|
@@ -1395,6 +1545,9 @@ async function checkBootstrap(config, options, dependencies) {
|
|
|
1395
1545
|
};
|
|
1396
1546
|
}
|
|
1397
1547
|
|
|
1548
|
+
// packages/aws/src/guided.ts
|
|
1549
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1550
|
+
|
|
1398
1551
|
// packages/aws/src/handoff.ts
|
|
1399
1552
|
var githubSubjectKeys = [
|
|
1400
1553
|
"repository_owner",
|
|
@@ -1887,6 +2040,7 @@ async function scaffoldBootstrapManifest(input) {
|
|
|
1887
2040
|
if (!repositoryId)
|
|
1888
2041
|
throw new Error("--github-repository-id is required when GitHub is unavailable");
|
|
1889
2042
|
return {
|
|
2043
|
+
desiredState: await installedBootstrapDesiredState({ mode: input.mode ?? "lambda" }),
|
|
1890
2044
|
region: region2,
|
|
1891
2045
|
core: {
|
|
1892
2046
|
stackName: `${slug}-bootstrap`,
|
|
@@ -1905,6 +2059,55 @@ async function scaffoldBootstrapManifest(input) {
|
|
|
1905
2059
|
}
|
|
1906
2060
|
};
|
|
1907
2061
|
}
|
|
2062
|
+
async function upgradeBootstrapManifest(path) {
|
|
2063
|
+
let parsed;
|
|
2064
|
+
try {
|
|
2065
|
+
parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Unable to read bootstrap manifest ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2072
|
+
throw new Error(`Bootstrap manifest ${path} must contain a JSON object`);
|
|
2073
|
+
const existing = parsed;
|
|
2074
|
+
const missing = incomplete(existing);
|
|
2075
|
+
if (missing.length) throw new Error(`Bootstrap manifest is incomplete: ${missing.join(", ")}`);
|
|
2076
|
+
if (existing.mode && existing.mode !== "lambda" && existing.mode !== "ecs")
|
|
2077
|
+
throw new Error("Bootstrap manifest has invalid mode; expected lambda or ecs");
|
|
2078
|
+
const desiredState = await installedBootstrapDesiredState({ mode: existing.mode ?? "lambda" });
|
|
2079
|
+
const previous = existing.desiredState;
|
|
2080
|
+
const metadataChanges = [
|
|
2081
|
+
...previous?.packageVersion !== desiredState.packageVersion ? [
|
|
2082
|
+
{
|
|
2083
|
+
path: "packageVersion",
|
|
2084
|
+
before: previous?.packageVersion,
|
|
2085
|
+
after: desiredState.packageVersion
|
|
2086
|
+
}
|
|
2087
|
+
] : [],
|
|
2088
|
+
...["core", "githubOidc"].flatMap((kind) => [
|
|
2089
|
+
...previous?.templates?.[kind]?.contractVersion !== desiredState.templates[kind].contractVersion ? [
|
|
2090
|
+
{
|
|
2091
|
+
path: `templates.${kind}.contractVersion`,
|
|
2092
|
+
before: previous?.templates?.[kind]?.contractVersion,
|
|
2093
|
+
after: desiredState.templates[kind].contractVersion
|
|
2094
|
+
}
|
|
2095
|
+
] : [],
|
|
2096
|
+
...previous?.templates?.[kind]?.templateRevision !== desiredState.templates[kind].templateRevision ? [
|
|
2097
|
+
{
|
|
2098
|
+
path: `templates.${kind}.templateRevision`,
|
|
2099
|
+
before: previous?.templates?.[kind]?.templateRevision,
|
|
2100
|
+
after: desiredState.templates[kind].templateRevision
|
|
2101
|
+
}
|
|
2102
|
+
] : []
|
|
2103
|
+
])
|
|
2104
|
+
];
|
|
2105
|
+
const changed = metadataChanges.map(({ path: path2 }) => path2);
|
|
2106
|
+
const manifest = { ...existing, desiredState };
|
|
2107
|
+
await writeFile2(path, `${JSON.stringify(manifest, null, 2)}
|
|
2108
|
+
`);
|
|
2109
|
+
return { manifest, desiredState, changed, metadataChanges };
|
|
2110
|
+
}
|
|
1908
2111
|
function incomplete(config) {
|
|
1909
2112
|
const fields = [
|
|
1910
2113
|
["region", config.region],
|
|
@@ -2177,13 +2380,13 @@ async function packageLambda(options) {
|
|
|
2177
2380
|
computeMode: "lambda",
|
|
2178
2381
|
artifactChecksums: {
|
|
2179
2382
|
...release.metadata.artifactChecksums,
|
|
2180
|
-
"index.mjs": sha256(await
|
|
2383
|
+
"index.mjs": sha256(await readFile4(join2(runtimeDir, "index.mjs")))
|
|
2181
2384
|
}
|
|
2182
2385
|
};
|
|
2183
|
-
await
|
|
2386
|
+
await writeFile3(join2(runtimeDir, "release.json"), `${JSON.stringify(runtimeMetadata, null, 2)}
|
|
2184
2387
|
`);
|
|
2185
2388
|
const sums = Object.entries(runtimeMetadata.artifactChecksums).map(([name, digest]) => `${digest} ${name}`).join("\n");
|
|
2186
|
-
await
|
|
2389
|
+
await writeFile3(join2(runtimeDir, "SHA256SUMS"), `${sums}
|
|
2187
2390
|
`);
|
|
2188
2391
|
const lambdaPath = join2(outputDir, "lambda.zip");
|
|
2189
2392
|
const archiveFiles = ["SHA256SUMS", "board.yaml", "index.mjs", "release.json"].sort();
|
|
@@ -2192,24 +2395,24 @@ async function packageLambda(options) {
|
|
|
2192
2395
|
archiveFiles.map(async (name) => [
|
|
2193
2396
|
name,
|
|
2194
2397
|
[
|
|
2195
|
-
strToU8(await
|
|
2398
|
+
strToU8(await readFile4(join2(runtimeDir, name), "utf8")),
|
|
2196
2399
|
{ mtime: new Date(1980, 0, 1, 0, 0, 0), level: 9 }
|
|
2197
2400
|
]
|
|
2198
2401
|
])
|
|
2199
2402
|
)
|
|
2200
2403
|
);
|
|
2201
|
-
await
|
|
2404
|
+
await writeFile3(lambdaPath, zipSync(archive));
|
|
2202
2405
|
await rm(runtimeDir, { recursive: true, force: true });
|
|
2203
|
-
const lambdaChecksum = sha256(await
|
|
2406
|
+
const lambdaChecksum = sha256(await readFile4(lambdaPath));
|
|
2204
2407
|
const deploymentChecksum = sha256(JSON.stringify(runtimeMetadata));
|
|
2205
2408
|
const packagedRelease = {
|
|
2206
2409
|
...runtimeMetadata,
|
|
2207
2410
|
artifactChecksums: { ...runtimeMetadata.artifactChecksums, "lambda.zip": lambdaChecksum },
|
|
2208
2411
|
artifactKey: `lambda/${deploymentChecksum}.zip`
|
|
2209
2412
|
};
|
|
2210
|
-
await
|
|
2413
|
+
await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
|
|
2211
2414
|
`);
|
|
2212
|
-
await
|
|
2415
|
+
await writeFile3(
|
|
2213
2416
|
join2(outputDir, "template.yml"),
|
|
2214
2417
|
deploymentTemplate(await cloudFormationTemplate("lambda"), {
|
|
2215
2418
|
ComputeMode: "lambda",
|
|
@@ -2237,9 +2440,9 @@ async function packageEcs(options) {
|
|
|
2237
2440
|
image: imageReference,
|
|
2238
2441
|
artifactKey: `ecs/${sha256(JSON.stringify({ ...release.metadata, image: imageReference }))}`
|
|
2239
2442
|
};
|
|
2240
|
-
await
|
|
2443
|
+
await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
|
|
2241
2444
|
`);
|
|
2242
|
-
await
|
|
2445
|
+
await writeFile3(
|
|
2243
2446
|
join2(outputDir, "template.yml"),
|
|
2244
2447
|
deploymentTemplate(await cloudFormationTemplate("ecs"), {
|
|
2245
2448
|
ComputeMode: "ecs",
|
|
@@ -2247,16 +2450,16 @@ async function packageEcs(options) {
|
|
|
2247
2450
|
DashboardVersion: packagedRelease.dashboardVersion
|
|
2248
2451
|
})
|
|
2249
2452
|
);
|
|
2250
|
-
await
|
|
2453
|
+
await writeFile3(
|
|
2251
2454
|
join2(outputDir, "SHA256SUMS"),
|
|
2252
|
-
`${sha256(await
|
|
2455
|
+
`${sha256(await readFile4(join2(outputDir, "release.json")))} release.json
|
|
2253
2456
|
`
|
|
2254
2457
|
);
|
|
2255
2458
|
return packagedRelease;
|
|
2256
2459
|
}
|
|
2257
2460
|
async function publishClientAssets(options) {
|
|
2258
2461
|
const assetsDir = resolve2(options.assetsDir);
|
|
2259
|
-
await
|
|
2462
|
+
await readFile4(join2(assetsDir, "index.html"));
|
|
2260
2463
|
const assetPath = `${options.assetsBaseUrl.replace(/\/+$/, "")}/dashboard/${options.version}`;
|
|
2261
2464
|
await run("aws", [
|
|
2262
2465
|
"s3",
|
|
@@ -2279,7 +2482,7 @@ async function publishClientAssets(options) {
|
|
|
2279
2482
|
return assetPath;
|
|
2280
2483
|
}
|
|
2281
2484
|
async function cloudFormationTemplate(mode = "lambda") {
|
|
2282
|
-
return
|
|
2485
|
+
return readFile4(
|
|
2283
2486
|
fileURLToPath2(
|
|
2284
2487
|
new URL(mode === "ecs" ? "../template-ecs.yml" : "../template.yml", import.meta.url)
|
|
2285
2488
|
),
|
|
@@ -2306,6 +2509,7 @@ export {
|
|
|
2306
2509
|
formatBootstrapCheckText,
|
|
2307
2510
|
formatBootstrapRemediationText,
|
|
2308
2511
|
githubOidcProvider,
|
|
2512
|
+
installedBootstrapDesiredState,
|
|
2309
2513
|
mergeBootstrapParameters,
|
|
2310
2514
|
packageEcs,
|
|
2311
2515
|
packageLambda,
|
|
@@ -2314,5 +2518,6 @@ export {
|
|
|
2314
2518
|
requiredBootstrapParameters,
|
|
2315
2519
|
resolveComputeMode,
|
|
2316
2520
|
scaffoldBootstrapManifest,
|
|
2521
|
+
upgradeBootstrapManifest,
|
|
2317
2522
|
verifyBootstrap
|
|
2318
2523
|
};
|
package/dist/remediation.d.ts
CHANGED
|
@@ -4,11 +4,20 @@ export type BootstrapAffectedStack = {
|
|
|
4
4
|
stackName: string;
|
|
5
5
|
issue?: string;
|
|
6
6
|
};
|
|
7
|
+
export type BootstrapRecoveryCommand = {
|
|
8
|
+
name: string;
|
|
9
|
+
command: string;
|
|
10
|
+
};
|
|
7
11
|
export type BootstrapRemediation = {
|
|
8
12
|
failureSummary: string;
|
|
9
13
|
affectedStacks: BootstrapAffectedStack[];
|
|
10
14
|
immediateSteps: string[];
|
|
11
15
|
upgradeSteps: string[];
|
|
16
|
+
desiredStateUpdateCommand?: string;
|
|
17
|
+
desiredStateCheckpoint?: string;
|
|
18
|
+
recoveryCommands: BootstrapRecoveryCommand[];
|
|
19
|
+
reviewCheckpoints: string[];
|
|
20
|
+
runbookTarget: string;
|
|
12
21
|
revalidateCommand: string;
|
|
13
22
|
safetyNote: string;
|
|
14
23
|
};
|
|
@@ -18,6 +27,7 @@ export type BootstrapRemediationInput = {
|
|
|
18
27
|
configPath?: string;
|
|
19
28
|
nextOperation?: string;
|
|
20
29
|
issues?: string[];
|
|
30
|
+
desiredStateUpdateRequired?: boolean;
|
|
21
31
|
};
|
|
22
32
|
export declare function bootstrapRemediation(config: BootstrapConfig, input?: BootstrapRemediationInput): BootstrapRemediation;
|
|
23
33
|
export declare function formatBootstrapRemediationText(remediation: BootstrapRemediation): string[];
|