@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 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>;
@@ -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 readFile5, writeFile as writeFile3 } from "node:fs/promises";
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 readFile4 } from "node:fs/promises";
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 json`;
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
- revalidateCommand: checkCommand,
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 Promise.all(
259
- ["core", "github-oidc"].map(async (kind) => {
260
- const mode = computeMode(config);
261
- const [path, template] = await Promise.all([
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: typeof packageManifest.version === "string" ? packageManifest.version : "unknown",
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 readFile3, rm, writeFile as writeFile2 } from "node:fs/promises";
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 = [`AWS bootstrap consistency (${result.packageVersion})`];
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 readFile3(join2(runtimeDir, "index.mjs")))
2395
+ "index.mjs": sha256(await readFile4(join2(runtimeDir, "index.mjs")))
2193
2396
  }
2194
2397
  };
2195
- await writeFile2(join2(runtimeDir, "release.json"), `${JSON.stringify(runtimeMetadata, null, 2)}
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 writeFile2(join2(runtimeDir, "SHA256SUMS"), `${sums}
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 readFile3(join2(runtimeDir, name), "utf8")),
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 writeFile2(lambdaPath, zipSync(archive));
2416
+ await writeFile3(lambdaPath, zipSync(archive));
2214
2417
  await rm(runtimeDir, { recursive: true, force: true });
2215
- const lambdaChecksum = sha256(await readFile3(lambdaPath));
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 writeFile2(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2425
+ await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2223
2426
  `);
2224
- await writeFile2(
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 writeFile2(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2455
+ await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2253
2456
  `);
2254
- await writeFile2(
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 writeFile2(
2465
+ await writeFile3(
2263
2466
  join2(outputDir, "SHA256SUMS"),
2264
- `${sha256(await readFile3(join2(outputDir, "release.json")))} release.json
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 readFile3(join2(assetsDir, "index.html"));
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 readFile3(
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 readFile4(options.parametersPath, "utf8"))
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 readFile4(stackPath, "utf8"));
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 readFile5(new URL("../package.json", import.meta.url), "utf8")
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 readFile5(path, "utf8"));
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 readFile5(path, "utf8"));
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 packageVersion = await installedPackageVersion();
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", packageVersion);
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 writeFile3(output, `${JSON.stringify(parameters, null, 2)}
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 readFile5(output, "utf8");
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 writeFile3(output, `${JSON.stringify(manifest, null, 2)}
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 readFile5(coreStackPath, "utf8")) : void 0,
3046
+ coreStack: coreStackPath ? JSON.parse(await readFile6(coreStackPath, "utf8")) : void 0,
2835
3047
  coreStackPath,
2836
- githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
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 readFile5(coreStackPath, "utf8")) : void 0,
3070
+ coreStack: coreStackPath ? JSON.parse(await readFile6(coreStackPath, "utf8")) : void 0,
2859
3071
  coreStackPath,
2860
- githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
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 readFile5(coreStackPath, "utf8")),
2876
- githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
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
- { resourceDrift: args.includes("--resource-drift") },
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 readFile5(coreStackPath, "utf8")),
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 readFile5(deployedStackPath, "utf8")),
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 writeFile3(output, `${JSON.stringify(parameters, null, 2)}
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 ?? packageVersion);
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 writeFile3(
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 writeFile3(`${outputDir}/deployment.json`, `${JSON.stringify(handoff, null, 2)}
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 readFile3, rm, writeFile as writeFile2 } from "node:fs/promises";
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 json`;
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
- revalidateCommand: checkCommand,
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 Promise.all(
970
- ["core", "github-oidc"].map(async (kind) => {
971
- const mode = computeMode(config);
972
- const [path, template] = await Promise.all([
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: typeof packageManifest.version === "string" ? packageManifest.version : "unknown",
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 = [`AWS bootstrap consistency (${result.packageVersion})`];
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 readFile3(join2(runtimeDir, "index.mjs")))
2383
+ "index.mjs": sha256(await readFile4(join2(runtimeDir, "index.mjs")))
2181
2384
  }
2182
2385
  };
2183
- await writeFile2(join2(runtimeDir, "release.json"), `${JSON.stringify(runtimeMetadata, null, 2)}
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 writeFile2(join2(runtimeDir, "SHA256SUMS"), `${sums}
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 readFile3(join2(runtimeDir, name), "utf8")),
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 writeFile2(lambdaPath, zipSync(archive));
2404
+ await writeFile3(lambdaPath, zipSync(archive));
2202
2405
  await rm(runtimeDir, { recursive: true, force: true });
2203
- const lambdaChecksum = sha256(await readFile3(lambdaPath));
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 writeFile2(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2413
+ await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2211
2414
  `);
2212
- await writeFile2(
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 writeFile2(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2443
+ await writeFile3(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2241
2444
  `);
2242
- await writeFile2(
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 writeFile2(
2453
+ await writeFile3(
2251
2454
  join2(outputDir, "SHA256SUMS"),
2252
- `${sha256(await readFile3(join2(outputDir, "release.json")))} release.json
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 readFile3(join2(assetsDir, "index.html"));
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 readFile3(
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
  };
@@ -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[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.14.7",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [