@continuous-excellence/ze-great-dashboard-aws 0.14.3 → 0.14.4

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.
@@ -1,4 +1,5 @@
1
1
  import { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind } from './bootstrap.js';
2
+ import { type BootstrapRemediation } from './remediation.js';
2
3
  export type BootstrapResourceDifference = {
3
4
  path?: string;
4
5
  differenceType?: string;
@@ -32,6 +33,7 @@ export type BootstrapCheck = {
32
33
  ok: boolean;
33
34
  packageVersion: string;
34
35
  stacks: BootstrapStackCheck[];
36
+ remediation: BootstrapRemediation;
35
37
  };
36
38
  export type BootstrapCheckDependencies = {
37
39
  execute(command: string, args: string[]): Promise<string>;
@@ -1,4 +1,5 @@
1
1
  import { type ComputeMode } from './compute-mode.js';
2
+ import { type BootstrapRemediation } from './remediation.js';
2
3
  export { type ComputeMode, computeMode, resolveComputeMode } from './compute-mode.js';
3
4
  export type BootstrapKind = 'core' | 'github-oidc';
4
5
  export type CloudFormationParameterValue = {
@@ -55,6 +56,7 @@ export type BootstrapPlan = {
55
56
  packageTemplates: BootstrapTemplateInspection[];
56
57
  configuration: BootstrapConfig;
57
58
  notes: string[];
59
+ remediation: BootstrapRemediation;
58
60
  };
59
61
  export type BootstrapConsistency = {
60
62
  ok: boolean;
package/dist/cli.js CHANGED
@@ -159,6 +159,46 @@ function sha256(value2) {
159
159
  return createHash("sha256").update(value2).digest("hex");
160
160
  }
161
161
 
162
+ // packages/aws/src/remediation.ts
163
+ var safetyNote = "AWS mutations are emitted for explicit administrator review and invocation; this library never executes them.";
164
+ function bootstrapRemediation(config, input = {}) {
165
+ const affectedStacks = input.affectedStacks ?? [
166
+ ["core", config.core?.stackName],
167
+ ["github-oidc", config.githubOidc?.stackName]
168
+ ].filter((entry) => Boolean(entry[1])).map(([kind, stackName2]) => ({ kind, stackName: stackName2 }));
169
+ const configPath = input.configPath ?? "manifest.json";
170
+ const checkCommand = `ze-great-dashboard-aws bootstrap check --config ${configPath} --format json`;
171
+ const issues = input.issues ?? [];
172
+ 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.");
173
+ return {
174
+ failureSummary: summary,
175
+ affectedStacks,
176
+ immediateSteps: [
177
+ ...issues.length ? ["Review each reported mismatch, access error, or drift finding."] : [],
178
+ input.nextOperation ?? "Run the next bootstrap operation from the generated handoff.",
179
+ "Capture the resulting CloudFormation stack output for the next validation step."
180
+ ],
181
+ upgradeSteps: [
182
+ "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.",
183
+ "Review IAM actions, retained resources, parameters, and the change set before an administrator executes it."
184
+ ],
185
+ revalidateCommand: checkCommand,
186
+ safetyNote
187
+ };
188
+ }
189
+ function formatBootstrapRemediationText(remediation) {
190
+ return [
191
+ `Remediation: ${remediation.failureSummary}`,
192
+ `Affected stacks: ${remediation.affectedStacks.length ? remediation.affectedStacks.map(({ kind, stackName: stackName2, issue }) => `${kind}=${stackName2}${issue ? ` (${issue})` : ""}`).join(", ") : "none identified"}`,
193
+ "Immediate steps:",
194
+ ...remediation.immediateSteps.map((step, index) => ` ${index + 1}. ${step}`),
195
+ "Upgrade steps:",
196
+ ...remediation.upgradeSteps.map((step, index) => ` ${index + 1}. ${step}`),
197
+ `Revalidate: ${remediation.revalidateCommand}`,
198
+ `Safety: ${remediation.safetyNote}`
199
+ ];
200
+ }
201
+
162
202
  // packages/aws/src/bootstrap.ts
163
203
  var templates = {
164
204
  core: {
@@ -231,7 +271,10 @@ async function bootstrapPlan(config) {
231
271
  "Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
232
272
  "Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
233
273
  "This plan performs no AWS or GitHub mutations."
234
- ]
274
+ ],
275
+ remediation: bootstrapRemediation(config, {
276
+ nextOperation: "Review the installed templates, then run bootstrap preflight before creating a change set."
277
+ })
235
278
  };
236
279
  }
237
280
  function capturedStack(input) {
@@ -1118,6 +1161,7 @@ function formatBootstrapCheckText(result) {
1118
1161
  );
1119
1162
  }
1120
1163
  }
1164
+ lines.push("", ...formatBootstrapRemediationText(result.remediation));
1121
1165
  return `${lines.join("\n")}
1122
1166
  `;
1123
1167
  }
@@ -1337,12 +1381,30 @@ async function checkBootstrap(config, options, dependencies) {
1337
1381
  return checked;
1338
1382
  })
1339
1383
  );
1384
+ const failed = stacks.filter(
1385
+ ({ consistency, resourceDrift: resourceDrift2 }) => !consistency.ok || Boolean(resourceDrift2 && !resourceDrift2.ok)
1386
+ );
1340
1387
  return {
1341
1388
  ok: stacks.every(
1342
1389
  ({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
1343
1390
  ),
1344
1391
  packageVersion: plan.packageVersion,
1345
- stacks
1392
+ stacks,
1393
+ remediation: bootstrapRemediation(config, {
1394
+ summary: failed.length ? `Bootstrap validation failed for ${failed.length} stack${failed.length === 1 ? "" : "s"}.` : "Bootstrap stacks are consistent and ready for the deployment check.",
1395
+ affectedStacks: (failed.length ? failed : stacks).map(
1396
+ ({ kind, stackName: stackName2, consistency, resourceDrift: resourceDrift2 }) => ({
1397
+ kind,
1398
+ stackName: stackName2,
1399
+ ...!consistency.ok ? { issue: consistency.mismatches[0] ?? "consistency mismatch" } : resourceDrift2 && !resourceDrift2.ok ? { issue: `resource drift: ${resourceDrift2.status}` } : {}
1400
+ })
1401
+ ),
1402
+ nextOperation: failed.length ? "Apply only reviewed administrator-approved bootstrap updates, then capture both stacks again." : "Run the deployment or release check that consumes these bootstrap outputs.",
1403
+ issues: failed.flatMap(({ consistency, resourceDrift: resourceDrift2 }) => [
1404
+ ...consistency.mismatches,
1405
+ ...resourceDrift2 && !resourceDrift2.ok ? [`resource drift: ${resourceDrift2.status}`] : []
1406
+ ])
1407
+ })
1346
1408
  };
1347
1409
  }
1348
1410
 
@@ -1559,6 +1621,10 @@ async function bootstrapHandoff(input) {
1559
1621
  templatePath,
1560
1622
  parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1561
1623
  workDir: input.workDir
1624
+ }),
1625
+ remediation: bootstrapRemediation(input.config, {
1626
+ configPath: input.configPath,
1627
+ nextOperation: "Review and execute the core bootstrap change set, then capture the core stack."
1562
1628
  })
1563
1629
  };
1564
1630
  }
@@ -1602,7 +1668,11 @@ async function bootstrapHandoff(input) {
1602
1668
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1603
1669
  input.config,
1604
1670
  input.runner
1605
- )
1671
+ ),
1672
+ remediation: bootstrapRemediation(input.config, {
1673
+ configPath: input.configPath,
1674
+ nextOperation: "Review and execute the GitHub OIDC change set, then capture the GitHub OIDC stack."
1675
+ })
1606
1676
  };
1607
1677
  }
1608
1678
  const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
@@ -1620,7 +1690,12 @@ async function bootstrapHandoff(input) {
1620
1690
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1621
1691
  ],
1622
1692
  commands: [],
1623
- prerequisite
1693
+ prerequisite,
1694
+ remediation: bootstrapRemediation(input.config, {
1695
+ configPath: input.configPath,
1696
+ summary: "The GitHub Environment immutable-subject prerequisite is not verified.",
1697
+ nextOperation: "A GitHub administrator must complete and verify the immutable-subject migration, then rerun bootstrap handoff."
1698
+ })
1624
1699
  };
1625
1700
  return {
1626
1701
  phase: "application-gateway",
@@ -1631,7 +1706,11 @@ async function bootstrapHandoff(input) {
1631
1706
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1632
1707
  ],
1633
1708
  commands: [],
1634
- prerequisite
1709
+ prerequisite,
1710
+ remediation: bootstrapRemediation(input.config, {
1711
+ configPath: input.configPath,
1712
+ nextOperation: "Configure and verify the consumer gateway, then run bootstrap verify and the deployment check."
1713
+ })
1635
1714
  };
1636
1715
  }
1637
1716
  function assertEqual(actual, expected, label) {
@@ -1751,7 +1830,10 @@ async function verifyBootstrap(input) {
1751
1830
  "--body",
1752
1831
  executionRole
1753
1832
  ]
1754
- ]
1833
+ ],
1834
+ remediation: bootstrapRemediation(input.config, {
1835
+ nextOperation: "Set the reviewed GitHub Environment variables, then run bootstrap check before deployment."
1836
+ })
1755
1837
  };
1756
1838
  }
1757
1839
 
@@ -1869,7 +1951,16 @@ async function bootstrapPreflight(input) {
1869
1951
  checks.push(
1870
1952
  missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
1871
1953
  );
1872
- if (missing.length) return { ready: false, checks };
1954
+ if (missing.length)
1955
+ return {
1956
+ ready: false,
1957
+ checks,
1958
+ remediation: bootstrapRemediation(input.config, {
1959
+ summary: "Bootstrap manifest configuration is incomplete.",
1960
+ issues: missing,
1961
+ nextOperation: "Complete the missing manifest fields, then rerun bootstrap preflight."
1962
+ })
1963
+ };
1873
1964
  const runner = input.runner;
1874
1965
  const identityRaw = await optional(runner, "aws", [
1875
1966
  "sts",
@@ -1977,9 +2068,15 @@ async function bootstrapPreflight(input) {
1977
2068
  status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
1978
2069
  detail: subject.detail
1979
2070
  });
2071
+ const ready = !checks.some(({ status }) => status === "missing" || status === "mismatch");
1980
2072
  return {
1981
- ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
1982
- checks
2073
+ ready,
2074
+ checks,
2075
+ remediation: bootstrapRemediation(input.config, {
2076
+ summary: ready ? "Bootstrap preflight is ready." : "Bootstrap preflight found configuration or identity issues.",
2077
+ issues: checks.filter(({ status }) => status === "missing" || status === "mismatch").map(({ detail }) => detail),
2078
+ nextOperation: ready ? "Run bootstrap plan, then create and review the next bootstrap change set." : "Resolve the reported configuration or identity issues, then rerun bootstrap preflight."
2079
+ })
1983
2080
  };
1984
2081
  }
1985
2082
  function quote(args2) {
@@ -2024,6 +2121,7 @@ async function bootstrapGuide(input) {
2024
2121
  );
2025
2122
  for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
2026
2123
  }
2124
+ lines.push("", ...formatBootstrapRemediationText(plan.remediation));
2027
2125
  return `${lines.join("\n")}
2028
2126
  `;
2029
2127
  }
@@ -2357,7 +2455,15 @@ async function runDoctor(options, dependencies = actualDependencies) {
2357
2455
  checkResult.detail = `WARNING: ${checkResult.detail}`;
2358
2456
  }
2359
2457
  }
2360
- return checks;
2458
+ const remediation = bootstrapRemediation(
2459
+ {},
2460
+ {
2461
+ summary: checks.some(({ ok }) => !ok) ? "Deployment doctor found issues that require operator review." : "Deployment doctor is healthy; proceed to the deployment check.",
2462
+ issues: checks.filter(({ ok }) => !ok).map(({ detail }) => detail),
2463
+ nextOperation: checks.some(({ ok }) => !ok) ? "Resolve failed checks, then rerun the deployment doctor and bootstrap check." : "Run bootstrap check before deploying the release."
2464
+ }
2465
+ );
2466
+ return checks.map((check2) => ({ ...check2, remediation }));
2361
2467
  }
2362
2468
 
2363
2469
  // packages/aws/src/cli.ts
@@ -2451,6 +2557,20 @@ function bootstrapStackName(kind, config) {
2451
2557
  function shellCommand(command) {
2452
2558
  return command.map((argument) => `'${argument.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
2453
2559
  }
2560
+ function outputFormat(defaultFormat = "json") {
2561
+ if (args.includes("--format-shell")) return "shell";
2562
+ const format = option("--format", defaultFormat);
2563
+ if (format !== "json" && format !== "text") throw new Error("--format must be json or text");
2564
+ return format;
2565
+ }
2566
+ function printBootstrap(value2, text, shell, defaultFormat = "json") {
2567
+ const format = outputFormat(defaultFormat);
2568
+ if (format === "shell") {
2569
+ if (!shell) throw new Error("--format-shell is only supported for command handoffs");
2570
+ console.log(shell);
2571
+ } else if (format === "text") console.log(text);
2572
+ else console.log(JSON.stringify(value2));
2573
+ }
2454
2574
  async function templateParameters(mode = "lambda") {
2455
2575
  const template = await cloudFormationTemplate(mode);
2456
2576
  const block = template.match(/^Parameters:\n[\s\S]*?(?=^[A-Za-z][A-Za-z0-9]*:\s*$)/m)?.[0];
@@ -2586,10 +2706,14 @@ try {
2586
2706
  packageVersion
2587
2707
  }
2588
2708
  );
2589
- for (const check of checks)
2590
- console.log(
2591
- `${check.warning ? "WARN" : check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`
2592
- );
2709
+ const remediation = checks[0]?.remediation;
2710
+ if (outputFormat("text") === "text") {
2711
+ for (const check of checks)
2712
+ console.log(
2713
+ `${check.warning ? "WARN" : check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`
2714
+ );
2715
+ if (remediation) console.log(formatBootstrapRemediationText(remediation).join("\n"));
2716
+ } else console.log(JSON.stringify({ checks, remediation }));
2593
2717
  if (checks.some(({ ok }) => !ok)) process.exitCode = 1;
2594
2718
  } else if (args[0] === "parameters") {
2595
2719
  const output = option("--output", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
@@ -2636,7 +2760,7 @@ try {
2636
2760
  });
2637
2761
  await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
2638
2762
  `);
2639
- console.log(JSON.stringify({ output }));
2763
+ printBootstrap({ output }, `Wrote ${output}. Next: run the generated deployment handoff.`);
2640
2764
  } else if (args[0] === "bootstrap") {
2641
2765
  const action = args[1];
2642
2766
  if (action === "init") {
@@ -2670,7 +2794,10 @@ try {
2670
2794
  });
2671
2795
  await writeFile3(output, `${JSON.stringify(manifest, null, 2)}
2672
2796
  `, { flag: "wx" });
2673
- console.log(JSON.stringify({ output, manifest }));
2797
+ printBootstrap(
2798
+ { output, manifest, remediation: (await bootstrapPlan(manifest)).remediation },
2799
+ `Wrote ${output}. Next: run bootstrap preflight, then bootstrap handoff.`
2800
+ );
2674
2801
  } else {
2675
2802
  const config = await bootstrapConfig();
2676
2803
  if (action === "preflight") {
@@ -2692,10 +2819,16 @@ try {
2692
2819
  }
2693
2820
  };
2694
2821
  const result = await bootstrapPreflight({ config, runner });
2695
- if (option("--format") === "text")
2696
- for (const check of result.checks)
2697
- console.log(`${check.status.toUpperCase()} ${check.name}: ${check.detail}`);
2698
- else console.log(JSON.stringify(result));
2822
+ printBootstrap(
2823
+ result,
2824
+ [
2825
+ ...result.checks.map(
2826
+ (check) => `${check.status.toUpperCase()} ${check.name}: ${check.detail}`
2827
+ ),
2828
+ "",
2829
+ ...formatBootstrapRemediationText(result.remediation)
2830
+ ].join("\n")
2831
+ );
2699
2832
  if (!result.ready) process.exitCode = 1;
2700
2833
  } else if (action === "guide") {
2701
2834
  const configPath = requiredOption("--config");
@@ -2708,17 +2841,31 @@ try {
2708
2841
  return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2709
2842
  }
2710
2843
  };
2711
- process.stdout.write(
2712
- await bootstrapGuide({
2713
- config,
2714
- configPath,
2715
- workDir: option("--work-dir"),
2716
- coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2717
- coreStackPath,
2718
- githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2719
- githubOidcStackPath: githubStackPath,
2720
- runner
2721
- })
2844
+ const handoff = await bootstrapHandoff({
2845
+ config,
2846
+ configPath,
2847
+ workDir: option("--work-dir"),
2848
+ coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2849
+ coreStackPath,
2850
+ githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2851
+ githubOidcStackPath: githubStackPath,
2852
+ runner
2853
+ });
2854
+ const guide = await bootstrapGuide({
2855
+ config,
2856
+ configPath,
2857
+ workDir: option("--work-dir"),
2858
+ coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2859
+ coreStackPath,
2860
+ githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2861
+ githubOidcStackPath: githubStackPath,
2862
+ runner
2863
+ });
2864
+ printBootstrap(
2865
+ { handoff, guide, remediation: handoff.remediation },
2866
+ guide,
2867
+ void 0,
2868
+ "text"
2722
2869
  );
2723
2870
  } else if (action === "handoff") {
2724
2871
  const configPath = requiredOption("--config");
@@ -2731,48 +2878,57 @@ try {
2731
2878
  return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2732
2879
  }
2733
2880
  };
2734
- console.log(
2735
- JSON.stringify(
2736
- await bootstrapHandoff({
2737
- config,
2738
- configPath,
2739
- workDir: option("--work-dir"),
2740
- coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2741
- coreStackPath,
2742
- githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2743
- githubOidcStackPath: githubStackPath,
2744
- runner
2745
- })
2746
- )
2881
+ const handoff = await bootstrapHandoff({
2882
+ config,
2883
+ configPath,
2884
+ workDir: option("--work-dir"),
2885
+ coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2886
+ coreStackPath,
2887
+ githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2888
+ githubOidcStackPath: githubStackPath,
2889
+ runner
2890
+ });
2891
+ printBootstrap(
2892
+ handoff,
2893
+ `${JSON.stringify(handoff, null, 2)}
2894
+ ${formatBootstrapRemediationText(handoff.remediation).join("\n")}`
2747
2895
  );
2748
2896
  } else if (action === "verify") {
2749
2897
  requiredOption("--config");
2750
2898
  const coreStackPath = requiredOption("--core-stack-json");
2751
2899
  const githubStackPath = requiredOption("--github-oidc-stack-json");
2752
- console.log(
2753
- JSON.stringify(
2754
- await verifyBootstrap({
2755
- config,
2756
- coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
2757
- githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
2758
- })
2759
- )
2900
+ const verified = await verifyBootstrap({
2901
+ config,
2902
+ coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
2903
+ githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
2904
+ });
2905
+ printBootstrap(
2906
+ verified,
2907
+ `Bootstrap verified.
2908
+ ${formatBootstrapRemediationText(verified.remediation).join("\n")}`
2760
2909
  );
2761
2910
  } else if (action === "template") {
2762
2911
  const kind = bootstrapKind();
2763
2912
  const mode = requireComputeMode(config, option("--mode"));
2764
- console.log(
2765
- JSON.stringify({
2766
- kind,
2767
- mode,
2768
- template: await bootstrapTemplatePath(kind, mode),
2769
- contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind, mode))
2913
+ const template = {
2914
+ kind,
2915
+ mode,
2916
+ template: await bootstrapTemplatePath(kind, mode),
2917
+ contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind, mode)),
2918
+ remediation: bootstrapRemediation(config, {
2919
+ nextOperation: `Review the ${kind} template, then run bootstrap parameters and create a reviewed change set.`
2770
2920
  })
2921
+ };
2922
+ printBootstrap(
2923
+ template,
2924
+ `Template: ${template.template}
2925
+ Contract: ${template.contractVersion}
2926
+ ${formatBootstrapRemediationText(template.remediation).join("\n")}`
2771
2927
  );
2772
2928
  } else if (action === "plan") {
2773
2929
  requiredOption("--config");
2774
2930
  const plan = await bootstrapPlan(config);
2775
- if (option("--format") === "text") {
2931
+ if (outputFormat() === "text") {
2776
2932
  console.log("AWS bootstrap plan (read-only)");
2777
2933
  console.log(`Package version: ${plan.packageVersion}`);
2778
2934
  for (const template of plan.packageTemplates) {
@@ -2788,6 +2944,7 @@ ${template.kind}: ${template.path}`);
2788
2944
  }
2789
2945
  console.log(`
2790
2946
  ${plan.notes.join("\n")}`);
2947
+ console.log(formatBootstrapRemediationText(plan.remediation).join("\n"));
2791
2948
  } else console.log(JSON.stringify(plan));
2792
2949
  } else if (action === "check") {
2793
2950
  requiredOption("--config");
@@ -2802,7 +2959,7 @@ ${plan.notes.join("\n")}`);
2802
2959
  }
2803
2960
  }
2804
2961
  );
2805
- if (option("--format") === "text") process.stdout.write(formatBootstrapCheckText(result));
2962
+ if (outputFormat() === "text") process.stdout.write(formatBootstrapCheckText(result));
2806
2963
  else console.log(JSON.stringify(result));
2807
2964
  if (!result.ok) process.exitCode = 1;
2808
2965
  } else if (action === "parameters") {
@@ -2840,8 +2997,15 @@ ${plan.notes.join("\n")}`);
2840
2997
  }
2841
2998
  await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
2842
2999
  `);
2843
- console.log(
2844
- JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
3000
+ printBootstrap(
3001
+ {
3002
+ output,
3003
+ kind,
3004
+ preservedDeployedValues: Boolean(deployedStackPath),
3005
+ remediation: (await bootstrapPlan(config)).remediation
3006
+ },
3007
+ `Wrote ${output}. Next: review and execute the generated ${kind} change set.
3008
+ ${formatBootstrapRemediationText((await bootstrapPlan(config)).remediation).join("\n")}`
2845
3009
  );
2846
3010
  } else if (action === "change-set") {
2847
3011
  const kind = bootstrapKind();
@@ -2871,17 +3035,22 @@ ${plan.notes.join("\n")}`);
2871
3035
  ...region2 ? ["--region", region2] : [],
2872
3036
  "--no-cli-pager"
2873
3037
  ];
2874
- console.log(
2875
- JSON.stringify({
2876
- kind,
2877
- packageVersion: plan.packageVersion,
2878
- contractVersion: template?.contractVersion,
2879
- templateRevision: template?.templateRevision,
2880
- templateSha256: template?.sha256,
2881
- reviewRequired: true,
2882
- awsCommand,
2883
- shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
2884
- })
3038
+ const handoff = {
3039
+ kind,
3040
+ packageVersion: plan.packageVersion,
3041
+ contractVersion: template?.contractVersion,
3042
+ templateRevision: template?.templateRevision,
3043
+ templateSha256: template?.sha256,
3044
+ reviewRequired: true,
3045
+ awsCommand,
3046
+ shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0,
3047
+ remediation: (await bootstrapPlan(config)).remediation
3048
+ };
3049
+ printBootstrap(
3050
+ handoff,
3051
+ `Run the reviewed change set command:
3052
+ ${shellCommand(awsCommand)}`,
3053
+ shellCommand(awsCommand)
2885
3054
  );
2886
3055
  } else {
2887
3056
  throw new Error(
package/dist/doctor.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { type BootstrapRemediation } from './remediation.js';
1
2
  export type DoctorCheck = {
2
3
  name: string;
3
4
  ok: boolean;
4
5
  detail: string;
5
6
  warning?: boolean;
7
+ remediation?: BootstrapRemediation;
6
8
  };
7
9
  export type DoctorDependencies = {
8
10
  execute(command: string, args: string[]): Promise<string>;
package/dist/guided.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type BootstrapConfig, type ComputeMode } from './bootstrap.js';
2
2
  import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
3
+ import { type BootstrapRemediation } from './remediation.js';
3
4
  export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
4
5
  export type BootstrapPreflightCheck = {
5
6
  name: string;
@@ -9,6 +10,7 @@ export type BootstrapPreflightCheck = {
9
10
  export type BootstrapPreflight = {
10
11
  ready: boolean;
11
12
  checks: BootstrapPreflightCheck[];
13
+ remediation: BootstrapRemediation;
12
14
  };
13
15
  export type BootstrapInitInput = {
14
16
  mode?: ComputeMode;
package/dist/handoff.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type BootstrapConfig } from './bootstrap.js';
2
+ import { type BootstrapRemediation } from './remediation.js';
2
3
  export type BootstrapPhase = 'core' | 'github-oidc' | 'github-environment' | 'application-gateway';
3
4
  export type CommandRunner = {
4
5
  execute(command: string, args: string[]): Promise<string>;
@@ -30,6 +31,7 @@ export type BootstrapHandoff = {
30
31
  reviewCheckpoints: string[];
31
32
  commands: HandoffCommand[];
32
33
  prerequisite?: ProviderPrerequisite;
34
+ remediation: BootstrapRemediation;
33
35
  };
34
36
  export type BootstrapVerification = {
35
37
  verified: true;
@@ -43,6 +45,7 @@ export type BootstrapVerification = {
43
45
  cloudFormationExecutionRoleArn: string;
44
46
  };
45
47
  githubEnvironmentInstructions: string[][];
48
+ remediation: BootstrapRemediation;
46
49
  };
47
50
  export declare const githubOidcProvider: BootstrapProvider;
48
51
  export declare function bootstrapHandoff(input: {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind, ty
3
3
  export { type BootstrapCheck, type BootstrapCheckDependencies, type BootstrapResourceDifference, type BootstrapResourceDrift, type BootstrapResourceDriftResult, type BootstrapStackCheck, checkBootstrap, formatBootstrapCheckText, } from './bootstrap-check.js';
4
4
  export { type BootstrapCheckStatus, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapPreflight, scaffoldBootstrapManifest, } 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
7
  export type ReleaseMetadata = {
7
8
  computeMode: ComputeMode;
8
9
  dashboardVersion: string;
package/dist/index.js CHANGED
@@ -872,6 +872,46 @@ function computeMode(config) {
872
872
  return resolveComputeMode({ persisted: config.mode });
873
873
  }
874
874
 
875
+ // packages/aws/src/remediation.ts
876
+ var safetyNote = "AWS mutations are emitted for explicit administrator review and invocation; this library never executes them.";
877
+ function bootstrapRemediation(config, input = {}) {
878
+ const affectedStacks = input.affectedStacks ?? [
879
+ ["core", config.core?.stackName],
880
+ ["github-oidc", config.githubOidc?.stackName]
881
+ ].filter((entry) => Boolean(entry[1])).map(([kind, stackName2]) => ({ kind, stackName: stackName2 }));
882
+ const configPath = input.configPath ?? "manifest.json";
883
+ const checkCommand = `ze-great-dashboard-aws bootstrap check --config ${configPath} --format json`;
884
+ const issues = input.issues ?? [];
885
+ 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
+ return {
887
+ failureSummary: summary,
888
+ affectedStacks,
889
+ immediateSteps: [
890
+ ...issues.length ? ["Review each reported mismatch, access error, or drift finding."] : [],
891
+ input.nextOperation ?? "Run the next bootstrap operation from the generated handoff.",
892
+ "Capture the resulting CloudFormation stack output for the next validation step."
893
+ ],
894
+ upgradeSteps: [
895
+ "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
+ "Review IAM actions, retained resources, parameters, and the change set before an administrator executes it."
897
+ ],
898
+ revalidateCommand: checkCommand,
899
+ safetyNote
900
+ };
901
+ }
902
+ function formatBootstrapRemediationText(remediation) {
903
+ return [
904
+ `Remediation: ${remediation.failureSummary}`,
905
+ `Affected stacks: ${remediation.affectedStacks.length ? remediation.affectedStacks.map(({ kind, stackName: stackName2, issue }) => `${kind}=${stackName2}${issue ? ` (${issue})` : ""}`).join(", ") : "none identified"}`,
906
+ "Immediate steps:",
907
+ ...remediation.immediateSteps.map((step, index) => ` ${index + 1}. ${step}`),
908
+ "Upgrade steps:",
909
+ ...remediation.upgradeSteps.map((step, index) => ` ${index + 1}. ${step}`),
910
+ `Revalidate: ${remediation.revalidateCommand}`,
911
+ `Safety: ${remediation.safetyNote}`
912
+ ];
913
+ }
914
+
875
915
  // packages/aws/src/bootstrap.ts
876
916
  var templates = {
877
917
  core: {
@@ -944,7 +984,10 @@ async function bootstrapPlan(config) {
944
984
  "Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
945
985
  "Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
946
986
  "This plan performs no AWS or GitHub mutations."
947
- ]
987
+ ],
988
+ remediation: bootstrapRemediation(config, {
989
+ nextOperation: "Review the installed templates, then run bootstrap preflight before creating a change set."
990
+ })
948
991
  };
949
992
  }
950
993
  function capturedStack(input) {
@@ -1105,6 +1148,7 @@ function formatBootstrapCheckText(result) {
1105
1148
  );
1106
1149
  }
1107
1150
  }
1151
+ lines.push("", ...formatBootstrapRemediationText(result.remediation));
1108
1152
  return `${lines.join("\n")}
1109
1153
  `;
1110
1154
  }
@@ -1324,12 +1368,30 @@ async function checkBootstrap(config, options, dependencies) {
1324
1368
  return checked;
1325
1369
  })
1326
1370
  );
1371
+ const failed = stacks.filter(
1372
+ ({ consistency, resourceDrift: resourceDrift2 }) => !consistency.ok || Boolean(resourceDrift2 && !resourceDrift2.ok)
1373
+ );
1327
1374
  return {
1328
1375
  ok: stacks.every(
1329
1376
  ({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
1330
1377
  ),
1331
1378
  packageVersion: plan.packageVersion,
1332
- stacks
1379
+ stacks,
1380
+ remediation: bootstrapRemediation(config, {
1381
+ 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
+ affectedStacks: (failed.length ? failed : stacks).map(
1383
+ ({ kind, stackName: stackName2, consistency, resourceDrift: resourceDrift2 }) => ({
1384
+ kind,
1385
+ stackName: stackName2,
1386
+ ...!consistency.ok ? { issue: consistency.mismatches[0] ?? "consistency mismatch" } : resourceDrift2 && !resourceDrift2.ok ? { issue: `resource drift: ${resourceDrift2.status}` } : {}
1387
+ })
1388
+ ),
1389
+ nextOperation: failed.length ? "Apply only reviewed administrator-approved bootstrap updates, then capture both stacks again." : "Run the deployment or release check that consumes these bootstrap outputs.",
1390
+ issues: failed.flatMap(({ consistency, resourceDrift: resourceDrift2 }) => [
1391
+ ...consistency.mismatches,
1392
+ ...resourceDrift2 && !resourceDrift2.ok ? [`resource drift: ${resourceDrift2.status}`] : []
1393
+ ])
1394
+ })
1333
1395
  };
1334
1396
  }
1335
1397
 
@@ -1546,6 +1608,10 @@ async function bootstrapHandoff(input) {
1546
1608
  templatePath,
1547
1609
  parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1548
1610
  workDir: input.workDir
1611
+ }),
1612
+ remediation: bootstrapRemediation(input.config, {
1613
+ configPath: input.configPath,
1614
+ nextOperation: "Review and execute the core bootstrap change set, then capture the core stack."
1549
1615
  })
1550
1616
  };
1551
1617
  }
@@ -1589,7 +1655,11 @@ async function bootstrapHandoff(input) {
1589
1655
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1590
1656
  input.config,
1591
1657
  input.runner
1592
- )
1658
+ ),
1659
+ remediation: bootstrapRemediation(input.config, {
1660
+ configPath: input.configPath,
1661
+ nextOperation: "Review and execute the GitHub OIDC change set, then capture the GitHub OIDC stack."
1662
+ })
1593
1663
  };
1594
1664
  }
1595
1665
  const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
@@ -1607,7 +1677,12 @@ async function bootstrapHandoff(input) {
1607
1677
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1608
1678
  ],
1609
1679
  commands: [],
1610
- prerequisite
1680
+ prerequisite,
1681
+ remediation: bootstrapRemediation(input.config, {
1682
+ configPath: input.configPath,
1683
+ summary: "The GitHub Environment immutable-subject prerequisite is not verified.",
1684
+ nextOperation: "A GitHub administrator must complete and verify the immutable-subject migration, then rerun bootstrap handoff."
1685
+ })
1611
1686
  };
1612
1687
  return {
1613
1688
  phase: "application-gateway",
@@ -1618,7 +1693,11 @@ async function bootstrapHandoff(input) {
1618
1693
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1619
1694
  ],
1620
1695
  commands: [],
1621
- prerequisite
1696
+ prerequisite,
1697
+ remediation: bootstrapRemediation(input.config, {
1698
+ configPath: input.configPath,
1699
+ nextOperation: "Configure and verify the consumer gateway, then run bootstrap verify and the deployment check."
1700
+ })
1622
1701
  };
1623
1702
  }
1624
1703
  function assertEqual(actual, expected, label) {
@@ -1738,7 +1817,10 @@ async function verifyBootstrap(input) {
1738
1817
  "--body",
1739
1818
  executionRole
1740
1819
  ]
1741
- ]
1820
+ ],
1821
+ remediation: bootstrapRemediation(input.config, {
1822
+ nextOperation: "Set the reviewed GitHub Environment variables, then run bootstrap check before deployment."
1823
+ })
1742
1824
  };
1743
1825
  }
1744
1826
 
@@ -1856,7 +1938,16 @@ async function bootstrapPreflight(input) {
1856
1938
  checks.push(
1857
1939
  missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
1858
1940
  );
1859
- if (missing.length) return { ready: false, checks };
1941
+ if (missing.length)
1942
+ return {
1943
+ ready: false,
1944
+ checks,
1945
+ remediation: bootstrapRemediation(input.config, {
1946
+ summary: "Bootstrap manifest configuration is incomplete.",
1947
+ issues: missing,
1948
+ nextOperation: "Complete the missing manifest fields, then rerun bootstrap preflight."
1949
+ })
1950
+ };
1860
1951
  const runner = input.runner;
1861
1952
  const identityRaw = await optional(runner, "aws", [
1862
1953
  "sts",
@@ -1964,9 +2055,15 @@ async function bootstrapPreflight(input) {
1964
2055
  status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
1965
2056
  detail: subject.detail
1966
2057
  });
2058
+ const ready = !checks.some(({ status }) => status === "missing" || status === "mismatch");
1967
2059
  return {
1968
- ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
1969
- checks
2060
+ ready,
2061
+ checks,
2062
+ remediation: bootstrapRemediation(input.config, {
2063
+ summary: ready ? "Bootstrap preflight is ready." : "Bootstrap preflight found configuration or identity issues.",
2064
+ issues: checks.filter(({ status }) => status === "missing" || status === "mismatch").map(({ detail }) => detail),
2065
+ nextOperation: ready ? "Run bootstrap plan, then create and review the next bootstrap change set." : "Resolve the reported configuration or identity issues, then rerun bootstrap preflight."
2066
+ })
1970
2067
  };
1971
2068
  }
1972
2069
  function quote(args) {
@@ -2011,6 +2108,7 @@ async function bootstrapGuide(input) {
2011
2108
  );
2012
2109
  for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
2013
2110
  }
2111
+ lines.push("", ...formatBootstrapRemediationText(plan.remediation));
2014
2112
  return `${lines.join("\n")}
2015
2113
  `;
2016
2114
  }
@@ -2193,6 +2291,7 @@ export {
2193
2291
  bootstrapHandoff,
2194
2292
  bootstrapPlan,
2195
2293
  bootstrapPreflight,
2294
+ bootstrapRemediation,
2196
2295
  bootstrapTemplate,
2197
2296
  bootstrapTemplatePath,
2198
2297
  bootstrapTemplateRevision,
@@ -2202,6 +2301,7 @@ export {
2202
2301
  coreBootstrapOutputs,
2203
2302
  deployedBootstrapStack,
2204
2303
  formatBootstrapCheckText,
2304
+ formatBootstrapRemediationText,
2205
2305
  githubOidcProvider,
2206
2306
  mergeBootstrapParameters,
2207
2307
  packageEcs,
@@ -0,0 +1,23 @@
1
+ import type { BootstrapConfig, BootstrapKind } from './bootstrap.js';
2
+ export type BootstrapAffectedStack = {
3
+ kind: BootstrapKind;
4
+ stackName: string;
5
+ issue?: string;
6
+ };
7
+ export type BootstrapRemediation = {
8
+ failureSummary: string;
9
+ affectedStacks: BootstrapAffectedStack[];
10
+ immediateSteps: string[];
11
+ upgradeSteps: string[];
12
+ revalidateCommand: string;
13
+ safetyNote: string;
14
+ };
15
+ export type BootstrapRemediationInput = {
16
+ summary?: string;
17
+ affectedStacks?: BootstrapAffectedStack[];
18
+ configPath?: string;
19
+ nextOperation?: string;
20
+ issues?: string[];
21
+ };
22
+ export declare function bootstrapRemediation(config: BootstrapConfig, input?: BootstrapRemediationInput): BootstrapRemediation;
23
+ 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.3",
3
+ "version": "0.14.4",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [