@continuous-excellence/ze-great-dashboard-aws 0.5.1 → 0.6.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/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,6 +1333,9 @@ 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);
@@ -1036,7 +1409,7 @@ function changeSetCommands(input) {
1036
1409
  },
1037
1410
  {
1038
1411
  name: "capture-stack",
1039
- captureFile: `${input.kind}-deployed-stack.json`,
1412
+ captureFile: workFile(input.workDir, `${input.kind}-deployed-stack.json`),
1040
1413
  args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
1041
1414
  }
1042
1415
  ];
@@ -1090,7 +1463,7 @@ async function contracts() {
1090
1463
  }
1091
1464
  async function bootstrapHandoff(input) {
1092
1465
  const expectedContracts = await contracts();
1093
- const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
1466
+ const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
1094
1467
  const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
1095
1468
  const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
1096
1469
  if (!core) {
@@ -1104,9 +1477,10 @@ async function bootstrapHandoff(input) {
1104
1477
  phase: "core",
1105
1478
  expectedContracts,
1106
1479
  templatePath,
1107
- parameterPath: "core-bootstrap.json",
1480
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1108
1481
  expectedOutputs: [
1109
1482
  "BootstrapContractVersion",
1483
+ "BootstrapTemplateRevision",
1110
1484
  "ArtifactBucketName",
1111
1485
  "ApplicationStackName",
1112
1486
  "CloudFormationExecutionRoleArn"
@@ -1121,7 +1495,8 @@ async function bootstrapHandoff(input) {
1121
1495
  config: input.config,
1122
1496
  configPath: input.configPath,
1123
1497
  templatePath,
1124
- parameterPath: "core-bootstrap.json"
1498
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1499
+ workDir: input.workDir
1125
1500
  })
1126
1501
  };
1127
1502
  }
@@ -1133,6 +1508,7 @@ async function bootstrapHandoff(input) {
1133
1508
  GitHubOwnerId: input.config.githubOidc?.ownerId,
1134
1509
  GitHubRepositoryId: input.config.githubOidc?.repositoryId,
1135
1510
  GitHubEnvironment: input.config.githubOidc?.environment,
1511
+ CoreBootstrapStackName: input.config.core?.stackName,
1136
1512
  ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
1137
1513
  ...coreBootstrapOutputs(core)
1138
1514
  });
@@ -1141,7 +1517,7 @@ async function bootstrapHandoff(input) {
1141
1517
  phase: "github-oidc",
1142
1518
  expectedContracts,
1143
1519
  templatePath,
1144
- parameterPath: "github-oidc-bootstrap.json",
1520
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1145
1521
  expectedOutputs: [
1146
1522
  "BootstrapContractVersion",
1147
1523
  "BootstrapTemplateRevision",
@@ -1157,8 +1533,9 @@ async function bootstrapHandoff(input) {
1157
1533
  config: input.config,
1158
1534
  configPath: input.configPath,
1159
1535
  templatePath,
1160
- parameterPath: "github-oidc-bootstrap.json",
1161
- coreCapturePath
1536
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1537
+ coreCapturePath,
1538
+ workDir: input.workDir
1162
1539
  }),
1163
1540
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1164
1541
  input.config,
@@ -1170,12 +1547,13 @@ async function bootstrapHandoff(input) {
1170
1547
  input.config,
1171
1548
  input.runner
1172
1549
  );
1550
+ const githubOidcCapturePath = input.githubOidcStackPath ?? workFile(input.workDir, "github-oidc-deployed-stack.json");
1173
1551
  if (prerequisite.status !== "ready")
1174
1552
  return {
1175
1553
  phase: "github-environment",
1176
1554
  expectedContracts,
1177
1555
  expectedOutputs: [],
1178
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1556
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1179
1557
  reviewCheckpoints: [
1180
1558
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1181
1559
  ],
@@ -1186,7 +1564,7 @@ async function bootstrapHandoff(input) {
1186
1564
  phase: "application-gateway",
1187
1565
  expectedContracts,
1188
1566
  expectedOutputs: [],
1189
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1567
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1190
1568
  reviewCheckpoints: [
1191
1569
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1192
1570
  ],
@@ -1267,7 +1645,7 @@ async function verifyBootstrap(input) {
1267
1645
  const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
1268
1646
  if (!owner || !repository || extra.length)
1269
1647
  throw new Error("GitHub repository must be owner/repository");
1270
- const deployRole = outputValues(oidc).GitHubDeployRoleArn;
1648
+ const deployRole = outputValues2(oidc).GitHubDeployRoleArn;
1271
1649
  if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
1272
1650
  const providerArn = github?.providerArn ?? "";
1273
1651
  const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
@@ -1544,8 +1922,13 @@ function quote(args2) {
1544
1922
  }
1545
1923
  async function bootstrapGuide(input) {
1546
1924
  const handoff = await bootstrapHandoff(input);
1925
+ const plan = await bootstrapPlan(input.config);
1547
1926
  const lines = [
1548
1927
  `Phase: ${handoff.phase}`,
1928
+ `Package version: ${plan.packageVersion}`,
1929
+ ...plan.packageTemplates.map(
1930
+ (template) => `Template ${template.kind}: contract ${template.contractVersion}${template.templateRevision ? `, revision ${template.templateRevision}` : ""}, sha256 ${template.sha256}`
1931
+ ),
1549
1932
  `Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
1550
1933
  ];
1551
1934
  if (handoff.expectedOutputs.length)
@@ -1558,7 +1941,9 @@ async function bootstrapGuide(input) {
1558
1941
  lines.push("PAUSE: review the change set before approval.");
1559
1942
  if (command.name === "execute-reviewed-change-set")
1560
1943
  lines.push("PAUSE: execute only after explicit approval.");
1561
- lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
1944
+ lines.push(
1945
+ command.captureFile ? `${quote(command.args)} > ${quote([command.captureFile])}` : quote(command.args)
1946
+ );
1562
1947
  }
1563
1948
  if (handoff.reviewCheckpoints.length)
1564
1949
  lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
@@ -1939,16 +2324,17 @@ function bootstrapValues(config, coreOutputs = {}) {
1939
2324
  coreOutputs.ApplicationStackName ?? config.core?.applicationStackName
1940
2325
  ),
1941
2326
  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),
2327
+ RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn ?? ""),
2328
+ ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn ?? ""),
1944
2329
  GitHubOidcProviderArn: option("--github-oidc-provider-arn", config.githubOidc?.providerArn),
1945
2330
  GitHubRepository: option("--github-repository", config.githubOidc?.repository),
1946
2331
  GitHubOwnerId: option("--github-owner-id", config.githubOidc?.ownerId),
1947
2332
  GitHubRepositoryId: option("--github-repository-id", config.githubOidc?.repositoryId),
1948
2333
  GitHubEnvironment: option("--github-environment", config.githubOidc?.environment),
2334
+ CoreBootstrapStackName: config.core?.stackName,
1949
2335
  ConsumerGatewayStackName: option(
1950
2336
  "--consumer-gateway-stack",
1951
- config.githubOidc?.consumerGatewayStackName
2337
+ config.githubOidc?.consumerGatewayStackName ?? ""
1952
2338
  ),
1953
2339
  CloudFormationExecutionRoleArn: option(
1954
2340
  "--execution-role-arn",
@@ -2149,9 +2535,11 @@ try {
2149
2535
  await bootstrapGuide({
2150
2536
  config,
2151
2537
  configPath,
2538
+ workDir: option("--work-dir"),
2152
2539
  coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2153
2540
  coreStackPath,
2154
2541
  githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2542
+ githubOidcStackPath: githubStackPath,
2155
2543
  runner
2156
2544
  })
2157
2545
  );
@@ -2171,9 +2559,11 @@ try {
2171
2559
  await bootstrapHandoff({
2172
2560
  config,
2173
2561
  configPath,
2562
+ workDir: option("--work-dir"),
2174
2563
  coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2175
2564
  coreStackPath,
2176
2565
  githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2566
+ githubOidcStackPath: githubStackPath,
2177
2567
  runner
2178
2568
  })
2179
2569
  )
@@ -2200,6 +2590,42 @@ try {
2200
2590
  contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
2201
2591
  })
2202
2592
  );
2593
+ } else if (action === "plan") {
2594
+ requiredOption("--config");
2595
+ const plan = await bootstrapPlan(config);
2596
+ if (option("--format") === "text") {
2597
+ console.log("AWS bootstrap plan (read-only)");
2598
+ console.log(`Package version: ${plan.packageVersion}`);
2599
+ for (const template of plan.packageTemplates) {
2600
+ console.log(`
2601
+ ${template.kind}: ${template.path}`);
2602
+ console.log(` contract: ${template.contractVersion}`);
2603
+ if (template.templateRevision) console.log(` revision: ${template.templateRevision}`);
2604
+ console.log(` sha256: ${template.sha256}`);
2605
+ console.log(
2606
+ ` resources: ${template.resources.map(({ logicalId, type }) => `${logicalId} (${type})`).join(", ")}`
2607
+ );
2608
+ console.log(` declared IAM actions: ${template.iamActions.join(", ") || "none"}`);
2609
+ }
2610
+ console.log(`
2611
+ ${plan.notes.join("\n")}`);
2612
+ } else console.log(JSON.stringify(plan));
2613
+ } else if (action === "check") {
2614
+ requiredOption("--config");
2615
+ const { execFile: execFile3 } = await import("node:child_process");
2616
+ const { promisify: promisify3 } = await import("node:util");
2617
+ const result = await checkBootstrap(
2618
+ config,
2619
+ { resourceDrift: args.includes("--resource-drift") },
2620
+ {
2621
+ async execute(command, commandArgs) {
2622
+ return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2623
+ }
2624
+ }
2625
+ );
2626
+ if (option("--format") === "text") process.stdout.write(formatBootstrapCheckText(result));
2627
+ else console.log(JSON.stringify(result));
2628
+ if (!result.ok) process.exitCode = 1;
2203
2629
  } else if (action === "parameters") {
2204
2630
  const kind = bootstrapKind();
2205
2631
  const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
@@ -2234,33 +2660,14 @@ try {
2234
2660
  console.log(
2235
2661
  JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
2236
2662
  );
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
2663
  } else if (action === "change-set") {
2259
2664
  const kind = bootstrapKind();
2260
2665
  const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
2261
2666
  const changeSetName = requiredOption("--change-set-name");
2262
2667
  const parametersPath = requiredOption("--parameters");
2263
2668
  const region2 = option("--region", config.region);
2669
+ const plan = await bootstrapPlan(config);
2670
+ const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
2264
2671
  await existingParameters(parametersPath);
2265
2672
  const awsCommand = [
2266
2673
  "aws",
@@ -2284,6 +2691,10 @@ try {
2284
2691
  console.log(
2285
2692
  JSON.stringify({
2286
2693
  kind,
2694
+ packageVersion: plan.packageVersion,
2695
+ contractVersion: template?.contractVersion,
2696
+ templateRevision: template?.templateRevision,
2697
+ templateSha256: template?.sha256,
2287
2698
  reviewRequired: true,
2288
2699
  awsCommand,
2289
2700
  shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
@@ -2291,7 +2702,7 @@ try {
2291
2702
  );
2292
2703
  } else {
2293
2704
  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]"
2705
+ "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
2706
  );
2296
2707
  }
2297
2708
  }
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 = {