@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/README.md +52 -129
- package/bootstrap/core-v1.yml +1 -0
- package/bootstrap/github-oidc-v2.yml +27 -1
- package/dist/bootstrap-check.d.ts +45 -0
- package/dist/bootstrap.d.ts +29 -0
- package/dist/cli.js +451 -37
- package/dist/guided.d.ts +1 -1
- package/dist/handoff.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +404 -12
- package/package.json +1 -1
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
|
|
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,
|
|
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,
|
|
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 =
|
|
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(
|
|
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,
|