@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/README.md +12 -6
- 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 +448 -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 +401 -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,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,
|
|
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,
|
|
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 =
|
|
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(
|
|
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,
|