@continuous-excellence/ze-great-dashboard-aws 0.1.32 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +431 -15
- package/dist/handoff.d.ts +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +383 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -892,8 +892,8 @@ function coreBootstrapOutputs(stack) {
|
|
|
892
892
|
(output) => Boolean(output.OutputValue)
|
|
893
893
|
).map((output) => [output.OutputKey, output.OutputValue])
|
|
894
894
|
);
|
|
895
|
-
const
|
|
896
|
-
const missing =
|
|
895
|
+
const required2 = ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"];
|
|
896
|
+
const missing = required2.filter((key) => !values[key]);
|
|
897
897
|
if (missing.length) throw new Error(`Core stack JSON is missing outputs: ${missing.join(", ")}`);
|
|
898
898
|
return values;
|
|
899
899
|
}
|
|
@@ -914,6 +914,383 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
914
914
|
return [...keys, ...optional].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
// packages/aws/src/handoff.ts
|
|
918
|
+
var githubSubjectKeys = [
|
|
919
|
+
"repository_owner",
|
|
920
|
+
"repository_owner_id",
|
|
921
|
+
"repository",
|
|
922
|
+
"repository_id",
|
|
923
|
+
"context"
|
|
924
|
+
];
|
|
925
|
+
function outputValues(stack) {
|
|
926
|
+
return Object.fromEntries(
|
|
927
|
+
(stack.Outputs ?? []).filter(
|
|
928
|
+
(entry) => typeof entry.OutputKey === "string" && typeof entry.OutputValue === "string"
|
|
929
|
+
).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
function parameterValues(stack) {
|
|
933
|
+
return Object.fromEntries(
|
|
934
|
+
(stack.Parameters ?? []).filter(
|
|
935
|
+
(entry) => typeof entry.ParameterKey === "string" && typeof entry.ParameterValue === "string"
|
|
936
|
+
).map(({ ParameterKey, ParameterValue }) => [ParameterKey, ParameterValue])
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
function capturedStackIdentity(input) {
|
|
940
|
+
const candidate = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
|
|
941
|
+
if (!candidate || typeof candidate !== "object") return {};
|
|
942
|
+
const { StackName, StackId } = candidate;
|
|
943
|
+
return {
|
|
944
|
+
...typeof StackName === "string" ? { StackName } : {},
|
|
945
|
+
...typeof StackId === "string" ? { StackId } : {}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function required(value, name) {
|
|
949
|
+
if (!value) throw new Error(`Bootstrap manifest is missing ${name}`);
|
|
950
|
+
return value;
|
|
951
|
+
}
|
|
952
|
+
function region(config) {
|
|
953
|
+
return required(config.region, "region");
|
|
954
|
+
}
|
|
955
|
+
function stackName(config, kind) {
|
|
956
|
+
return required(
|
|
957
|
+
kind === "core" ? config.core?.stackName : config.githubOidc?.stackName,
|
|
958
|
+
`${kind}.stackName`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
function changeSetCommands(input) {
|
|
962
|
+
const name = `${input.kind}-initial-review`;
|
|
963
|
+
const stack = stackName(input.config, input.kind);
|
|
964
|
+
const common = ["--stack-name", stack, "--region", region(input.config)];
|
|
965
|
+
const parameters = [
|
|
966
|
+
"ze-great-dashboard-aws",
|
|
967
|
+
"bootstrap",
|
|
968
|
+
"parameters",
|
|
969
|
+
"--kind",
|
|
970
|
+
input.kind,
|
|
971
|
+
"--config",
|
|
972
|
+
input.configPath,
|
|
973
|
+
"--output",
|
|
974
|
+
input.parameterPath
|
|
975
|
+
];
|
|
976
|
+
if (input.kind === "github-oidc")
|
|
977
|
+
parameters.push("--core-stack-json", input.coreCapturePath ?? "core-deployed-stack.json");
|
|
978
|
+
return [
|
|
979
|
+
{ name: "generate-parameters", args: parameters },
|
|
980
|
+
{
|
|
981
|
+
name: "create-change-set",
|
|
982
|
+
args: [
|
|
983
|
+
"aws",
|
|
984
|
+
"cloudformation",
|
|
985
|
+
"create-change-set",
|
|
986
|
+
...common,
|
|
987
|
+
"--change-set-name",
|
|
988
|
+
name,
|
|
989
|
+
"--change-set-type",
|
|
990
|
+
"CREATE",
|
|
991
|
+
"--template-body",
|
|
992
|
+
`file://${input.templatePath}`,
|
|
993
|
+
"--parameters",
|
|
994
|
+
`file://${input.parameterPath}`,
|
|
995
|
+
"--capabilities",
|
|
996
|
+
"CAPABILITY_NAMED_IAM",
|
|
997
|
+
"--no-cli-pager"
|
|
998
|
+
]
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
name: "wait-for-change-set",
|
|
1002
|
+
args: [
|
|
1003
|
+
"aws",
|
|
1004
|
+
"cloudformation",
|
|
1005
|
+
"wait",
|
|
1006
|
+
"change-set-create-complete",
|
|
1007
|
+
...common,
|
|
1008
|
+
"--change-set-name",
|
|
1009
|
+
name
|
|
1010
|
+
]
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "review-change-set",
|
|
1014
|
+
args: [
|
|
1015
|
+
"aws",
|
|
1016
|
+
"cloudformation",
|
|
1017
|
+
"describe-change-set",
|
|
1018
|
+
...common,
|
|
1019
|
+
"--change-set-name",
|
|
1020
|
+
name,
|
|
1021
|
+
"--no-cli-pager"
|
|
1022
|
+
]
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "execute-reviewed-change-set",
|
|
1026
|
+
args: ["aws", "cloudformation", "execute-change-set", ...common, "--change-set-name", name]
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
name: "wait-for-stack",
|
|
1030
|
+
args: ["aws", "cloudformation", "wait", "stack-create-complete", ...common]
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
name: "capture-stack",
|
|
1034
|
+
captureFile: `${input.kind}-deployed-stack.json`,
|
|
1035
|
+
args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
|
|
1036
|
+
}
|
|
1037
|
+
];
|
|
1038
|
+
}
|
|
1039
|
+
var githubOidcProvider = {
|
|
1040
|
+
name: "github-oidc",
|
|
1041
|
+
async prerequisite(config, runner) {
|
|
1042
|
+
const repository = config.githubOidc?.repository;
|
|
1043
|
+
if (!repository)
|
|
1044
|
+
return {
|
|
1045
|
+
status: "unverified",
|
|
1046
|
+
blocking: false,
|
|
1047
|
+
detail: "GitHub repository is absent from the manifest."
|
|
1048
|
+
};
|
|
1049
|
+
if (!runner)
|
|
1050
|
+
return {
|
|
1051
|
+
status: "unverified",
|
|
1052
|
+
blocking: false,
|
|
1053
|
+
detail: "GitHub OIDC subject setting was not queried."
|
|
1054
|
+
};
|
|
1055
|
+
try {
|
|
1056
|
+
const response = JSON.parse(
|
|
1057
|
+
await runner.execute("gh", ["api", `repos/${repository}/actions/oidc/customization/sub`])
|
|
1058
|
+
);
|
|
1059
|
+
const keys = response.include_claim_keys;
|
|
1060
|
+
if (response.use_default === false && Array.isArray(keys) && githubSubjectKeys.every((key) => keys.includes(key)))
|
|
1061
|
+
return {
|
|
1062
|
+
status: "ready",
|
|
1063
|
+
blocking: false,
|
|
1064
|
+
detail: "GitHub emits the immutable owner/repository-ID OIDC subject required by this adapter."
|
|
1065
|
+
};
|
|
1066
|
+
return {
|
|
1067
|
+
status: "immutable-subject-required",
|
|
1068
|
+
blocking: true,
|
|
1069
|
+
detail: "Coordinate migration: inventory and temporarily make existing name-based trust policies compatible, enable immutable GitHub OIDC subjects as a separate GitHub-admin action, verify existing deployments, then retire legacy trust."
|
|
1070
|
+
};
|
|
1071
|
+
} catch {
|
|
1072
|
+
return {
|
|
1073
|
+
status: "unverified",
|
|
1074
|
+
blocking: false,
|
|
1075
|
+
detail: "GitHub OIDC subject setting could not be verified (gh CLI, authentication, permission, or network unavailable)."
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
async function contracts() {
|
|
1081
|
+
return {
|
|
1082
|
+
core: bootstrapContractVersion(await bootstrapTemplate("core")),
|
|
1083
|
+
githubOidc: bootstrapContractVersion(await bootstrapTemplate("github-oidc"))
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
async function bootstrapHandoff(input) {
|
|
1087
|
+
const expectedContracts = await contracts();
|
|
1088
|
+
const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
|
|
1089
|
+
const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
|
|
1090
|
+
const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
|
|
1091
|
+
if (!core) {
|
|
1092
|
+
requiredBootstrapParameters("core", {
|
|
1093
|
+
ArtifactBucketName: input.config.core?.artifactBucketName,
|
|
1094
|
+
ApplicationStackName: input.config.core?.applicationStackName,
|
|
1095
|
+
DashboardFunctionName: input.config.core?.dashboardFunctionName
|
|
1096
|
+
});
|
|
1097
|
+
const templatePath = await bootstrapTemplatePath("core");
|
|
1098
|
+
return {
|
|
1099
|
+
phase: "core",
|
|
1100
|
+
expectedContracts,
|
|
1101
|
+
templatePath,
|
|
1102
|
+
parameterPath: "core-bootstrap.json",
|
|
1103
|
+
requiredCapturedFiles: [],
|
|
1104
|
+
reviewCheckpoints: [
|
|
1105
|
+
"Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
|
|
1106
|
+
"Do not approve retained bucket or role replacements."
|
|
1107
|
+
],
|
|
1108
|
+
commands: changeSetCommands({
|
|
1109
|
+
kind: "core",
|
|
1110
|
+
config: input.config,
|
|
1111
|
+
configPath: input.configPath,
|
|
1112
|
+
templatePath,
|
|
1113
|
+
parameterPath: "core-bootstrap.json"
|
|
1114
|
+
})
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
coreBootstrapOutputs(core);
|
|
1118
|
+
if (!oidc) {
|
|
1119
|
+
requiredBootstrapParameters("github-oidc", {
|
|
1120
|
+
GitHubOidcProviderArn: input.config.githubOidc?.providerArn,
|
|
1121
|
+
GitHubRepository: input.config.githubOidc?.repository,
|
|
1122
|
+
GitHubOwnerId: input.config.githubOidc?.ownerId,
|
|
1123
|
+
GitHubRepositoryId: input.config.githubOidc?.repositoryId,
|
|
1124
|
+
GitHubEnvironment: input.config.githubOidc?.environment,
|
|
1125
|
+
...coreBootstrapOutputs(core)
|
|
1126
|
+
});
|
|
1127
|
+
const templatePath = await bootstrapTemplatePath("github-oidc");
|
|
1128
|
+
return {
|
|
1129
|
+
phase: "github-oidc",
|
|
1130
|
+
expectedContracts,
|
|
1131
|
+
templatePath,
|
|
1132
|
+
parameterPath: "github-oidc-bootstrap.json",
|
|
1133
|
+
requiredCapturedFiles: [coreCapturePath],
|
|
1134
|
+
reviewCheckpoints: [
|
|
1135
|
+
"Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
|
|
1136
|
+
"Do not execute until the GitHub Environment prerequisite is ready."
|
|
1137
|
+
],
|
|
1138
|
+
commands: changeSetCommands({
|
|
1139
|
+
kind: "github-oidc",
|
|
1140
|
+
config: input.config,
|
|
1141
|
+
configPath: input.configPath,
|
|
1142
|
+
templatePath,
|
|
1143
|
+
parameterPath: "github-oidc-bootstrap.json",
|
|
1144
|
+
coreCapturePath
|
|
1145
|
+
}),
|
|
1146
|
+
prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1147
|
+
input.config,
|
|
1148
|
+
input.runner
|
|
1149
|
+
)
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1153
|
+
input.config,
|
|
1154
|
+
input.runner
|
|
1155
|
+
);
|
|
1156
|
+
if (prerequisite.status !== "ready")
|
|
1157
|
+
return {
|
|
1158
|
+
phase: "github-environment",
|
|
1159
|
+
expectedContracts,
|
|
1160
|
+
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1161
|
+
reviewCheckpoints: [
|
|
1162
|
+
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
1163
|
+
],
|
|
1164
|
+
commands: [],
|
|
1165
|
+
prerequisite
|
|
1166
|
+
};
|
|
1167
|
+
return {
|
|
1168
|
+
phase: "application-gateway",
|
|
1169
|
+
expectedContracts,
|
|
1170
|
+
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1171
|
+
reviewCheckpoints: [
|
|
1172
|
+
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
1173
|
+
],
|
|
1174
|
+
commands: [],
|
|
1175
|
+
prerequisite
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function assertEqual(actual, expected, label) {
|
|
1179
|
+
if (actual !== expected)
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
`${label} mismatch (captured: ${actual ?? "missing"}; manifest: ${expected ?? "missing"})`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
async function verifyBootstrap(input) {
|
|
1185
|
+
const expected = await contracts();
|
|
1186
|
+
const core = deployedBootstrapStack(input.coreStack, expected.core);
|
|
1187
|
+
const oidc = deployedBootstrapStack(input.githubOidcStack, expected.githubOidc);
|
|
1188
|
+
for (const [kind, capture] of [
|
|
1189
|
+
["Core", capturedStackIdentity(input.coreStack)],
|
|
1190
|
+
["GitHub OIDC", capturedStackIdentity(input.githubOidcStack)]
|
|
1191
|
+
]) {
|
|
1192
|
+
const expectedName = kind === "Core" ? input.config.core?.stackName : input.config.githubOidc?.stackName;
|
|
1193
|
+
assertEqual(capture.StackName, expectedName, `${kind} stack name`);
|
|
1194
|
+
const capturedRegion = capture.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
|
|
1195
|
+
assertEqual(capturedRegion, input.config.region, `${kind} stack Region`);
|
|
1196
|
+
}
|
|
1197
|
+
const coreOutputs = coreBootstrapOutputs(core);
|
|
1198
|
+
const coreParameters = parameterValues(core);
|
|
1199
|
+
assertEqual(
|
|
1200
|
+
coreParameters.ArtifactBucketName,
|
|
1201
|
+
input.config.core?.artifactBucketName,
|
|
1202
|
+
"Artifact bucket"
|
|
1203
|
+
);
|
|
1204
|
+
assertEqual(
|
|
1205
|
+
coreParameters.ApplicationStackName,
|
|
1206
|
+
input.config.core?.applicationStackName,
|
|
1207
|
+
"Application stack"
|
|
1208
|
+
);
|
|
1209
|
+
assertEqual(
|
|
1210
|
+
coreParameters.DashboardFunctionName,
|
|
1211
|
+
input.config.core?.dashboardFunctionName,
|
|
1212
|
+
"Dashboard function"
|
|
1213
|
+
);
|
|
1214
|
+
assertEqual(
|
|
1215
|
+
coreOutputs.ArtifactBucketName,
|
|
1216
|
+
input.config.core?.artifactBucketName,
|
|
1217
|
+
"Core output artifact bucket"
|
|
1218
|
+
);
|
|
1219
|
+
assertEqual(
|
|
1220
|
+
coreOutputs.ApplicationStackName,
|
|
1221
|
+
input.config.core?.applicationStackName,
|
|
1222
|
+
"Core output application stack"
|
|
1223
|
+
);
|
|
1224
|
+
const oidcParameters = parameterValues(oidc);
|
|
1225
|
+
const github = input.config.githubOidc;
|
|
1226
|
+
const executionRole = required(
|
|
1227
|
+
coreOutputs.CloudFormationExecutionRoleArn,
|
|
1228
|
+
"core CloudFormationExecutionRoleArn output"
|
|
1229
|
+
);
|
|
1230
|
+
for (const [key, value] of Object.entries({
|
|
1231
|
+
GitHubOidcProviderArn: github?.providerArn,
|
|
1232
|
+
GitHubRepository: github?.repository,
|
|
1233
|
+
GitHubOwnerId: github?.ownerId,
|
|
1234
|
+
GitHubRepositoryId: github?.repositoryId,
|
|
1235
|
+
GitHubEnvironment: github?.environment,
|
|
1236
|
+
ApplicationStackName: coreOutputs.ApplicationStackName,
|
|
1237
|
+
ArtifactBucketName: coreOutputs.ArtifactBucketName,
|
|
1238
|
+
CloudFormationExecutionRoleArn: executionRole
|
|
1239
|
+
}))
|
|
1240
|
+
assertEqual(oidcParameters[key], value, `OIDC ${key}`);
|
|
1241
|
+
if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
|
|
1242
|
+
throw new Error("GitHub immutable owner and repository IDs must be numeric");
|
|
1243
|
+
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
1244
|
+
if (!owner || !repository || extra.length)
|
|
1245
|
+
throw new Error("GitHub repository must be owner/repository");
|
|
1246
|
+
const deployRole = outputValues(oidc).GitHubDeployRoleArn;
|
|
1247
|
+
if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
|
|
1248
|
+
const providerArn = github?.providerArn ?? "";
|
|
1249
|
+
const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
1250
|
+
const deployAccount = deployRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
|
|
1251
|
+
const executionAccount = executionRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
|
|
1252
|
+
if (!account || account !== deployAccount || account !== executionAccount)
|
|
1253
|
+
throw new Error("Provider and reviewed role ARNs must belong to the same AWS account");
|
|
1254
|
+
return {
|
|
1255
|
+
verified: true,
|
|
1256
|
+
immutableSubject: `repo:${owner}@${github?.ownerId}/${repository}@${github?.repositoryId}:environment:${github?.environment}`,
|
|
1257
|
+
environmentVariables: {
|
|
1258
|
+
AWS_DEPLOY_ROLE_ARN: deployRole,
|
|
1259
|
+
AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN: executionRole
|
|
1260
|
+
},
|
|
1261
|
+
reviewedArns: {
|
|
1262
|
+
githubDeployRoleArn: deployRole,
|
|
1263
|
+
cloudFormationExecutionRoleArn: executionRole
|
|
1264
|
+
},
|
|
1265
|
+
githubEnvironmentInstructions: [
|
|
1266
|
+
[
|
|
1267
|
+
"gh",
|
|
1268
|
+
"variable",
|
|
1269
|
+
"set",
|
|
1270
|
+
"AWS_DEPLOY_ROLE_ARN",
|
|
1271
|
+
"--repo",
|
|
1272
|
+
`${owner}/${repository}`,
|
|
1273
|
+
"--env",
|
|
1274
|
+
github?.environment ?? "",
|
|
1275
|
+
"--body",
|
|
1276
|
+
deployRole
|
|
1277
|
+
],
|
|
1278
|
+
[
|
|
1279
|
+
"gh",
|
|
1280
|
+
"variable",
|
|
1281
|
+
"set",
|
|
1282
|
+
"AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN",
|
|
1283
|
+
"--repo",
|
|
1284
|
+
`${owner}/${repository}`,
|
|
1285
|
+
"--env",
|
|
1286
|
+
github?.environment ?? "",
|
|
1287
|
+
"--body",
|
|
1288
|
+
executionRole
|
|
1289
|
+
]
|
|
1290
|
+
]
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
917
1294
|
// packages/aws/src/index.ts
|
|
918
1295
|
var run = promisify(execFile);
|
|
919
1296
|
function deploymentTemplate(template, values) {
|
|
@@ -1318,9 +1695,9 @@ try {
|
|
|
1318
1695
|
console.log(JSON.stringify({ published: true, version, assetPath }));
|
|
1319
1696
|
} else if (args[0] === "doctor") {
|
|
1320
1697
|
const parametersPath = option("--parameters", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
|
|
1321
|
-
const
|
|
1698
|
+
const region2 = option("--region", process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1") ?? "us-east-1";
|
|
1322
1699
|
const checks = await runDoctor(
|
|
1323
|
-
{ parametersPath, region },
|
|
1700
|
+
{ parametersPath, region: region2 },
|
|
1324
1701
|
{
|
|
1325
1702
|
async execute(command, commandArgs) {
|
|
1326
1703
|
const { execFile: execFile3 } = await import("node:child_process");
|
|
@@ -1380,9 +1757,45 @@ try {
|
|
|
1380
1757
|
console.log(JSON.stringify({ output }));
|
|
1381
1758
|
} else if (args[0] === "bootstrap") {
|
|
1382
1759
|
const action = args[1];
|
|
1383
|
-
const kind = bootstrapKind();
|
|
1384
1760
|
const config = await bootstrapConfig();
|
|
1385
|
-
if (action === "
|
|
1761
|
+
if (action === "handoff") {
|
|
1762
|
+
const configPath = requiredOption("--config");
|
|
1763
|
+
const coreStackPath = option("--core-stack-json");
|
|
1764
|
+
const githubStackPath = option("--github-oidc-stack-json");
|
|
1765
|
+
const runner = {
|
|
1766
|
+
async execute(command, commandArgs) {
|
|
1767
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
1768
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
1769
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
console.log(
|
|
1773
|
+
JSON.stringify(
|
|
1774
|
+
await bootstrapHandoff({
|
|
1775
|
+
config,
|
|
1776
|
+
configPath,
|
|
1777
|
+
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
1778
|
+
coreStackPath,
|
|
1779
|
+
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
1780
|
+
runner
|
|
1781
|
+
})
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
} else if (action === "verify") {
|
|
1785
|
+
requiredOption("--config");
|
|
1786
|
+
const coreStackPath = requiredOption("--core-stack-json");
|
|
1787
|
+
const githubStackPath = requiredOption("--github-oidc-stack-json");
|
|
1788
|
+
console.log(
|
|
1789
|
+
JSON.stringify(
|
|
1790
|
+
await verifyBootstrap({
|
|
1791
|
+
config,
|
|
1792
|
+
coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
|
|
1793
|
+
githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
|
|
1794
|
+
})
|
|
1795
|
+
)
|
|
1796
|
+
);
|
|
1797
|
+
} else if (action === "template") {
|
|
1798
|
+
const kind = bootstrapKind();
|
|
1386
1799
|
console.log(
|
|
1387
1800
|
JSON.stringify({
|
|
1388
1801
|
kind,
|
|
@@ -1391,6 +1804,7 @@ try {
|
|
|
1391
1804
|
})
|
|
1392
1805
|
);
|
|
1393
1806
|
} else if (action === "parameters") {
|
|
1807
|
+
const kind = bootstrapKind();
|
|
1394
1808
|
const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
|
|
1395
1809
|
let coreOutputs = {};
|
|
1396
1810
|
const coreStackPath = option("--core-stack-json");
|
|
@@ -1418,15 +1832,16 @@ try {
|
|
|
1418
1832
|
JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
|
|
1419
1833
|
);
|
|
1420
1834
|
} else if (action === "status") {
|
|
1421
|
-
const
|
|
1422
|
-
const
|
|
1835
|
+
const kind = bootstrapKind();
|
|
1836
|
+
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
1837
|
+
const region2 = option("--region", config.region);
|
|
1423
1838
|
const awsCommand = [
|
|
1424
1839
|
"aws",
|
|
1425
1840
|
"cloudformation",
|
|
1426
1841
|
"describe-stacks",
|
|
1427
1842
|
"--stack-name",
|
|
1428
|
-
|
|
1429
|
-
...
|
|
1843
|
+
stackName2,
|
|
1844
|
+
...region2 ? ["--region", region2] : [],
|
|
1430
1845
|
"--no-cli-pager"
|
|
1431
1846
|
];
|
|
1432
1847
|
console.log(
|
|
@@ -1438,17 +1853,18 @@ try {
|
|
|
1438
1853
|
})
|
|
1439
1854
|
);
|
|
1440
1855
|
} else if (action === "change-set") {
|
|
1441
|
-
const
|
|
1856
|
+
const kind = bootstrapKind();
|
|
1857
|
+
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
1442
1858
|
const changeSetName = requiredOption("--change-set-name");
|
|
1443
1859
|
const parametersPath = requiredOption("--parameters");
|
|
1444
|
-
const
|
|
1860
|
+
const region2 = option("--region", config.region);
|
|
1445
1861
|
await existingParameters(parametersPath);
|
|
1446
1862
|
const awsCommand = [
|
|
1447
1863
|
"aws",
|
|
1448
1864
|
"cloudformation",
|
|
1449
1865
|
"create-change-set",
|
|
1450
1866
|
"--stack-name",
|
|
1451
|
-
|
|
1867
|
+
stackName2,
|
|
1452
1868
|
"--change-set-name",
|
|
1453
1869
|
changeSetName,
|
|
1454
1870
|
"--change-set-type",
|
|
@@ -1459,7 +1875,7 @@ try {
|
|
|
1459
1875
|
`file://${parametersPath}`,
|
|
1460
1876
|
"--capabilities",
|
|
1461
1877
|
"CAPABILITY_NAMED_IAM",
|
|
1462
|
-
...
|
|
1878
|
+
...region2 ? ["--region", region2] : [],
|
|
1463
1879
|
"--no-cli-pager"
|
|
1464
1880
|
];
|
|
1465
1881
|
console.log(
|
|
@@ -1472,7 +1888,7 @@ try {
|
|
|
1472
1888
|
);
|
|
1473
1889
|
} else {
|
|
1474
1890
|
throw new Error(
|
|
1475
|
-
"Usage: ze-great-dashboard-aws bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
|
|
1891
|
+
"Usage: ze-great-dashboard-aws bootstrap handoff|verify --config manifest.json [options], or bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
|
|
1476
1892
|
);
|
|
1477
1893
|
}
|
|
1478
1894
|
} else if (args[0] !== "package")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type BootstrapConfig } from './bootstrap.js';
|
|
2
|
+
export type BootstrapPhase = 'core' | 'github-oidc' | 'github-environment' | 'application-gateway';
|
|
3
|
+
export type CommandRunner = {
|
|
4
|
+
execute(command: string, args: string[]): Promise<string>;
|
|
5
|
+
};
|
|
6
|
+
export type BootstrapProvider = {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
prerequisite(config: BootstrapConfig, runner?: CommandRunner): Promise<ProviderPrerequisite>;
|
|
9
|
+
};
|
|
10
|
+
export type ProviderPrerequisite = {
|
|
11
|
+
status: 'ready' | 'immutable-subject-required' | 'unverified';
|
|
12
|
+
blocking: boolean;
|
|
13
|
+
detail: string;
|
|
14
|
+
};
|
|
15
|
+
export type HandoffCommand = {
|
|
16
|
+
name: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
captureFile?: string;
|
|
19
|
+
};
|
|
20
|
+
export type BootstrapHandoff = {
|
|
21
|
+
phase: BootstrapPhase;
|
|
22
|
+
expectedContracts: {
|
|
23
|
+
core: string;
|
|
24
|
+
githubOidc: string;
|
|
25
|
+
};
|
|
26
|
+
templatePath?: string;
|
|
27
|
+
parameterPath?: string;
|
|
28
|
+
requiredCapturedFiles: string[];
|
|
29
|
+
reviewCheckpoints: string[];
|
|
30
|
+
commands: HandoffCommand[];
|
|
31
|
+
prerequisite?: ProviderPrerequisite;
|
|
32
|
+
};
|
|
33
|
+
export type BootstrapVerification = {
|
|
34
|
+
verified: true;
|
|
35
|
+
immutableSubject: string;
|
|
36
|
+
environmentVariables: {
|
|
37
|
+
AWS_DEPLOY_ROLE_ARN: string;
|
|
38
|
+
AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN: string;
|
|
39
|
+
};
|
|
40
|
+
reviewedArns: {
|
|
41
|
+
githubDeployRoleArn: string;
|
|
42
|
+
cloudFormationExecutionRoleArn: string;
|
|
43
|
+
};
|
|
44
|
+
githubEnvironmentInstructions: string[][];
|
|
45
|
+
};
|
|
46
|
+
export declare const githubOidcProvider: BootstrapProvider;
|
|
47
|
+
export declare function bootstrapHandoff(input: {
|
|
48
|
+
config: BootstrapConfig;
|
|
49
|
+
configPath: string;
|
|
50
|
+
coreStack?: unknown;
|
|
51
|
+
coreStackPath?: string;
|
|
52
|
+
githubOidcStack?: unknown;
|
|
53
|
+
provider?: BootstrapProvider;
|
|
54
|
+
runner?: CommandRunner;
|
|
55
|
+
}): Promise<BootstrapHandoff>;
|
|
56
|
+
export declare function verifyBootstrap(input: {
|
|
57
|
+
config: BootstrapConfig;
|
|
58
|
+
coreStack: unknown;
|
|
59
|
+
githubOidcStack: unknown;
|
|
60
|
+
}): Promise<BootstrapVerification>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { type BootstrapConfig, type BootstrapKind, bootstrapContractVersion, bootstrapTemplate, bootstrapTemplatePath, type CloudFormationParameterValue, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requiredBootstrapParameters, } from './bootstrap.js';
|
|
2
|
+
export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
|
|
2
3
|
export type ReleaseMetadata = {
|
|
3
4
|
dashboardVersion: string;
|
|
4
5
|
clientAssetUrl: string;
|
package/dist/index.js
CHANGED
|
@@ -879,8 +879,8 @@ function coreBootstrapOutputs(stack) {
|
|
|
879
879
|
(output) => Boolean(output.OutputValue)
|
|
880
880
|
).map((output) => [output.OutputKey, output.OutputValue])
|
|
881
881
|
);
|
|
882
|
-
const
|
|
883
|
-
const missing =
|
|
882
|
+
const required2 = ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"];
|
|
883
|
+
const missing = required2.filter((key) => !values[key]);
|
|
884
884
|
if (missing.length) throw new Error(`Core stack JSON is missing outputs: ${missing.join(", ")}`);
|
|
885
885
|
return values;
|
|
886
886
|
}
|
|
@@ -901,6 +901,383 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
901
901
|
return [...keys, ...optional].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
+
// packages/aws/src/handoff.ts
|
|
905
|
+
var githubSubjectKeys = [
|
|
906
|
+
"repository_owner",
|
|
907
|
+
"repository_owner_id",
|
|
908
|
+
"repository",
|
|
909
|
+
"repository_id",
|
|
910
|
+
"context"
|
|
911
|
+
];
|
|
912
|
+
function outputValues(stack) {
|
|
913
|
+
return Object.fromEntries(
|
|
914
|
+
(stack.Outputs ?? []).filter(
|
|
915
|
+
(entry) => typeof entry.OutputKey === "string" && typeof entry.OutputValue === "string"
|
|
916
|
+
).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
function parameterValues(stack) {
|
|
920
|
+
return Object.fromEntries(
|
|
921
|
+
(stack.Parameters ?? []).filter(
|
|
922
|
+
(entry) => typeof entry.ParameterKey === "string" && typeof entry.ParameterValue === "string"
|
|
923
|
+
).map(({ ParameterKey, ParameterValue }) => [ParameterKey, ParameterValue])
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
function capturedStackIdentity(input) {
|
|
927
|
+
const candidate = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
|
|
928
|
+
if (!candidate || typeof candidate !== "object") return {};
|
|
929
|
+
const { StackName, StackId } = candidate;
|
|
930
|
+
return {
|
|
931
|
+
...typeof StackName === "string" ? { StackName } : {},
|
|
932
|
+
...typeof StackId === "string" ? { StackId } : {}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function required(value, name) {
|
|
936
|
+
if (!value) throw new Error(`Bootstrap manifest is missing ${name}`);
|
|
937
|
+
return value;
|
|
938
|
+
}
|
|
939
|
+
function region(config) {
|
|
940
|
+
return required(config.region, "region");
|
|
941
|
+
}
|
|
942
|
+
function stackName(config, kind) {
|
|
943
|
+
return required(
|
|
944
|
+
kind === "core" ? config.core?.stackName : config.githubOidc?.stackName,
|
|
945
|
+
`${kind}.stackName`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
function changeSetCommands(input) {
|
|
949
|
+
const name = `${input.kind}-initial-review`;
|
|
950
|
+
const stack = stackName(input.config, input.kind);
|
|
951
|
+
const common = ["--stack-name", stack, "--region", region(input.config)];
|
|
952
|
+
const parameters = [
|
|
953
|
+
"ze-great-dashboard-aws",
|
|
954
|
+
"bootstrap",
|
|
955
|
+
"parameters",
|
|
956
|
+
"--kind",
|
|
957
|
+
input.kind,
|
|
958
|
+
"--config",
|
|
959
|
+
input.configPath,
|
|
960
|
+
"--output",
|
|
961
|
+
input.parameterPath
|
|
962
|
+
];
|
|
963
|
+
if (input.kind === "github-oidc")
|
|
964
|
+
parameters.push("--core-stack-json", input.coreCapturePath ?? "core-deployed-stack.json");
|
|
965
|
+
return [
|
|
966
|
+
{ name: "generate-parameters", args: parameters },
|
|
967
|
+
{
|
|
968
|
+
name: "create-change-set",
|
|
969
|
+
args: [
|
|
970
|
+
"aws",
|
|
971
|
+
"cloudformation",
|
|
972
|
+
"create-change-set",
|
|
973
|
+
...common,
|
|
974
|
+
"--change-set-name",
|
|
975
|
+
name,
|
|
976
|
+
"--change-set-type",
|
|
977
|
+
"CREATE",
|
|
978
|
+
"--template-body",
|
|
979
|
+
`file://${input.templatePath}`,
|
|
980
|
+
"--parameters",
|
|
981
|
+
`file://${input.parameterPath}`,
|
|
982
|
+
"--capabilities",
|
|
983
|
+
"CAPABILITY_NAMED_IAM",
|
|
984
|
+
"--no-cli-pager"
|
|
985
|
+
]
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: "wait-for-change-set",
|
|
989
|
+
args: [
|
|
990
|
+
"aws",
|
|
991
|
+
"cloudformation",
|
|
992
|
+
"wait",
|
|
993
|
+
"change-set-create-complete",
|
|
994
|
+
...common,
|
|
995
|
+
"--change-set-name",
|
|
996
|
+
name
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: "review-change-set",
|
|
1001
|
+
args: [
|
|
1002
|
+
"aws",
|
|
1003
|
+
"cloudformation",
|
|
1004
|
+
"describe-change-set",
|
|
1005
|
+
...common,
|
|
1006
|
+
"--change-set-name",
|
|
1007
|
+
name,
|
|
1008
|
+
"--no-cli-pager"
|
|
1009
|
+
]
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
name: "execute-reviewed-change-set",
|
|
1013
|
+
args: ["aws", "cloudformation", "execute-change-set", ...common, "--change-set-name", name]
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "wait-for-stack",
|
|
1017
|
+
args: ["aws", "cloudformation", "wait", "stack-create-complete", ...common]
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "capture-stack",
|
|
1021
|
+
captureFile: `${input.kind}-deployed-stack.json`,
|
|
1022
|
+
args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
|
|
1023
|
+
}
|
|
1024
|
+
];
|
|
1025
|
+
}
|
|
1026
|
+
var githubOidcProvider = {
|
|
1027
|
+
name: "github-oidc",
|
|
1028
|
+
async prerequisite(config, runner) {
|
|
1029
|
+
const repository = config.githubOidc?.repository;
|
|
1030
|
+
if (!repository)
|
|
1031
|
+
return {
|
|
1032
|
+
status: "unverified",
|
|
1033
|
+
blocking: false,
|
|
1034
|
+
detail: "GitHub repository is absent from the manifest."
|
|
1035
|
+
};
|
|
1036
|
+
if (!runner)
|
|
1037
|
+
return {
|
|
1038
|
+
status: "unverified",
|
|
1039
|
+
blocking: false,
|
|
1040
|
+
detail: "GitHub OIDC subject setting was not queried."
|
|
1041
|
+
};
|
|
1042
|
+
try {
|
|
1043
|
+
const response = JSON.parse(
|
|
1044
|
+
await runner.execute("gh", ["api", `repos/${repository}/actions/oidc/customization/sub`])
|
|
1045
|
+
);
|
|
1046
|
+
const keys = response.include_claim_keys;
|
|
1047
|
+
if (response.use_default === false && Array.isArray(keys) && githubSubjectKeys.every((key) => keys.includes(key)))
|
|
1048
|
+
return {
|
|
1049
|
+
status: "ready",
|
|
1050
|
+
blocking: false,
|
|
1051
|
+
detail: "GitHub emits the immutable owner/repository-ID OIDC subject required by this adapter."
|
|
1052
|
+
};
|
|
1053
|
+
return {
|
|
1054
|
+
status: "immutable-subject-required",
|
|
1055
|
+
blocking: true,
|
|
1056
|
+
detail: "Coordinate migration: inventory and temporarily make existing name-based trust policies compatible, enable immutable GitHub OIDC subjects as a separate GitHub-admin action, verify existing deployments, then retire legacy trust."
|
|
1057
|
+
};
|
|
1058
|
+
} catch {
|
|
1059
|
+
return {
|
|
1060
|
+
status: "unverified",
|
|
1061
|
+
blocking: false,
|
|
1062
|
+
detail: "GitHub OIDC subject setting could not be verified (gh CLI, authentication, permission, or network unavailable)."
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
async function contracts() {
|
|
1068
|
+
return {
|
|
1069
|
+
core: bootstrapContractVersion(await bootstrapTemplate("core")),
|
|
1070
|
+
githubOidc: bootstrapContractVersion(await bootstrapTemplate("github-oidc"))
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
async function bootstrapHandoff(input) {
|
|
1074
|
+
const expectedContracts = await contracts();
|
|
1075
|
+
const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
|
|
1076
|
+
const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
|
|
1077
|
+
const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
|
|
1078
|
+
if (!core) {
|
|
1079
|
+
requiredBootstrapParameters("core", {
|
|
1080
|
+
ArtifactBucketName: input.config.core?.artifactBucketName,
|
|
1081
|
+
ApplicationStackName: input.config.core?.applicationStackName,
|
|
1082
|
+
DashboardFunctionName: input.config.core?.dashboardFunctionName
|
|
1083
|
+
});
|
|
1084
|
+
const templatePath = await bootstrapTemplatePath("core");
|
|
1085
|
+
return {
|
|
1086
|
+
phase: "core",
|
|
1087
|
+
expectedContracts,
|
|
1088
|
+
templatePath,
|
|
1089
|
+
parameterPath: "core-bootstrap.json",
|
|
1090
|
+
requiredCapturedFiles: [],
|
|
1091
|
+
reviewCheckpoints: [
|
|
1092
|
+
"Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
|
|
1093
|
+
"Do not approve retained bucket or role replacements."
|
|
1094
|
+
],
|
|
1095
|
+
commands: changeSetCommands({
|
|
1096
|
+
kind: "core",
|
|
1097
|
+
config: input.config,
|
|
1098
|
+
configPath: input.configPath,
|
|
1099
|
+
templatePath,
|
|
1100
|
+
parameterPath: "core-bootstrap.json"
|
|
1101
|
+
})
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
coreBootstrapOutputs(core);
|
|
1105
|
+
if (!oidc) {
|
|
1106
|
+
requiredBootstrapParameters("github-oidc", {
|
|
1107
|
+
GitHubOidcProviderArn: input.config.githubOidc?.providerArn,
|
|
1108
|
+
GitHubRepository: input.config.githubOidc?.repository,
|
|
1109
|
+
GitHubOwnerId: input.config.githubOidc?.ownerId,
|
|
1110
|
+
GitHubRepositoryId: input.config.githubOidc?.repositoryId,
|
|
1111
|
+
GitHubEnvironment: input.config.githubOidc?.environment,
|
|
1112
|
+
...coreBootstrapOutputs(core)
|
|
1113
|
+
});
|
|
1114
|
+
const templatePath = await bootstrapTemplatePath("github-oidc");
|
|
1115
|
+
return {
|
|
1116
|
+
phase: "github-oidc",
|
|
1117
|
+
expectedContracts,
|
|
1118
|
+
templatePath,
|
|
1119
|
+
parameterPath: "github-oidc-bootstrap.json",
|
|
1120
|
+
requiredCapturedFiles: [coreCapturePath],
|
|
1121
|
+
reviewCheckpoints: [
|
|
1122
|
+
"Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
|
|
1123
|
+
"Do not execute until the GitHub Environment prerequisite is ready."
|
|
1124
|
+
],
|
|
1125
|
+
commands: changeSetCommands({
|
|
1126
|
+
kind: "github-oidc",
|
|
1127
|
+
config: input.config,
|
|
1128
|
+
configPath: input.configPath,
|
|
1129
|
+
templatePath,
|
|
1130
|
+
parameterPath: "github-oidc-bootstrap.json",
|
|
1131
|
+
coreCapturePath
|
|
1132
|
+
}),
|
|
1133
|
+
prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1134
|
+
input.config,
|
|
1135
|
+
input.runner
|
|
1136
|
+
)
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1140
|
+
input.config,
|
|
1141
|
+
input.runner
|
|
1142
|
+
);
|
|
1143
|
+
if (prerequisite.status !== "ready")
|
|
1144
|
+
return {
|
|
1145
|
+
phase: "github-environment",
|
|
1146
|
+
expectedContracts,
|
|
1147
|
+
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1148
|
+
reviewCheckpoints: [
|
|
1149
|
+
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
1150
|
+
],
|
|
1151
|
+
commands: [],
|
|
1152
|
+
prerequisite
|
|
1153
|
+
};
|
|
1154
|
+
return {
|
|
1155
|
+
phase: "application-gateway",
|
|
1156
|
+
expectedContracts,
|
|
1157
|
+
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1158
|
+
reviewCheckpoints: [
|
|
1159
|
+
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
1160
|
+
],
|
|
1161
|
+
commands: [],
|
|
1162
|
+
prerequisite
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function assertEqual(actual, expected, label) {
|
|
1166
|
+
if (actual !== expected)
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`${label} mismatch (captured: ${actual ?? "missing"}; manifest: ${expected ?? "missing"})`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
async function verifyBootstrap(input) {
|
|
1172
|
+
const expected = await contracts();
|
|
1173
|
+
const core = deployedBootstrapStack(input.coreStack, expected.core);
|
|
1174
|
+
const oidc = deployedBootstrapStack(input.githubOidcStack, expected.githubOidc);
|
|
1175
|
+
for (const [kind, capture] of [
|
|
1176
|
+
["Core", capturedStackIdentity(input.coreStack)],
|
|
1177
|
+
["GitHub OIDC", capturedStackIdentity(input.githubOidcStack)]
|
|
1178
|
+
]) {
|
|
1179
|
+
const expectedName = kind === "Core" ? input.config.core?.stackName : input.config.githubOidc?.stackName;
|
|
1180
|
+
assertEqual(capture.StackName, expectedName, `${kind} stack name`);
|
|
1181
|
+
const capturedRegion = capture.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
|
|
1182
|
+
assertEqual(capturedRegion, input.config.region, `${kind} stack Region`);
|
|
1183
|
+
}
|
|
1184
|
+
const coreOutputs = coreBootstrapOutputs(core);
|
|
1185
|
+
const coreParameters = parameterValues(core);
|
|
1186
|
+
assertEqual(
|
|
1187
|
+
coreParameters.ArtifactBucketName,
|
|
1188
|
+
input.config.core?.artifactBucketName,
|
|
1189
|
+
"Artifact bucket"
|
|
1190
|
+
);
|
|
1191
|
+
assertEqual(
|
|
1192
|
+
coreParameters.ApplicationStackName,
|
|
1193
|
+
input.config.core?.applicationStackName,
|
|
1194
|
+
"Application stack"
|
|
1195
|
+
);
|
|
1196
|
+
assertEqual(
|
|
1197
|
+
coreParameters.DashboardFunctionName,
|
|
1198
|
+
input.config.core?.dashboardFunctionName,
|
|
1199
|
+
"Dashboard function"
|
|
1200
|
+
);
|
|
1201
|
+
assertEqual(
|
|
1202
|
+
coreOutputs.ArtifactBucketName,
|
|
1203
|
+
input.config.core?.artifactBucketName,
|
|
1204
|
+
"Core output artifact bucket"
|
|
1205
|
+
);
|
|
1206
|
+
assertEqual(
|
|
1207
|
+
coreOutputs.ApplicationStackName,
|
|
1208
|
+
input.config.core?.applicationStackName,
|
|
1209
|
+
"Core output application stack"
|
|
1210
|
+
);
|
|
1211
|
+
const oidcParameters = parameterValues(oidc);
|
|
1212
|
+
const github = input.config.githubOidc;
|
|
1213
|
+
const executionRole = required(
|
|
1214
|
+
coreOutputs.CloudFormationExecutionRoleArn,
|
|
1215
|
+
"core CloudFormationExecutionRoleArn output"
|
|
1216
|
+
);
|
|
1217
|
+
for (const [key, value] of Object.entries({
|
|
1218
|
+
GitHubOidcProviderArn: github?.providerArn,
|
|
1219
|
+
GitHubRepository: github?.repository,
|
|
1220
|
+
GitHubOwnerId: github?.ownerId,
|
|
1221
|
+
GitHubRepositoryId: github?.repositoryId,
|
|
1222
|
+
GitHubEnvironment: github?.environment,
|
|
1223
|
+
ApplicationStackName: coreOutputs.ApplicationStackName,
|
|
1224
|
+
ArtifactBucketName: coreOutputs.ArtifactBucketName,
|
|
1225
|
+
CloudFormationExecutionRoleArn: executionRole
|
|
1226
|
+
}))
|
|
1227
|
+
assertEqual(oidcParameters[key], value, `OIDC ${key}`);
|
|
1228
|
+
if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
|
|
1229
|
+
throw new Error("GitHub immutable owner and repository IDs must be numeric");
|
|
1230
|
+
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
1231
|
+
if (!owner || !repository || extra.length)
|
|
1232
|
+
throw new Error("GitHub repository must be owner/repository");
|
|
1233
|
+
const deployRole = outputValues(oidc).GitHubDeployRoleArn;
|
|
1234
|
+
if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
|
|
1235
|
+
const providerArn = github?.providerArn ?? "";
|
|
1236
|
+
const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
1237
|
+
const deployAccount = deployRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
|
|
1238
|
+
const executionAccount = executionRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
|
|
1239
|
+
if (!account || account !== deployAccount || account !== executionAccount)
|
|
1240
|
+
throw new Error("Provider and reviewed role ARNs must belong to the same AWS account");
|
|
1241
|
+
return {
|
|
1242
|
+
verified: true,
|
|
1243
|
+
immutableSubject: `repo:${owner}@${github?.ownerId}/${repository}@${github?.repositoryId}:environment:${github?.environment}`,
|
|
1244
|
+
environmentVariables: {
|
|
1245
|
+
AWS_DEPLOY_ROLE_ARN: deployRole,
|
|
1246
|
+
AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN: executionRole
|
|
1247
|
+
},
|
|
1248
|
+
reviewedArns: {
|
|
1249
|
+
githubDeployRoleArn: deployRole,
|
|
1250
|
+
cloudFormationExecutionRoleArn: executionRole
|
|
1251
|
+
},
|
|
1252
|
+
githubEnvironmentInstructions: [
|
|
1253
|
+
[
|
|
1254
|
+
"gh",
|
|
1255
|
+
"variable",
|
|
1256
|
+
"set",
|
|
1257
|
+
"AWS_DEPLOY_ROLE_ARN",
|
|
1258
|
+
"--repo",
|
|
1259
|
+
`${owner}/${repository}`,
|
|
1260
|
+
"--env",
|
|
1261
|
+
github?.environment ?? "",
|
|
1262
|
+
"--body",
|
|
1263
|
+
deployRole
|
|
1264
|
+
],
|
|
1265
|
+
[
|
|
1266
|
+
"gh",
|
|
1267
|
+
"variable",
|
|
1268
|
+
"set",
|
|
1269
|
+
"AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN",
|
|
1270
|
+
"--repo",
|
|
1271
|
+
`${owner}/${repository}`,
|
|
1272
|
+
"--env",
|
|
1273
|
+
github?.environment ?? "",
|
|
1274
|
+
"--body",
|
|
1275
|
+
executionRole
|
|
1276
|
+
]
|
|
1277
|
+
]
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
904
1281
|
// packages/aws/src/index.ts
|
|
905
1282
|
var run = promisify(execFile);
|
|
906
1283
|
function deploymentTemplate(template, values) {
|
|
@@ -1041,14 +1418,17 @@ async function cloudFormationTemplate() {
|
|
|
1041
1418
|
}
|
|
1042
1419
|
export {
|
|
1043
1420
|
bootstrapContractVersion,
|
|
1421
|
+
bootstrapHandoff,
|
|
1044
1422
|
bootstrapTemplate,
|
|
1045
1423
|
bootstrapTemplatePath,
|
|
1046
1424
|
cloudFormationTemplate,
|
|
1047
1425
|
coreBootstrapOutputs,
|
|
1048
1426
|
deployLambda,
|
|
1049
1427
|
deployedBootstrapStack,
|
|
1428
|
+
githubOidcProvider,
|
|
1050
1429
|
mergeBootstrapParameters,
|
|
1051
1430
|
packageLambda,
|
|
1052
1431
|
publishClientAssets,
|
|
1053
|
-
requiredBootstrapParameters
|
|
1432
|
+
requiredBootstrapParameters,
|
|
1433
|
+
verifyBootstrap
|
|
1054
1434
|
};
|