@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/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,6 +1320,9 @@ 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);
@@ -1023,7 +1396,7 @@ function changeSetCommands(input) {
1023
1396
  },
1024
1397
  {
1025
1398
  name: "capture-stack",
1026
- captureFile: `${input.kind}-deployed-stack.json`,
1399
+ captureFile: workFile(input.workDir, `${input.kind}-deployed-stack.json`),
1027
1400
  args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
1028
1401
  }
1029
1402
  ];
@@ -1077,7 +1450,7 @@ async function contracts() {
1077
1450
  }
1078
1451
  async function bootstrapHandoff(input) {
1079
1452
  const expectedContracts = await contracts();
1080
- const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
1453
+ const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
1081
1454
  const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
1082
1455
  const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
1083
1456
  if (!core) {
@@ -1091,9 +1464,10 @@ async function bootstrapHandoff(input) {
1091
1464
  phase: "core",
1092
1465
  expectedContracts,
1093
1466
  templatePath,
1094
- parameterPath: "core-bootstrap.json",
1467
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1095
1468
  expectedOutputs: [
1096
1469
  "BootstrapContractVersion",
1470
+ "BootstrapTemplateRevision",
1097
1471
  "ArtifactBucketName",
1098
1472
  "ApplicationStackName",
1099
1473
  "CloudFormationExecutionRoleArn"
@@ -1108,7 +1482,8 @@ async function bootstrapHandoff(input) {
1108
1482
  config: input.config,
1109
1483
  configPath: input.configPath,
1110
1484
  templatePath,
1111
- parameterPath: "core-bootstrap.json"
1485
+ parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1486
+ workDir: input.workDir
1112
1487
  })
1113
1488
  };
1114
1489
  }
@@ -1120,6 +1495,7 @@ async function bootstrapHandoff(input) {
1120
1495
  GitHubOwnerId: input.config.githubOidc?.ownerId,
1121
1496
  GitHubRepositoryId: input.config.githubOidc?.repositoryId,
1122
1497
  GitHubEnvironment: input.config.githubOidc?.environment,
1498
+ CoreBootstrapStackName: input.config.core?.stackName,
1123
1499
  ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
1124
1500
  ...coreBootstrapOutputs(core)
1125
1501
  });
@@ -1128,7 +1504,7 @@ async function bootstrapHandoff(input) {
1128
1504
  phase: "github-oidc",
1129
1505
  expectedContracts,
1130
1506
  templatePath,
1131
- parameterPath: "github-oidc-bootstrap.json",
1507
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1132
1508
  expectedOutputs: [
1133
1509
  "BootstrapContractVersion",
1134
1510
  "BootstrapTemplateRevision",
@@ -1144,8 +1520,9 @@ async function bootstrapHandoff(input) {
1144
1520
  config: input.config,
1145
1521
  configPath: input.configPath,
1146
1522
  templatePath,
1147
- parameterPath: "github-oidc-bootstrap.json",
1148
- coreCapturePath
1523
+ parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
1524
+ coreCapturePath,
1525
+ workDir: input.workDir
1149
1526
  }),
1150
1527
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1151
1528
  input.config,
@@ -1157,12 +1534,13 @@ async function bootstrapHandoff(input) {
1157
1534
  input.config,
1158
1535
  input.runner
1159
1536
  );
1537
+ const githubOidcCapturePath = input.githubOidcStackPath ?? workFile(input.workDir, "github-oidc-deployed-stack.json");
1160
1538
  if (prerequisite.status !== "ready")
1161
1539
  return {
1162
1540
  phase: "github-environment",
1163
1541
  expectedContracts,
1164
1542
  expectedOutputs: [],
1165
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1543
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1166
1544
  reviewCheckpoints: [
1167
1545
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1168
1546
  ],
@@ -1173,7 +1551,7 @@ async function bootstrapHandoff(input) {
1173
1551
  phase: "application-gateway",
1174
1552
  expectedContracts,
1175
1553
  expectedOutputs: [],
1176
- requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1554
+ requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
1177
1555
  reviewCheckpoints: [
1178
1556
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1179
1557
  ],
@@ -1254,7 +1632,7 @@ async function verifyBootstrap(input) {
1254
1632
  const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
1255
1633
  if (!owner || !repository || extra.length)
1256
1634
  throw new Error("GitHub repository must be owner/repository");
1257
- const deployRole = outputValues(oidc).GitHubDeployRoleArn;
1635
+ const deployRole = outputValues2(oidc).GitHubDeployRoleArn;
1258
1636
  if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
1259
1637
  const providerArn = github?.providerArn ?? "";
1260
1638
  const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
@@ -1531,8 +1909,13 @@ function quote(args) {
1531
1909
  }
1532
1910
  async function bootstrapGuide(input) {
1533
1911
  const handoff = await bootstrapHandoff(input);
1912
+ const plan = await bootstrapPlan(input.config);
1534
1913
  const lines = [
1535
1914
  `Phase: ${handoff.phase}`,
1915
+ `Package version: ${plan.packageVersion}`,
1916
+ ...plan.packageTemplates.map(
1917
+ (template) => `Template ${template.kind}: contract ${template.contractVersion}${template.templateRevision ? `, revision ${template.templateRevision}` : ""}, sha256 ${template.sha256}`
1918
+ ),
1536
1919
  `Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
1537
1920
  ];
1538
1921
  if (handoff.expectedOutputs.length)
@@ -1545,7 +1928,9 @@ async function bootstrapGuide(input) {
1545
1928
  lines.push("PAUSE: review the change set before approval.");
1546
1929
  if (command.name === "execute-reviewed-change-set")
1547
1930
  lines.push("PAUSE: execute only after explicit approval.");
1548
- lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
1931
+ lines.push(
1932
+ command.captureFile ? `${quote(command.args)} > ${quote([command.captureFile])}` : quote(command.args)
1933
+ );
1549
1934
  }
1550
1935
  if (handoff.reviewCheckpoints.length)
1551
1936
  lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
@@ -1704,17 +2089,21 @@ async function cloudFormationTemplate() {
1704
2089
  return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
1705
2090
  }
1706
2091
  export {
2092
+ bootstrapConsistency,
1707
2093
  bootstrapContractVersion,
1708
2094
  bootstrapGuide,
1709
2095
  bootstrapHandoff,
2096
+ bootstrapPlan,
1710
2097
  bootstrapPreflight,
1711
2098
  bootstrapTemplate,
1712
2099
  bootstrapTemplatePath,
1713
2100
  bootstrapTemplateRevision,
2101
+ checkBootstrap,
1714
2102
  cloudFormationTemplate,
1715
2103
  coreBootstrapOutputs,
1716
2104
  deployLambda,
1717
2105
  deployedBootstrapStack,
2106
+ formatBootstrapCheckText,
1718
2107
  githubOidcProvider,
1719
2108
  mergeBootstrapParameters,
1720
2109
  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.0",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [