@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/handoff.d.ts CHANGED
@@ -48,9 +48,11 @@ export declare const githubOidcProvider: BootstrapProvider;
48
48
  export declare function bootstrapHandoff(input: {
49
49
  config: BootstrapConfig;
50
50
  configPath: string;
51
+ workDir?: string;
51
52
  coreStack?: unknown;
52
53
  coreStackPath?: string;
53
54
  githubOidcStack?: unknown;
55
+ githubOidcStackPath?: string;
54
56
  provider?: BootstrapProvider;
55
57
  runner?: CommandRunner;
56
58
  }): Promise<BootstrapHandoff>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { type BootstrapConfig, type BootstrapKind, bootstrapContractVersion, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requiredBootstrapParameters, } from './bootstrap.js';
1
+ export { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind, type BootstrapPlan, type BootstrapTemplateInspection, bootstrapConsistency, bootstrapContractVersion, bootstrapPlan, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requiredBootstrapParameters, } from './bootstrap.js';
2
+ export { type BootstrapCheck, type BootstrapCheckDependencies, type BootstrapResourceDifference, type BootstrapResourceDrift, type BootstrapResourceDriftResult, type BootstrapStackCheck, checkBootstrap, formatBootstrapCheckText, } from './bootstrap-check.js';
2
3
  export { type BootstrapCheckStatus, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
3
4
  export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
4
5
  export type ReleaseMetadata = {
package/dist/index.js CHANGED
@@ -849,6 +849,125 @@ function bootstrapTemplateRevision(template) {
849
849
  if (!revision) throw new Error("Bootstrap template has no BootstrapTemplateRevision output");
850
850
  return revision;
851
851
  }
852
+ function templateRevisionIfPresent(template) {
853
+ return template.match(/BootstrapTemplateRevision:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
854
+ }
855
+ function inspectTemplate(kind, path, template) {
856
+ const resourcesBlock = template.match(/^Resources:\n([\s\S]*?)(?=^[A-Za-z][A-Za-z0-9]*:\s*$)/m)?.[1] ?? "";
857
+ const resources = [
858
+ ...resourcesBlock.matchAll(/^ {2}([A-Za-z][A-Za-z0-9]+):\n {4}Type: ([^\n]+)$/gm)
859
+ ].map((match) => ({ logicalId: match[1], type: match[2] }));
860
+ const iamActions = [...template.matchAll(/^\s+Action: (?:\[([^\]]+)\]|([^\n]+))$/gm)].flatMap(([, list, scalar]) => (list ?? scalar ?? "").split(",").map((action) => action.trim())).filter(Boolean);
861
+ return {
862
+ kind,
863
+ path,
864
+ contractVersion: bootstrapContractVersion(template),
865
+ templateRevision: templateRevisionIfPresent(template),
866
+ sha256: sha256(template),
867
+ resources,
868
+ iamActions: [...new Set(iamActions)]
869
+ };
870
+ }
871
+ async function bootstrapPlan(config) {
872
+ const packageManifest = JSON.parse(
873
+ await readFile2(new URL("../package.json", import.meta.url), "utf8")
874
+ );
875
+ const packageTemplates = await Promise.all(
876
+ ["core", "github-oidc"].map(async (kind) => {
877
+ const [path, template] = await Promise.all([
878
+ bootstrapTemplatePath(kind),
879
+ bootstrapTemplate(kind)
880
+ ]);
881
+ return inspectTemplate(kind, path, template);
882
+ })
883
+ );
884
+ return {
885
+ packageVersion: typeof packageManifest.version === "string" ? packageManifest.version : "unknown",
886
+ packageTemplates,
887
+ configuration: config,
888
+ notes: [
889
+ "Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
890
+ "Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
891
+ "This plan performs no AWS or GitHub mutations."
892
+ ]
893
+ };
894
+ }
895
+ function capturedStack(input) {
896
+ const stack = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
897
+ return stack && typeof stack === "object" ? stack : {};
898
+ }
899
+ function outputValues(stack) {
900
+ return Object.fromEntries(
901
+ (stack.Outputs ?? []).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
902
+ );
903
+ }
904
+ function bootstrapConsistency(kind, config, stackInput, expectedContract, expectedRevision, coreOutputs = {}) {
905
+ const stack = capturedStack(stackInput);
906
+ const mismatches = [];
907
+ const expectedStackName = kind === "core" ? config.core?.stackName : config.githubOidc?.stackName;
908
+ if (stack.StackName !== expectedStackName)
909
+ mismatches.push(
910
+ `stack name is ${stack.StackName ?? "missing"}; expected ${expectedStackName ?? "missing"}`
911
+ );
912
+ const observedRegion = stack.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
913
+ if (observedRegion !== config.region)
914
+ mismatches.push(
915
+ `Region is ${observedRegion ?? "missing"}; expected ${config.region ?? "missing"}`
916
+ );
917
+ if (!["CREATE_COMPLETE", "UPDATE_COMPLETE"].includes(stack.StackStatus ?? ""))
918
+ mismatches.push(
919
+ `stack status is ${stack.StackStatus ?? "missing"}; expected a successful stable status`
920
+ );
921
+ const outputs2 = outputValues(stack);
922
+ if (outputs2.BootstrapContractVersion !== expectedContract)
923
+ mismatches.push(
924
+ `contract is ${outputs2.BootstrapContractVersion ?? "missing"}; expected ${expectedContract}`
925
+ );
926
+ if (expectedRevision && outputs2.BootstrapTemplateRevision !== expectedRevision)
927
+ mismatches.push(
928
+ `template revision is ${outputs2.BootstrapTemplateRevision ?? "missing"}; expected ${expectedRevision}`
929
+ );
930
+ const requiredOutputs = kind === "core" ? ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"] : ["GitHubDeployRoleArn"];
931
+ for (const key of requiredOutputs) if (!outputs2[key]) mismatches.push(`${key} output is missing`);
932
+ if (kind === "core") {
933
+ if (outputs2.ArtifactBucketName !== config.core?.artifactBucketName)
934
+ mismatches.push(
935
+ `ArtifactBucketName output is ${outputs2.ArtifactBucketName ?? "missing"}; expected ${config.core?.artifactBucketName ?? "missing"}`
936
+ );
937
+ if (outputs2.ApplicationStackName !== config.core?.applicationStackName)
938
+ mismatches.push(
939
+ `ApplicationStackName output is ${outputs2.ApplicationStackName ?? "missing"}; expected ${config.core?.applicationStackName ?? "missing"}`
940
+ );
941
+ }
942
+ const parameters = Object.fromEntries(
943
+ (stack.Parameters ?? []).map(({ ParameterKey, ParameterValue }) => [
944
+ ParameterKey,
945
+ ParameterValue
946
+ ])
947
+ );
948
+ const expectedParameters = kind === "core" ? {
949
+ ArtifactBucketName: config.core?.artifactBucketName,
950
+ ApplicationStackName: config.core?.applicationStackName,
951
+ DashboardFunctionName: config.core?.dashboardFunctionName,
952
+ RuntimeSecretArn: config.core?.runtimeSecretArn ?? "",
953
+ ArtifactKmsKeyArn: config.core?.artifactKmsKeyArn ?? ""
954
+ } : {
955
+ GitHubOidcProviderArn: config.githubOidc?.providerArn,
956
+ GitHubRepository: config.githubOidc?.repository,
957
+ GitHubOwnerId: config.githubOidc?.ownerId,
958
+ GitHubRepositoryId: config.githubOidc?.repositoryId,
959
+ GitHubEnvironment: config.githubOidc?.environment,
960
+ CoreBootstrapStackName: config.core?.stackName,
961
+ ApplicationStackName: config.core?.applicationStackName,
962
+ ArtifactBucketName: config.core?.artifactBucketName,
963
+ CloudFormationExecutionRoleArn: coreOutputs.CloudFormationExecutionRoleArn,
964
+ ConsumerGatewayStackName: config.githubOidc?.consumerGatewayStackName ?? ""
965
+ };
966
+ for (const [key, expected] of Object.entries(expectedParameters))
967
+ if (expected !== void 0 && parameters[key] !== expected)
968
+ mismatches.push(`${key} is ${parameters[key] ?? "missing"}; expected ${expected}`);
969
+ return { ok: mismatches.length === 0, mismatches };
970
+ }
852
971
  function mergeBootstrapParameters(requested, deployed) {
853
972
  const supplied = new Map(requested.map((entry) => [entry.ParameterKey, entry.ParameterValue]));
854
973
  const duplicates = requested.map((entry) => entry.ParameterKey);
@@ -896,6 +1015,7 @@ function requiredBootstrapParameters(kind, values) {
896
1015
  "GitHubOwnerId",
897
1016
  "GitHubRepositoryId",
898
1017
  "GitHubEnvironment",
1018
+ "CoreBootstrapStackName",
899
1019
  "ApplicationStackName",
900
1020
  "ArtifactBucketName",
901
1021
  "CloudFormationExecutionRoleArn"
@@ -906,6 +1026,256 @@ function requiredBootstrapParameters(kind, values) {
906
1026
  return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
907
1027
  }
908
1028
 
1029
+ // packages/aws/src/bootstrap-check.ts
1030
+ function formatBootstrapCheckText(result) {
1031
+ const lines = [`AWS bootstrap consistency (${result.packageVersion})`];
1032
+ for (const stack of result.stacks) {
1033
+ lines.push(
1034
+ `${stack.consistency.ok && (!stack.resourceDrift || stack.resourceDrift.ok) ? "PASS" : "FAIL"} ${stack.kind}: ${stack.stackName} (${stack.stackStatus ?? "unavailable"})`,
1035
+ ` template: contract ${stack.contractVersion}, revision ${stack.templateRevision ?? "none"}, sha256 ${stack.templateSha256}`
1036
+ );
1037
+ for (const mismatch of stack.consistency.mismatches) lines.push(` mismatch: ${mismatch}`);
1038
+ if (!stack.resourceDrift) continue;
1039
+ lines.push(` resource drift: ${stack.resourceDrift.status}`);
1040
+ if (stack.resourceDrift.reason) lines.push(` drift error: ${stack.resourceDrift.reason}`);
1041
+ for (const resource of stack.resourceDrift.resources) {
1042
+ lines.push(
1043
+ ` drifted resource: ${resource.logicalId} (${resource.resourceType ?? "unknown"}, ${resource.status ?? "unknown"})`
1044
+ );
1045
+ for (const difference of resource.differences)
1046
+ lines.push(
1047
+ ` ${difference.path ?? "unknown property"}: ${difference.differenceType ?? "changed"}; expected ${difference.expectedValue ?? "unknown"}, actual ${difference.actualValue ?? "unknown"}`
1048
+ );
1049
+ }
1050
+ }
1051
+ return `${lines.join("\n")}
1052
+ `;
1053
+ }
1054
+ function message(error) {
1055
+ return error instanceof Error ? error.message : String(error);
1056
+ }
1057
+ function parseJson(raw, operation) {
1058
+ try {
1059
+ const value2 = JSON.parse(raw);
1060
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2))
1061
+ throw new Error("response is not an object");
1062
+ return value2;
1063
+ } catch (error) {
1064
+ throw new Error(`${operation} returned invalid JSON: ${message(error)}`);
1065
+ }
1066
+ }
1067
+ function stackFrom(response) {
1068
+ const stacks = response.Stacks;
1069
+ return Array.isArray(stacks) && stacks[0] && typeof stacks[0] === "object" ? stacks[0] : void 0;
1070
+ }
1071
+ function outputs(stack) {
1072
+ const entries = stack?.Outputs;
1073
+ if (!Array.isArray(entries)) return {};
1074
+ return Object.fromEntries(
1075
+ entries.filter(
1076
+ (entry) => Boolean(entry && typeof entry === "object" && typeof entry.OutputKey === "string")
1077
+ ).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
1078
+ );
1079
+ }
1080
+ async function resourceDrift(stackName2, region2, dependencies) {
1081
+ try {
1082
+ const detected = parseJson(
1083
+ await dependencies.execute("aws", [
1084
+ "cloudformation",
1085
+ "detect-stack-drift",
1086
+ "--stack-name",
1087
+ stackName2,
1088
+ "--region",
1089
+ region2,
1090
+ "--output",
1091
+ "json",
1092
+ "--no-cli-pager"
1093
+ ]),
1094
+ "detect-stack-drift"
1095
+ );
1096
+ const detectionId = detected.StackDriftDetectionId;
1097
+ if (typeof detectionId !== "string")
1098
+ throw new Error("detect-stack-drift returned no StackDriftDetectionId");
1099
+ const now = dependencies.now ?? Date.now;
1100
+ const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
1101
+ const deadline = now() + 10 * 60 * 1e3;
1102
+ let status;
1103
+ while (true) {
1104
+ status = parseJson(
1105
+ await dependencies.execute("aws", [
1106
+ "cloudformation",
1107
+ "describe-stack-drift-detection-status",
1108
+ "--stack-drift-detection-id",
1109
+ detectionId,
1110
+ "--region",
1111
+ region2,
1112
+ "--output",
1113
+ "json",
1114
+ "--no-cli-pager"
1115
+ ]),
1116
+ "describe-stack-drift-detection-status"
1117
+ );
1118
+ if (status.DetectionStatus !== "DETECTION_IN_PROGRESS") break;
1119
+ if (now() >= deadline)
1120
+ return {
1121
+ ok: false,
1122
+ status: "TIMED_OUT",
1123
+ detectionId,
1124
+ reason: "CloudFormation drift detection did not complete within 10 minutes",
1125
+ resources: []
1126
+ };
1127
+ await sleep(5e3);
1128
+ }
1129
+ if (status.DetectionStatus !== "DETECTION_COMPLETE")
1130
+ return {
1131
+ ok: false,
1132
+ status: String(status.DetectionStatus ?? "UNKNOWN"),
1133
+ detectionId,
1134
+ reason: typeof status.DetectionStatusReason === "string" ? status.DetectionStatusReason : "CloudFormation drift detection failed",
1135
+ resources: []
1136
+ };
1137
+ const stackDriftStatus = String(status.StackDriftStatus ?? "UNKNOWN");
1138
+ if (stackDriftStatus !== "DRIFTED")
1139
+ return {
1140
+ ok: stackDriftStatus === "IN_SYNC",
1141
+ status: stackDriftStatus,
1142
+ detectionId,
1143
+ resources: []
1144
+ };
1145
+ const described = parseJson(
1146
+ await dependencies.execute("aws", [
1147
+ "cloudformation",
1148
+ "describe-stack-resource-drifts",
1149
+ "--stack-name",
1150
+ stackName2,
1151
+ "--stack-resource-drift-status-filters",
1152
+ "MODIFIED",
1153
+ "DELETED",
1154
+ "--region",
1155
+ region2,
1156
+ "--output",
1157
+ "json",
1158
+ "--no-cli-pager"
1159
+ ]),
1160
+ "describe-stack-resource-drifts"
1161
+ );
1162
+ const resources = Array.isArray(described.StackResourceDrifts) ? described.StackResourceDrifts.flatMap((entry) => {
1163
+ if (!entry || typeof entry !== "object") return [];
1164
+ const drift = entry;
1165
+ if (typeof drift.LogicalResourceId !== "string") return [];
1166
+ const differences = Array.isArray(drift.PropertyDifferences) ? drift.PropertyDifferences.flatMap((item) => {
1167
+ if (!item || typeof item !== "object") return [];
1168
+ const difference = item;
1169
+ return [
1170
+ {
1171
+ ...typeof difference.PropertyPath === "string" ? { path: difference.PropertyPath } : {},
1172
+ ...typeof difference.DifferenceType === "string" ? { differenceType: difference.DifferenceType } : {},
1173
+ ...typeof difference.ExpectedValue === "string" ? { expectedValue: difference.ExpectedValue } : {},
1174
+ ...typeof difference.ActualValue === "string" ? { actualValue: difference.ActualValue } : {}
1175
+ }
1176
+ ];
1177
+ }) : [];
1178
+ return [
1179
+ {
1180
+ logicalId: drift.LogicalResourceId,
1181
+ ...typeof drift.ResourceType === "string" ? { resourceType: drift.ResourceType } : {},
1182
+ ...typeof drift.StackResourceDriftStatus === "string" ? { status: drift.StackResourceDriftStatus } : {},
1183
+ differences
1184
+ }
1185
+ ];
1186
+ }) : [];
1187
+ return { ok: false, status: stackDriftStatus, detectionId, resources };
1188
+ } catch (error) {
1189
+ return { ok: false, status: "ERROR", reason: message(error), resources: [] };
1190
+ }
1191
+ }
1192
+ async function checkBootstrap(config, options, dependencies) {
1193
+ const requiredManifestValues = [
1194
+ ["region", config.region],
1195
+ ["core.stackName", config.core?.stackName],
1196
+ ["core.artifactBucketName", config.core?.artifactBucketName],
1197
+ ["core.applicationStackName", config.core?.applicationStackName],
1198
+ ["core.dashboardFunctionName", config.core?.dashboardFunctionName],
1199
+ ["githubOidc.stackName", config.githubOidc?.stackName],
1200
+ ["githubOidc.providerArn", config.githubOidc?.providerArn],
1201
+ ["githubOidc.repository", config.githubOidc?.repository],
1202
+ ["githubOidc.ownerId", config.githubOidc?.ownerId],
1203
+ ["githubOidc.repositoryId", config.githubOidc?.repositoryId],
1204
+ ["githubOidc.environment", config.githubOidc?.environment]
1205
+ ];
1206
+ const missing = requiredManifestValues.filter(([, value2]) => !value2).map(([name]) => name);
1207
+ if (missing.length) throw new Error(`Bootstrap manifest is missing: ${missing.join(", ")}`);
1208
+ const stackNames = {
1209
+ core: config.core?.stackName,
1210
+ "github-oidc": config.githubOidc?.stackName
1211
+ };
1212
+ const plan = await bootstrapPlan(config);
1213
+ const responses = await Promise.all(
1214
+ ["core", "github-oidc"].map(async (kind) => {
1215
+ const stackName2 = stackNames[kind];
1216
+ try {
1217
+ const response = parseJson(
1218
+ await dependencies.execute("aws", [
1219
+ "cloudformation",
1220
+ "describe-stacks",
1221
+ "--stack-name",
1222
+ stackName2,
1223
+ "--region",
1224
+ config.region,
1225
+ "--output",
1226
+ "json",
1227
+ "--no-cli-pager"
1228
+ ]),
1229
+ "describe-stacks"
1230
+ );
1231
+ return { kind, stackName: stackName2, response };
1232
+ } catch (error) {
1233
+ return { kind, stackName: stackName2, error: message(error) };
1234
+ }
1235
+ })
1236
+ );
1237
+ const coreResponse = responses.find(({ kind }) => kind === "core")?.response;
1238
+ const coreOutputValues = outputs(stackFrom(coreResponse ?? {}));
1239
+ const stacks = await Promise.all(
1240
+ responses.map(async ({ kind, stackName: stackName2, response, error }) => {
1241
+ const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
1242
+ if (!template) throw new Error(`Installed package has no ${kind} bootstrap template`);
1243
+ const stack = stackFrom(response ?? {});
1244
+ const consistency = error ? { ok: false, mismatches: [`unable to describe stack: ${error}`] } : bootstrapConsistency(
1245
+ kind,
1246
+ config,
1247
+ response,
1248
+ template.contractVersion,
1249
+ template.templateRevision,
1250
+ coreOutputValues
1251
+ );
1252
+ const checked = {
1253
+ kind,
1254
+ stackName: stackName2,
1255
+ ...typeof stack?.StackStatus === "string" ? { stackStatus: stack.StackStatus } : {},
1256
+ contractVersion: template.contractVersion,
1257
+ templateRevision: template.templateRevision,
1258
+ templateSha256: template.sha256,
1259
+ consistency
1260
+ };
1261
+ if (options.resourceDrift && !error)
1262
+ checked.resourceDrift = await resourceDrift(
1263
+ stackName2,
1264
+ config.region,
1265
+ dependencies
1266
+ );
1267
+ return checked;
1268
+ })
1269
+ );
1270
+ return {
1271
+ ok: stacks.every(
1272
+ ({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
1273
+ ),
1274
+ packageVersion: plan.packageVersion,
1275
+ stacks
1276
+ };
1277
+ }
1278
+
909
1279
  // packages/aws/src/handoff.ts
910
1280
  var githubSubjectKeys = [
911
1281
  "repository_owner",
@@ -914,7 +1284,7 @@ var githubSubjectKeys = [
914
1284
  "repository_id",
915
1285
  "context"
916
1286
  ];
917
- function outputValues(stack) {
1287
+ function outputValues2(stack) {
918
1288
  return Object.fromEntries(
919
1289
  (stack.Outputs ?? []).filter(
920
1290
  (entry) => typeof entry.OutputKey === "string" && typeof entry.OutputValue === "string"
@@ -950,11 +1320,17 @@ function stackName(config, kind) {
950
1320
  `${kind}.stackName`
951
1321
  );
952
1322
  }
1323
+ function workFile(workDir, name) {
1324
+ return workDir ? `${workDir.replace(/\/+$/, "")}/${name}` : name;
1325
+ }
953
1326
  function changeSetCommands(input) {
954
1327
  const name = `${input.kind}-initial-review`;
955
1328
  const stack = stackName(input.config, input.kind);
956
1329
  const common = ["--stack-name", stack, "--region", region(input.config)];
957
1330
  const parameters = [
1331
+ "npm",
1332
+ "exec",
1333
+ "--",
958
1334
  "ze-great-dashboard-aws",
959
1335
  "bootstrap",
960
1336
  "parameters",
@@ -1023,7 +1399,7 @@ function changeSetCommands(input) {
1023
1399
  },
1024
1400
  {
1025
1401
  name: "capture-stack",
1026
- captureFile: `${input.kind}-deployed-stack.json`,
1402
+ captureFile: workFile(input.workDir, `${input.kind}-deployed-stack.json`),
1027
1403
  args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
1028
1404
  }
1029
1405
  ];
@@ -1077,7 +1453,7 @@ async function contracts() {
1077
1453
  }
1078
1454
  async function bootstrapHandoff(input) {
1079
1455
  const expectedContracts = await contracts();
1080
- const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
1456
+ const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
1081
1457
  const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
1082
1458
  const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
1083
1459
  if (!core) {
@@ -1091,9 +1467,10 @@ async function bootstrapHandoff(input) {
1091
1467
  phase: "core",
1092
1468
  expectedContracts,
1093
1469
  templatePath,
1094
- parameterPath: "core-bootstrap.json",
1470
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1095
1471
  expectedOutputs: [
1096
1472
  "BootstrapContractVersion",
1473
+ "BootstrapTemplateRevision",
1097
1474
  "ArtifactBucketName",
1098
1475
  "ApplicationStackName",
1099
1476
  "CloudFormationExecutionRoleArn"
@@ -1108,7 +1485,8 @@ async function bootstrapHandoff(input) {
1108
1485
  config: input.config,
1109
1486
  configPath: input.configPath,
1110
1487
  templatePath,
1111
- parameterPath: "core-bootstrap.json"
1488
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1489
+ workDir: input.workDir
1112
1490
  })
1113
1491
  };
1114
1492
  }
@@ -1120,6 +1498,7 @@ async function bootstrapHandoff(input) {
1120
1498
  GitHubOwnerId: input.config.githubOidc?.ownerId,
1121
1499
  GitHubRepositoryId: input.config.githubOidc?.repositoryId,
1122
1500
  GitHubEnvironment: input.config.githubOidc?.environment,
1501
+ CoreBootstrapStackName: input.config.core?.stackName,
1123
1502
  ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
1124
1503
  ...coreBootstrapOutputs(core)
1125
1504
  });
@@ -1128,7 +1507,7 @@ async function bootstrapHandoff(input) {
1128
1507
  phase: "github-oidc",
1129
1508
  expectedContracts,
1130
1509
  templatePath,
1131
- parameterPath: "github-oidc-bootstrap.json",
1510
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1132
1511
  expectedOutputs: [
1133
1512
  "BootstrapContractVersion",
1134
1513
  "BootstrapTemplateRevision",
@@ -1144,8 +1523,9 @@ async function bootstrapHandoff(input) {
1144
1523
  config: input.config,
1145
1524
  configPath: input.configPath,
1146
1525
  templatePath,
1147
- parameterPath: "github-oidc-bootstrap.json",
1148
- coreCapturePath
1526
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1527
+ coreCapturePath,
1528
+ workDir: input.workDir
1149
1529
  }),
1150
1530
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1151
1531
  input.config,
@@ -1157,12 +1537,13 @@ async function bootstrapHandoff(input) {
1157
1537
  input.config,
1158
1538
  input.runner
1159
1539
  );
1540
+ const githubOidcCapturePath = input.githubOidcStackPath ?? workFile(input.workDir, "github-oidc-deployed-stack.json");
1160
1541
  if (prerequisite.status !== "ready")
1161
1542
  return {
1162
1543
  phase: "github-environment",
1163
1544
  expectedContracts,
1164
1545
  expectedOutputs: [],
1165
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1546
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1166
1547
  reviewCheckpoints: [
1167
1548
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1168
1549
  ],
@@ -1173,7 +1554,7 @@ async function bootstrapHandoff(input) {
1173
1554
  phase: "application-gateway",
1174
1555
  expectedContracts,
1175
1556
  expectedOutputs: [],
1176
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1557
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1177
1558
  reviewCheckpoints: [
1178
1559
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1179
1560
  ],
@@ -1254,7 +1635,7 @@ async function verifyBootstrap(input) {
1254
1635
  const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
1255
1636
  if (!owner || !repository || extra.length)
1256
1637
  throw new Error("GitHub repository must be owner/repository");
1257
- const deployRole = outputValues(oidc).GitHubDeployRoleArn;
1638
+ const deployRole = outputValues2(oidc).GitHubDeployRoleArn;
1258
1639
  if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
1259
1640
  const providerArn = github?.providerArn ?? "";
1260
1641
  const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
@@ -1531,8 +1912,13 @@ function quote(args) {
1531
1912
  }
1532
1913
  async function bootstrapGuide(input) {
1533
1914
  const handoff = await bootstrapHandoff(input);
1915
+ const plan = await bootstrapPlan(input.config);
1534
1916
  const lines = [
1535
1917
  `Phase: ${handoff.phase}`,
1918
+ `Package version: ${plan.packageVersion}`,
1919
+ ...plan.packageTemplates.map(
1920
+ (template) => `Template ${template.kind}: contract ${template.contractVersion}${template.templateRevision ? `, revision ${template.templateRevision}` : ""}, sha256 ${template.sha256}`
1921
+ ),
1536
1922
  `Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
1537
1923
  ];
1538
1924
  if (handoff.expectedOutputs.length)
@@ -1545,7 +1931,9 @@ async function bootstrapGuide(input) {
1545
1931
  lines.push("PAUSE: review the change set before approval.");
1546
1932
  if (command.name === "execute-reviewed-change-set")
1547
1933
  lines.push("PAUSE: execute only after explicit approval.");
1548
- lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
1934
+ lines.push(
1935
+ command.captureFile ? `${quote(command.args)} > ${quote([command.captureFile])}` : quote(command.args)
1936
+ );
1549
1937
  }
1550
1938
  if (handoff.reviewCheckpoints.length)
1551
1939
  lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
@@ -1704,17 +2092,21 @@ async function cloudFormationTemplate() {
1704
2092
  return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
1705
2093
  }
1706
2094
  export {
2095
+ bootstrapConsistency,
1707
2096
  bootstrapContractVersion,
1708
2097
  bootstrapGuide,
1709
2098
  bootstrapHandoff,
2099
+ bootstrapPlan,
1710
2100
  bootstrapPreflight,
1711
2101
  bootstrapTemplate,
1712
2102
  bootstrapTemplatePath,
1713
2103
  bootstrapTemplateRevision,
2104
+ checkBootstrap,
1714
2105
  cloudFormationTemplate,
1715
2106
  coreBootstrapOutputs,
1716
2107
  deployLambda,
1717
2108
  deployedBootstrapStack,
2109
+ formatBootstrapCheckText,
1718
2110
  githubOidcProvider,
1719
2111
  mergeBootstrapParameters,
1720
2112
  packageLambda,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [