@continuous-excellence/ze-great-dashboard-aws 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -862,6 +862,125 @@ function bootstrapTemplateRevision(template) {
862
862
  if (!revision) throw new Error("Bootstrap template has no BootstrapTemplateRevision output");
863
863
  return revision;
864
864
  }
865
+ function templateRevisionIfPresent(template) {
866
+ return template.match(/BootstrapTemplateRevision:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
867
+ }
868
+ function inspectTemplate(kind, path, template) {
869
+ const resourcesBlock = template.match(/^Resources:\n([\s\S]*?)(?=^[A-Za-z][A-Za-z0-9]*:\s*$)/m)?.[1] ?? "";
870
+ const resources = [
871
+ ...resourcesBlock.matchAll(/^ {2}([A-Za-z][A-Za-z0-9]+):\n {4}Type: ([^\n]+)$/gm)
872
+ ].map((match) => ({ logicalId: match[1], type: match[2] }));
873
+ const iamActions = [...template.matchAll(/^\s+Action: (?:\[([^\]]+)\]|([^\n]+))$/gm)].flatMap(([, list, scalar]) => (list ?? scalar ?? "").split(",").map((action) => action.trim())).filter(Boolean);
874
+ return {
875
+ kind,
876
+ path,
877
+ contractVersion: bootstrapContractVersion(template),
878
+ templateRevision: templateRevisionIfPresent(template),
879
+ sha256: sha256(template),
880
+ resources,
881
+ iamActions: [...new Set(iamActions)]
882
+ };
883
+ }
884
+ async function bootstrapPlan(config) {
885
+ const packageManifest = JSON.parse(
886
+ await readFile2(new URL("../package.json", import.meta.url), "utf8")
887
+ );
888
+ const packageTemplates = await Promise.all(
889
+ ["core", "github-oidc"].map(async (kind) => {
890
+ const [path, template] = await Promise.all([
891
+ bootstrapTemplatePath(kind),
892
+ bootstrapTemplate(kind)
893
+ ]);
894
+ return inspectTemplate(kind, path, template);
895
+ })
896
+ );
897
+ return {
898
+ packageVersion: typeof packageManifest.version === "string" ? packageManifest.version : "unknown",
899
+ packageTemplates,
900
+ configuration: config,
901
+ notes: [
902
+ "Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
903
+ "Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
904
+ "This plan performs no AWS or GitHub mutations."
905
+ ]
906
+ };
907
+ }
908
+ function capturedStack(input) {
909
+ const stack = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
910
+ return stack && typeof stack === "object" ? stack : {};
911
+ }
912
+ function outputValues(stack) {
913
+ return Object.fromEntries(
914
+ (stack.Outputs ?? []).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
915
+ );
916
+ }
917
+ function bootstrapConsistency(kind, config, stackInput, expectedContract, expectedRevision, coreOutputs = {}) {
918
+ const stack = capturedStack(stackInput);
919
+ const mismatches = [];
920
+ const expectedStackName = kind === "core" ? config.core?.stackName : config.githubOidc?.stackName;
921
+ if (stack.StackName !== expectedStackName)
922
+ mismatches.push(
923
+ `stack name is ${stack.StackName ?? "missing"}; expected ${expectedStackName ?? "missing"}`
924
+ );
925
+ const observedRegion = stack.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
926
+ if (observedRegion !== config.region)
927
+ mismatches.push(
928
+ `Region is ${observedRegion ?? "missing"}; expected ${config.region ?? "missing"}`
929
+ );
930
+ if (!["CREATE_COMPLETE", "UPDATE_COMPLETE"].includes(stack.StackStatus ?? ""))
931
+ mismatches.push(
932
+ `stack status is ${stack.StackStatus ?? "missing"}; expected a successful stable status`
933
+ );
934
+ const outputs2 = outputValues(stack);
935
+ if (outputs2.BootstrapContractVersion !== expectedContract)
936
+ mismatches.push(
937
+ `contract is ${outputs2.BootstrapContractVersion ?? "missing"}; expected ${expectedContract}`
938
+ );
939
+ if (expectedRevision && outputs2.BootstrapTemplateRevision !== expectedRevision)
940
+ mismatches.push(
941
+ `template revision is ${outputs2.BootstrapTemplateRevision ?? "missing"}; expected ${expectedRevision}`
942
+ );
943
+ const requiredOutputs = kind === "core" ? ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"] : ["GitHubDeployRoleArn"];
944
+ for (const key of requiredOutputs) if (!outputs2[key]) mismatches.push(`${key} output is missing`);
945
+ if (kind === "core") {
946
+ if (outputs2.ArtifactBucketName !== config.core?.artifactBucketName)
947
+ mismatches.push(
948
+ `ArtifactBucketName output is ${outputs2.ArtifactBucketName ?? "missing"}; expected ${config.core?.artifactBucketName ?? "missing"}`
949
+ );
950
+ if (outputs2.ApplicationStackName !== config.core?.applicationStackName)
951
+ mismatches.push(
952
+ `ApplicationStackName output is ${outputs2.ApplicationStackName ?? "missing"}; expected ${config.core?.applicationStackName ?? "missing"}`
953
+ );
954
+ }
955
+ const parameters = Object.fromEntries(
956
+ (stack.Parameters ?? []).map(({ ParameterKey, ParameterValue }) => [
957
+ ParameterKey,
958
+ ParameterValue
959
+ ])
960
+ );
961
+ const expectedParameters = kind === "core" ? {
962
+ ArtifactBucketName: config.core?.artifactBucketName,
963
+ ApplicationStackName: config.core?.applicationStackName,
964
+ DashboardFunctionName: config.core?.dashboardFunctionName,
965
+ RuntimeSecretArn: config.core?.runtimeSecretArn ?? "",
966
+ ArtifactKmsKeyArn: config.core?.artifactKmsKeyArn ?? ""
967
+ } : {
968
+ GitHubOidcProviderArn: config.githubOidc?.providerArn,
969
+ GitHubRepository: config.githubOidc?.repository,
970
+ GitHubOwnerId: config.githubOidc?.ownerId,
971
+ GitHubRepositoryId: config.githubOidc?.repositoryId,
972
+ GitHubEnvironment: config.githubOidc?.environment,
973
+ CoreBootstrapStackName: config.core?.stackName,
974
+ ApplicationStackName: config.core?.applicationStackName,
975
+ ArtifactBucketName: config.core?.artifactBucketName,
976
+ CloudFormationExecutionRoleArn: coreOutputs.CloudFormationExecutionRoleArn,
977
+ ConsumerGatewayStackName: config.githubOidc?.consumerGatewayStackName ?? ""
978
+ };
979
+ for (const [key, expected] of Object.entries(expectedParameters))
980
+ if (expected !== void 0 && parameters[key] !== expected)
981
+ mismatches.push(`${key} is ${parameters[key] ?? "missing"}; expected ${expected}`);
982
+ return { ok: mismatches.length === 0, mismatches };
983
+ }
865
984
  function mergeBootstrapParameters(requested, deployed) {
866
985
  const supplied = new Map(requested.map((entry) => [entry.ParameterKey, entry.ParameterValue]));
867
986
  const duplicates = requested.map((entry) => entry.ParameterKey);
@@ -909,6 +1028,7 @@ function requiredBootstrapParameters(kind, values) {
909
1028
  "GitHubOwnerId",
910
1029
  "GitHubRepositoryId",
911
1030
  "GitHubEnvironment",
1031
+ "CoreBootstrapStackName",
912
1032
  "ApplicationStackName",
913
1033
  "ArtifactBucketName",
914
1034
  "CloudFormationExecutionRoleArn"
@@ -919,6 +1039,256 @@ function requiredBootstrapParameters(kind, values) {
919
1039
  return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
920
1040
  }
921
1041
 
1042
+ // packages/aws/src/bootstrap-check.ts
1043
+ function formatBootstrapCheckText(result) {
1044
+ const lines = [`AWS bootstrap consistency (${result.packageVersion})`];
1045
+ for (const stack of result.stacks) {
1046
+ lines.push(
1047
+ `${stack.consistency.ok && (!stack.resourceDrift || stack.resourceDrift.ok) ? "PASS" : "FAIL"} ${stack.kind}: ${stack.stackName} (${stack.stackStatus ?? "unavailable"})`,
1048
+ ` template: contract ${stack.contractVersion}, revision ${stack.templateRevision ?? "none"}, sha256 ${stack.templateSha256}`
1049
+ );
1050
+ for (const mismatch of stack.consistency.mismatches) lines.push(` mismatch: ${mismatch}`);
1051
+ if (!stack.resourceDrift) continue;
1052
+ lines.push(` resource drift: ${stack.resourceDrift.status}`);
1053
+ if (stack.resourceDrift.reason) lines.push(` drift error: ${stack.resourceDrift.reason}`);
1054
+ for (const resource of stack.resourceDrift.resources) {
1055
+ lines.push(
1056
+ ` drifted resource: ${resource.logicalId} (${resource.resourceType ?? "unknown"}, ${resource.status ?? "unknown"})`
1057
+ );
1058
+ for (const difference of resource.differences)
1059
+ lines.push(
1060
+ ` ${difference.path ?? "unknown property"}: ${difference.differenceType ?? "changed"}; expected ${difference.expectedValue ?? "unknown"}, actual ${difference.actualValue ?? "unknown"}`
1061
+ );
1062
+ }
1063
+ }
1064
+ return `${lines.join("\n")}
1065
+ `;
1066
+ }
1067
+ function message(error) {
1068
+ return error instanceof Error ? error.message : String(error);
1069
+ }
1070
+ function parseJson(raw, operation) {
1071
+ try {
1072
+ const value2 = JSON.parse(raw);
1073
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2))
1074
+ throw new Error("response is not an object");
1075
+ return value2;
1076
+ } catch (error) {
1077
+ throw new Error(`${operation} returned invalid JSON: ${message(error)}`);
1078
+ }
1079
+ }
1080
+ function stackFrom(response) {
1081
+ const stacks = response.Stacks;
1082
+ return Array.isArray(stacks) && stacks[0] && typeof stacks[0] === "object" ? stacks[0] : void 0;
1083
+ }
1084
+ function outputs(stack) {
1085
+ const entries = stack?.Outputs;
1086
+ if (!Array.isArray(entries)) return {};
1087
+ return Object.fromEntries(
1088
+ entries.filter(
1089
+ (entry) => Boolean(entry && typeof entry === "object" && typeof entry.OutputKey === "string")
1090
+ ).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
1091
+ );
1092
+ }
1093
+ async function resourceDrift(stackName2, region2, dependencies) {
1094
+ try {
1095
+ const detected = parseJson(
1096
+ await dependencies.execute("aws", [
1097
+ "cloudformation",
1098
+ "detect-stack-drift",
1099
+ "--stack-name",
1100
+ stackName2,
1101
+ "--region",
1102
+ region2,
1103
+ "--output",
1104
+ "json",
1105
+ "--no-cli-pager"
1106
+ ]),
1107
+ "detect-stack-drift"
1108
+ );
1109
+ const detectionId = detected.StackDriftDetectionId;
1110
+ if (typeof detectionId !== "string")
1111
+ throw new Error("detect-stack-drift returned no StackDriftDetectionId");
1112
+ const now = dependencies.now ?? Date.now;
1113
+ const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
1114
+ const deadline = now() + 10 * 60 * 1e3;
1115
+ let status;
1116
+ while (true) {
1117
+ status = parseJson(
1118
+ await dependencies.execute("aws", [
1119
+ "cloudformation",
1120
+ "describe-stack-drift-detection-status",
1121
+ "--stack-drift-detection-id",
1122
+ detectionId,
1123
+ "--region",
1124
+ region2,
1125
+ "--output",
1126
+ "json",
1127
+ "--no-cli-pager"
1128
+ ]),
1129
+ "describe-stack-drift-detection-status"
1130
+ );
1131
+ if (status.DetectionStatus !== "DETECTION_IN_PROGRESS") break;
1132
+ if (now() >= deadline)
1133
+ return {
1134
+ ok: false,
1135
+ status: "TIMED_OUT",
1136
+ detectionId,
1137
+ reason: "CloudFormation drift detection did not complete within 10 minutes",
1138
+ resources: []
1139
+ };
1140
+ await sleep(5e3);
1141
+ }
1142
+ if (status.DetectionStatus !== "DETECTION_COMPLETE")
1143
+ return {
1144
+ ok: false,
1145
+ status: String(status.DetectionStatus ?? "UNKNOWN"),
1146
+ detectionId,
1147
+ reason: typeof status.DetectionStatusReason === "string" ? status.DetectionStatusReason : "CloudFormation drift detection failed",
1148
+ resources: []
1149
+ };
1150
+ const stackDriftStatus = String(status.StackDriftStatus ?? "UNKNOWN");
1151
+ if (stackDriftStatus !== "DRIFTED")
1152
+ return {
1153
+ ok: stackDriftStatus === "IN_SYNC",
1154
+ status: stackDriftStatus,
1155
+ detectionId,
1156
+ resources: []
1157
+ };
1158
+ const described = parseJson(
1159
+ await dependencies.execute("aws", [
1160
+ "cloudformation",
1161
+ "describe-stack-resource-drifts",
1162
+ "--stack-name",
1163
+ stackName2,
1164
+ "--stack-resource-drift-status-filters",
1165
+ "MODIFIED",
1166
+ "DELETED",
1167
+ "--region",
1168
+ region2,
1169
+ "--output",
1170
+ "json",
1171
+ "--no-cli-pager"
1172
+ ]),
1173
+ "describe-stack-resource-drifts"
1174
+ );
1175
+ const resources = Array.isArray(described.StackResourceDrifts) ? described.StackResourceDrifts.flatMap((entry) => {
1176
+ if (!entry || typeof entry !== "object") return [];
1177
+ const drift = entry;
1178
+ if (typeof drift.LogicalResourceId !== "string") return [];
1179
+ const differences = Array.isArray(drift.PropertyDifferences) ? drift.PropertyDifferences.flatMap((item) => {
1180
+ if (!item || typeof item !== "object") return [];
1181
+ const difference = item;
1182
+ return [
1183
+ {
1184
+ ...typeof difference.PropertyPath === "string" ? { path: difference.PropertyPath } : {},
1185
+ ...typeof difference.DifferenceType === "string" ? { differenceType: difference.DifferenceType } : {},
1186
+ ...typeof difference.ExpectedValue === "string" ? { expectedValue: difference.ExpectedValue } : {},
1187
+ ...typeof difference.ActualValue === "string" ? { actualValue: difference.ActualValue } : {}
1188
+ }
1189
+ ];
1190
+ }) : [];
1191
+ return [
1192
+ {
1193
+ logicalId: drift.LogicalResourceId,
1194
+ ...typeof drift.ResourceType === "string" ? { resourceType: drift.ResourceType } : {},
1195
+ ...typeof drift.StackResourceDriftStatus === "string" ? { status: drift.StackResourceDriftStatus } : {},
1196
+ differences
1197
+ }
1198
+ ];
1199
+ }) : [];
1200
+ return { ok: false, status: stackDriftStatus, detectionId, resources };
1201
+ } catch (error) {
1202
+ return { ok: false, status: "ERROR", reason: message(error), resources: [] };
1203
+ }
1204
+ }
1205
+ async function checkBootstrap(config, options, dependencies) {
1206
+ const requiredManifestValues = [
1207
+ ["region", config.region],
1208
+ ["core.stackName", config.core?.stackName],
1209
+ ["core.artifactBucketName", config.core?.artifactBucketName],
1210
+ ["core.applicationStackName", config.core?.applicationStackName],
1211
+ ["core.dashboardFunctionName", config.core?.dashboardFunctionName],
1212
+ ["githubOidc.stackName", config.githubOidc?.stackName],
1213
+ ["githubOidc.providerArn", config.githubOidc?.providerArn],
1214
+ ["githubOidc.repository", config.githubOidc?.repository],
1215
+ ["githubOidc.ownerId", config.githubOidc?.ownerId],
1216
+ ["githubOidc.repositoryId", config.githubOidc?.repositoryId],
1217
+ ["githubOidc.environment", config.githubOidc?.environment]
1218
+ ];
1219
+ const missing = requiredManifestValues.filter(([, value2]) => !value2).map(([name]) => name);
1220
+ if (missing.length) throw new Error(`Bootstrap manifest is missing: ${missing.join(", ")}`);
1221
+ const stackNames = {
1222
+ core: config.core?.stackName,
1223
+ "github-oidc": config.githubOidc?.stackName
1224
+ };
1225
+ const plan = await bootstrapPlan(config);
1226
+ const responses = await Promise.all(
1227
+ ["core", "github-oidc"].map(async (kind) => {
1228
+ const stackName2 = stackNames[kind];
1229
+ try {
1230
+ const response = parseJson(
1231
+ await dependencies.execute("aws", [
1232
+ "cloudformation",
1233
+ "describe-stacks",
1234
+ "--stack-name",
1235
+ stackName2,
1236
+ "--region",
1237
+ config.region,
1238
+ "--output",
1239
+ "json",
1240
+ "--no-cli-pager"
1241
+ ]),
1242
+ "describe-stacks"
1243
+ );
1244
+ return { kind, stackName: stackName2, response };
1245
+ } catch (error) {
1246
+ return { kind, stackName: stackName2, error: message(error) };
1247
+ }
1248
+ })
1249
+ );
1250
+ const coreResponse = responses.find(({ kind }) => kind === "core")?.response;
1251
+ const coreOutputValues = outputs(stackFrom(coreResponse ?? {}));
1252
+ const stacks = await Promise.all(
1253
+ responses.map(async ({ kind, stackName: stackName2, response, error }) => {
1254
+ const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
1255
+ if (!template) throw new Error(`Installed package has no ${kind} bootstrap template`);
1256
+ const stack = stackFrom(response ?? {});
1257
+ const consistency = error ? { ok: false, mismatches: [`unable to describe stack: ${error}`] } : bootstrapConsistency(
1258
+ kind,
1259
+ config,
1260
+ response,
1261
+ template.contractVersion,
1262
+ template.templateRevision,
1263
+ coreOutputValues
1264
+ );
1265
+ const checked = {
1266
+ kind,
1267
+ stackName: stackName2,
1268
+ ...typeof stack?.StackStatus === "string" ? { stackStatus: stack.StackStatus } : {},
1269
+ contractVersion: template.contractVersion,
1270
+ templateRevision: template.templateRevision,
1271
+ templateSha256: template.sha256,
1272
+ consistency
1273
+ };
1274
+ if (options.resourceDrift && !error)
1275
+ checked.resourceDrift = await resourceDrift(
1276
+ stackName2,
1277
+ config.region,
1278
+ dependencies
1279
+ );
1280
+ return checked;
1281
+ })
1282
+ );
1283
+ return {
1284
+ ok: stacks.every(
1285
+ ({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
1286
+ ),
1287
+ packageVersion: plan.packageVersion,
1288
+ stacks
1289
+ };
1290
+ }
1291
+
922
1292
  // packages/aws/src/handoff.ts
923
1293
  var githubSubjectKeys = [
924
1294
  "repository_owner",
@@ -927,7 +1297,7 @@ var githubSubjectKeys = [
927
1297
  "repository_id",
928
1298
  "context"
929
1299
  ];
930
- function outputValues(stack) {
1300
+ function outputValues2(stack) {
931
1301
  return Object.fromEntries(
932
1302
  (stack.Outputs ?? []).filter(
933
1303
  (entry) => typeof entry.OutputKey === "string" && typeof entry.OutputValue === "string"
@@ -963,11 +1333,17 @@ function stackName(config, kind) {
963
1333
  `${kind}.stackName`
964
1334
  );
965
1335
  }
1336
+ function workFile(workDir, name) {
1337
+ return workDir ? `${workDir.replace(/\/+$/, "")}/${name}` : name;
1338
+ }
966
1339
  function changeSetCommands(input) {
967
1340
  const name = `${input.kind}-initial-review`;
968
1341
  const stack = stackName(input.config, input.kind);
969
1342
  const common = ["--stack-name", stack, "--region", region(input.config)];
970
1343
  const parameters = [
1344
+ "npm",
1345
+ "exec",
1346
+ "--",
971
1347
  "ze-great-dashboard-aws",
972
1348
  "bootstrap",
973
1349
  "parameters",
@@ -1036,7 +1412,7 @@ function changeSetCommands(input) {
1036
1412
  },
1037
1413
  {
1038
1414
  name: "capture-stack",
1039
- captureFile: `${input.kind}-deployed-stack.json`,
1415
+ captureFile: workFile(input.workDir, `${input.kind}-deployed-stack.json`),
1040
1416
  args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
1041
1417
  }
1042
1418
  ];
@@ -1090,7 +1466,7 @@ async function contracts() {
1090
1466
  }
1091
1467
  async function bootstrapHandoff(input) {
1092
1468
  const expectedContracts = await contracts();
1093
- const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
1469
+ const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
1094
1470
  const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
1095
1471
  const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
1096
1472
  if (!core) {
@@ -1104,9 +1480,10 @@ async function bootstrapHandoff(input) {
1104
1480
  phase: "core",
1105
1481
  expectedContracts,
1106
1482
  templatePath,
1107
- parameterPath: "core-bootstrap.json",
1483
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1108
1484
  expectedOutputs: [
1109
1485
  "BootstrapContractVersion",
1486
+ "BootstrapTemplateRevision",
1110
1487
  "ArtifactBucketName",
1111
1488
  "ApplicationStackName",
1112
1489
  "CloudFormationExecutionRoleArn"
@@ -1121,7 +1498,8 @@ async function bootstrapHandoff(input) {
1121
1498
  config: input.config,
1122
1499
  configPath: input.configPath,
1123
1500
  templatePath,
1124
- parameterPath: "core-bootstrap.json"
1501
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1502
+ workDir: input.workDir
1125
1503
  })
1126
1504
  };
1127
1505
  }
@@ -1133,6 +1511,7 @@ async function bootstrapHandoff(input) {
1133
1511
  GitHubOwnerId: input.config.githubOidc?.ownerId,
1134
1512
  GitHubRepositoryId: input.config.githubOidc?.repositoryId,
1135
1513
  GitHubEnvironment: input.config.githubOidc?.environment,
1514
+ CoreBootstrapStackName: input.config.core?.stackName,
1136
1515
  ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
1137
1516
  ...coreBootstrapOutputs(core)
1138
1517
  });
@@ -1141,7 +1520,7 @@ async function bootstrapHandoff(input) {
1141
1520
  phase: "github-oidc",
1142
1521
  expectedContracts,
1143
1522
  templatePath,
1144
- parameterPath: "github-oidc-bootstrap.json",
1523
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1145
1524
  expectedOutputs: [
1146
1525
  "BootstrapContractVersion",
1147
1526
  "BootstrapTemplateRevision",
@@ -1157,8 +1536,9 @@ async function bootstrapHandoff(input) {
1157
1536
  config: input.config,
1158
1537
  configPath: input.configPath,
1159
1538
  templatePath,
1160
- parameterPath: "github-oidc-bootstrap.json",
1161
- coreCapturePath
1539
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1540
+ coreCapturePath,
1541
+ workDir: input.workDir
1162
1542
  }),
1163
1543
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1164
1544
  input.config,
@@ -1170,12 +1550,13 @@ async function bootstrapHandoff(input) {
1170
1550
  input.config,
1171
1551
  input.runner
1172
1552
  );
1553
+ const githubOidcCapturePath = input.githubOidcStackPath ?? workFile(input.workDir, "github-oidc-deployed-stack.json");
1173
1554
  if (prerequisite.status !== "ready")
1174
1555
  return {
1175
1556
  phase: "github-environment",
1176
1557
  expectedContracts,
1177
1558
  expectedOutputs: [],
1178
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1559
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1179
1560
  reviewCheckpoints: [
1180
1561
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1181
1562
  ],
@@ -1186,7 +1567,7 @@ async function bootstrapHandoff(input) {
1186
1567
  phase: "application-gateway",
1187
1568
  expectedContracts,
1188
1569
  expectedOutputs: [],
1189
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1570
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1190
1571
  reviewCheckpoints: [
1191
1572
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1192
1573
  ],
@@ -1267,7 +1648,7 @@ async function verifyBootstrap(input) {
1267
1648
  const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
1268
1649
  if (!owner || !repository || extra.length)
1269
1650
  throw new Error("GitHub repository must be owner/repository");
1270
- const deployRole = outputValues(oidc).GitHubDeployRoleArn;
1651
+ const deployRole = outputValues2(oidc).GitHubDeployRoleArn;
1271
1652
  if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
1272
1653
  const providerArn = github?.providerArn ?? "";
1273
1654
  const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
@@ -1544,8 +1925,13 @@ function quote(args2) {
1544
1925
  }
1545
1926
  async function bootstrapGuide(input) {
1546
1927
  const handoff = await bootstrapHandoff(input);
1928
+ const plan = await bootstrapPlan(input.config);
1547
1929
  const lines = [
1548
1930
  `Phase: ${handoff.phase}`,
1931
+ `Package version: ${plan.packageVersion}`,
1932
+ ...plan.packageTemplates.map(
1933
+ (template) => `Template ${template.kind}: contract ${template.contractVersion}${template.templateRevision ? `, revision ${template.templateRevision}` : ""}, sha256 ${template.sha256}`
1934
+ ),
1549
1935
  `Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
1550
1936
  ];
1551
1937
  if (handoff.expectedOutputs.length)
@@ -1558,7 +1944,9 @@ async function bootstrapGuide(input) {
1558
1944
  lines.push("PAUSE: review the change set before approval.");
1559
1945
  if (command.name === "execute-reviewed-change-set")
1560
1946
  lines.push("PAUSE: execute only after explicit approval.");
1561
- lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
1947
+ lines.push(
1948
+ command.captureFile ? `${quote(command.args)} > ${quote([command.captureFile])}` : quote(command.args)
1949
+ );
1562
1950
  }
1563
1951
  if (handoff.reviewCheckpoints.length)
1564
1952
  lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
@@ -1939,16 +2327,17 @@ function bootstrapValues(config, coreOutputs = {}) {
1939
2327
  coreOutputs.ApplicationStackName ?? config.core?.applicationStackName
1940
2328
  ),
1941
2329
  DashboardFunctionName: option("--function-name", config.core?.dashboardFunctionName),
1942
- RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn),
1943
- ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn),
2330
+ RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn ?? ""),
2331
+ ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn ?? ""),
1944
2332
  GitHubOidcProviderArn: option("--github-oidc-provider-arn", config.githubOidc?.providerArn),
1945
2333
  GitHubRepository: option("--github-repository", config.githubOidc?.repository),
1946
2334
  GitHubOwnerId: option("--github-owner-id", config.githubOidc?.ownerId),
1947
2335
  GitHubRepositoryId: option("--github-repository-id", config.githubOidc?.repositoryId),
1948
2336
  GitHubEnvironment: option("--github-environment", config.githubOidc?.environment),
2337
+ CoreBootstrapStackName: config.core?.stackName,
1949
2338
  ConsumerGatewayStackName: option(
1950
2339
  "--consumer-gateway-stack",
1951
- config.githubOidc?.consumerGatewayStackName
2340
+ config.githubOidc?.consumerGatewayStackName ?? ""
1952
2341
  ),
1953
2342
  CloudFormationExecutionRoleArn: option(
1954
2343
  "--execution-role-arn",
@@ -2149,9 +2538,11 @@ try {
2149
2538
  await bootstrapGuide({
2150
2539
  config,
2151
2540
  configPath,
2541
+ workDir: option("--work-dir"),
2152
2542
  coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2153
2543
  coreStackPath,
2154
2544
  githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2545
+ githubOidcStackPath: githubStackPath,
2155
2546
  runner
2156
2547
  })
2157
2548
  );
@@ -2171,9 +2562,11 @@ try {
2171
2562
  await bootstrapHandoff({
2172
2563
  config,
2173
2564
  configPath,
2565
+ workDir: option("--work-dir"),
2174
2566
  coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2175
2567
  coreStackPath,
2176
2568
  githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2569
+ githubOidcStackPath: githubStackPath,
2177
2570
  runner
2178
2571
  })
2179
2572
  )
@@ -2200,6 +2593,42 @@ try {
2200
2593
  contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
2201
2594
  })
2202
2595
  );
2596
+ } else if (action === "plan") {
2597
+ requiredOption("--config");
2598
+ const plan = await bootstrapPlan(config);
2599
+ if (option("--format") === "text") {
2600
+ console.log("AWS bootstrap plan (read-only)");
2601
+ console.log(`Package version: ${plan.packageVersion}`);
2602
+ for (const template of plan.packageTemplates) {
2603
+ console.log(`
2604
+ ${template.kind}: ${template.path}`);
2605
+ console.log(` contract: ${template.contractVersion}`);
2606
+ if (template.templateRevision) console.log(` revision: ${template.templateRevision}`);
2607
+ console.log(` sha256: ${template.sha256}`);
2608
+ console.log(
2609
+ ` resources: ${template.resources.map(({ logicalId, type }) => `${logicalId} (${type})`).join(", ")}`
2610
+ );
2611
+ console.log(` declared IAM actions: ${template.iamActions.join(", ") || "none"}`);
2612
+ }
2613
+ console.log(`
2614
+ ${plan.notes.join("\n")}`);
2615
+ } else console.log(JSON.stringify(plan));
2616
+ } else if (action === "check") {
2617
+ requiredOption("--config");
2618
+ const { execFile: execFile3 } = await import("node:child_process");
2619
+ const { promisify: promisify3 } = await import("node:util");
2620
+ const result = await checkBootstrap(
2621
+ config,
2622
+ { resourceDrift: args.includes("--resource-drift") },
2623
+ {
2624
+ async execute(command, commandArgs) {
2625
+ return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2626
+ }
2627
+ }
2628
+ );
2629
+ if (option("--format") === "text") process.stdout.write(formatBootstrapCheckText(result));
2630
+ else console.log(JSON.stringify(result));
2631
+ if (!result.ok) process.exitCode = 1;
2203
2632
  } else if (action === "parameters") {
2204
2633
  const kind = bootstrapKind();
2205
2634
  const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
@@ -2234,33 +2663,14 @@ try {
2234
2663
  console.log(
2235
2664
  JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
2236
2665
  );
2237
- } else if (action === "status") {
2238
- const kind = bootstrapKind();
2239
- const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
2240
- const region2 = option("--region", config.region);
2241
- const awsCommand = [
2242
- "aws",
2243
- "cloudformation",
2244
- "describe-stacks",
2245
- "--stack-name",
2246
- stackName2,
2247
- ...region2 ? ["--region", region2] : [],
2248
- "--no-cli-pager"
2249
- ];
2250
- console.log(
2251
- JSON.stringify({
2252
- kind,
2253
- contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind)),
2254
- awsCommand,
2255
- shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
2256
- })
2257
- );
2258
2666
  } else if (action === "change-set") {
2259
2667
  const kind = bootstrapKind();
2260
2668
  const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
2261
2669
  const changeSetName = requiredOption("--change-set-name");
2262
2670
  const parametersPath = requiredOption("--parameters");
2263
2671
  const region2 = option("--region", config.region);
2672
+ const plan = await bootstrapPlan(config);
2673
+ const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
2264
2674
  await existingParameters(parametersPath);
2265
2675
  const awsCommand = [
2266
2676
  "aws",
@@ -2284,6 +2694,10 @@ try {
2284
2694
  console.log(
2285
2695
  JSON.stringify({
2286
2696
  kind,
2697
+ packageVersion: plan.packageVersion,
2698
+ contractVersion: template?.contractVersion,
2699
+ templateRevision: template?.templateRevision,
2700
+ templateSha256: template?.sha256,
2287
2701
  reviewRequired: true,
2288
2702
  awsCommand,
2289
2703
  shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
@@ -2291,7 +2705,7 @@ try {
2291
2705
  );
2292
2706
  } else {
2293
2707
  throw new Error(
2294
- "Usage: ze-great-dashboard-aws bootstrap init|preflight|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
2708
+ "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]"
2295
2709
  );
2296
2710
  }
2297
2711
  }
package/dist/guided.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BootstrapConfig } from './bootstrap.js';
1
+ import { type BootstrapConfig } from './bootstrap.js';
2
2
  import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
3
3
  export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
4
4
  export type BootstrapPreflightCheck = {