@continuous-excellence/ze-great-dashboard-aws 0.1.33 → 0.3.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 +877 -113
- package/dist/guided.d.ts +32 -0
- package/dist/handoff.d.ts +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +662 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -743,7 +743,7 @@ import { z as z2 } from "zod";
|
|
|
743
743
|
|
|
744
744
|
// packages/aws/src/internal-board.ts
|
|
745
745
|
import { z } from "zod";
|
|
746
|
-
var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((
|
|
746
|
+
var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value2) => Number.parseInt(value2, 10) > 0).brand();
|
|
747
747
|
var positionSchema = z.object({
|
|
748
748
|
x: z.number().int().min(0),
|
|
749
749
|
y: z.number().int().min(0),
|
|
@@ -834,8 +834,8 @@ async function assembleRelease(input) {
|
|
|
834
834
|
);
|
|
835
835
|
return { metadata, files };
|
|
836
836
|
}
|
|
837
|
-
function sha256(
|
|
838
|
-
return createHash("sha256").update(
|
|
837
|
+
function sha256(value2) {
|
|
838
|
+
return createHash("sha256").update(value2).digest("hex");
|
|
839
839
|
}
|
|
840
840
|
|
|
841
841
|
// packages/aws/src/bootstrap.ts
|
|
@@ -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
|
}
|
|
@@ -910,8 +910,656 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
910
910
|
];
|
|
911
911
|
const missing = keys.filter((key) => !values[key]);
|
|
912
912
|
if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
|
|
913
|
-
const
|
|
914
|
-
return [...keys, ...
|
|
913
|
+
const optional2 = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
|
|
914
|
+
return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
915
|
+
}
|
|
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(value2, name) {
|
|
949
|
+
if (!value2) throw new Error(`Bootstrap manifest is missing ${name}`);
|
|
950
|
+
return value2;
|
|
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
|
+
expectedOutputs: [
|
|
1104
|
+
"BootstrapContractVersion",
|
|
1105
|
+
"ArtifactBucketName",
|
|
1106
|
+
"ApplicationStackName",
|
|
1107
|
+
"CloudFormationExecutionRoleArn"
|
|
1108
|
+
],
|
|
1109
|
+
requiredCapturedFiles: [],
|
|
1110
|
+
reviewCheckpoints: [
|
|
1111
|
+
"Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
|
|
1112
|
+
"Do not approve retained bucket or role replacements."
|
|
1113
|
+
],
|
|
1114
|
+
commands: changeSetCommands({
|
|
1115
|
+
kind: "core",
|
|
1116
|
+
config: input.config,
|
|
1117
|
+
configPath: input.configPath,
|
|
1118
|
+
templatePath,
|
|
1119
|
+
parameterPath: "core-bootstrap.json"
|
|
1120
|
+
})
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
coreBootstrapOutputs(core);
|
|
1124
|
+
if (!oidc) {
|
|
1125
|
+
requiredBootstrapParameters("github-oidc", {
|
|
1126
|
+
GitHubOidcProviderArn: input.config.githubOidc?.providerArn,
|
|
1127
|
+
GitHubRepository: input.config.githubOidc?.repository,
|
|
1128
|
+
GitHubOwnerId: input.config.githubOidc?.ownerId,
|
|
1129
|
+
GitHubRepositoryId: input.config.githubOidc?.repositoryId,
|
|
1130
|
+
GitHubEnvironment: input.config.githubOidc?.environment,
|
|
1131
|
+
...coreBootstrapOutputs(core)
|
|
1132
|
+
});
|
|
1133
|
+
const templatePath = await bootstrapTemplatePath("github-oidc");
|
|
1134
|
+
return {
|
|
1135
|
+
phase: "github-oidc",
|
|
1136
|
+
expectedContracts,
|
|
1137
|
+
templatePath,
|
|
1138
|
+
parameterPath: "github-oidc-bootstrap.json",
|
|
1139
|
+
expectedOutputs: ["BootstrapContractVersion", "GitHubDeployRoleArn"],
|
|
1140
|
+
requiredCapturedFiles: [coreCapturePath],
|
|
1141
|
+
reviewCheckpoints: [
|
|
1142
|
+
"Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
|
|
1143
|
+
"Do not execute until the GitHub Environment prerequisite is ready."
|
|
1144
|
+
],
|
|
1145
|
+
commands: changeSetCommands({
|
|
1146
|
+
kind: "github-oidc",
|
|
1147
|
+
config: input.config,
|
|
1148
|
+
configPath: input.configPath,
|
|
1149
|
+
templatePath,
|
|
1150
|
+
parameterPath: "github-oidc-bootstrap.json",
|
|
1151
|
+
coreCapturePath
|
|
1152
|
+
}),
|
|
1153
|
+
prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1154
|
+
input.config,
|
|
1155
|
+
input.runner
|
|
1156
|
+
)
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1160
|
+
input.config,
|
|
1161
|
+
input.runner
|
|
1162
|
+
);
|
|
1163
|
+
if (prerequisite.status !== "ready")
|
|
1164
|
+
return {
|
|
1165
|
+
phase: "github-environment",
|
|
1166
|
+
expectedContracts,
|
|
1167
|
+
expectedOutputs: [],
|
|
1168
|
+
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1169
|
+
reviewCheckpoints: [
|
|
1170
|
+
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
1171
|
+
],
|
|
1172
|
+
commands: [],
|
|
1173
|
+
prerequisite
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
phase: "application-gateway",
|
|
1177
|
+
expectedContracts,
|
|
1178
|
+
expectedOutputs: [],
|
|
1179
|
+
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1180
|
+
reviewCheckpoints: [
|
|
1181
|
+
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
1182
|
+
],
|
|
1183
|
+
commands: [],
|
|
1184
|
+
prerequisite
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function assertEqual(actual, expected, label) {
|
|
1188
|
+
if (actual !== expected)
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
`${label} mismatch (captured: ${actual ?? "missing"}; manifest: ${expected ?? "missing"})`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
async function verifyBootstrap(input) {
|
|
1194
|
+
const expected = await contracts();
|
|
1195
|
+
const core = deployedBootstrapStack(input.coreStack, expected.core);
|
|
1196
|
+
const oidc = deployedBootstrapStack(input.githubOidcStack, expected.githubOidc);
|
|
1197
|
+
for (const [kind, capture] of [
|
|
1198
|
+
["Core", capturedStackIdentity(input.coreStack)],
|
|
1199
|
+
["GitHub OIDC", capturedStackIdentity(input.githubOidcStack)]
|
|
1200
|
+
]) {
|
|
1201
|
+
const expectedName = kind === "Core" ? input.config.core?.stackName : input.config.githubOidc?.stackName;
|
|
1202
|
+
assertEqual(capture.StackName, expectedName, `${kind} stack name`);
|
|
1203
|
+
const capturedRegion = capture.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
|
|
1204
|
+
assertEqual(capturedRegion, input.config.region, `${kind} stack Region`);
|
|
1205
|
+
}
|
|
1206
|
+
const coreOutputs = coreBootstrapOutputs(core);
|
|
1207
|
+
const coreParameters = parameterValues(core);
|
|
1208
|
+
assertEqual(
|
|
1209
|
+
coreParameters.ArtifactBucketName,
|
|
1210
|
+
input.config.core?.artifactBucketName,
|
|
1211
|
+
"Artifact bucket"
|
|
1212
|
+
);
|
|
1213
|
+
assertEqual(
|
|
1214
|
+
coreParameters.ApplicationStackName,
|
|
1215
|
+
input.config.core?.applicationStackName,
|
|
1216
|
+
"Application stack"
|
|
1217
|
+
);
|
|
1218
|
+
assertEqual(
|
|
1219
|
+
coreParameters.DashboardFunctionName,
|
|
1220
|
+
input.config.core?.dashboardFunctionName,
|
|
1221
|
+
"Dashboard function"
|
|
1222
|
+
);
|
|
1223
|
+
assertEqual(
|
|
1224
|
+
coreOutputs.ArtifactBucketName,
|
|
1225
|
+
input.config.core?.artifactBucketName,
|
|
1226
|
+
"Core output artifact bucket"
|
|
1227
|
+
);
|
|
1228
|
+
assertEqual(
|
|
1229
|
+
coreOutputs.ApplicationStackName,
|
|
1230
|
+
input.config.core?.applicationStackName,
|
|
1231
|
+
"Core output application stack"
|
|
1232
|
+
);
|
|
1233
|
+
const oidcParameters = parameterValues(oidc);
|
|
1234
|
+
const github = input.config.githubOidc;
|
|
1235
|
+
const executionRole = required(
|
|
1236
|
+
coreOutputs.CloudFormationExecutionRoleArn,
|
|
1237
|
+
"core CloudFormationExecutionRoleArn output"
|
|
1238
|
+
);
|
|
1239
|
+
for (const [key, value2] of Object.entries({
|
|
1240
|
+
GitHubOidcProviderArn: github?.providerArn,
|
|
1241
|
+
GitHubRepository: github?.repository,
|
|
1242
|
+
GitHubOwnerId: github?.ownerId,
|
|
1243
|
+
GitHubRepositoryId: github?.repositoryId,
|
|
1244
|
+
GitHubEnvironment: github?.environment,
|
|
1245
|
+
ApplicationStackName: coreOutputs.ApplicationStackName,
|
|
1246
|
+
ArtifactBucketName: coreOutputs.ArtifactBucketName,
|
|
1247
|
+
CloudFormationExecutionRoleArn: executionRole
|
|
1248
|
+
}))
|
|
1249
|
+
assertEqual(oidcParameters[key], value2, `OIDC ${key}`);
|
|
1250
|
+
if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
|
|
1251
|
+
throw new Error("GitHub immutable owner and repository IDs must be numeric");
|
|
1252
|
+
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
1253
|
+
if (!owner || !repository || extra.length)
|
|
1254
|
+
throw new Error("GitHub repository must be owner/repository");
|
|
1255
|
+
const deployRole = outputValues(oidc).GitHubDeployRoleArn;
|
|
1256
|
+
if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
|
|
1257
|
+
const providerArn = github?.providerArn ?? "";
|
|
1258
|
+
const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
1259
|
+
const deployAccount = deployRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
|
|
1260
|
+
const executionAccount = executionRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
|
|
1261
|
+
if (!account || account !== deployAccount || account !== executionAccount)
|
|
1262
|
+
throw new Error("Provider and reviewed role ARNs must belong to the same AWS account");
|
|
1263
|
+
return {
|
|
1264
|
+
verified: true,
|
|
1265
|
+
immutableSubject: `repo:${owner}@${github?.ownerId}/${repository}@${github?.repositoryId}:environment:${github?.environment}`,
|
|
1266
|
+
environmentVariables: {
|
|
1267
|
+
AWS_DEPLOY_ROLE_ARN: deployRole,
|
|
1268
|
+
AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN: executionRole
|
|
1269
|
+
},
|
|
1270
|
+
reviewedArns: {
|
|
1271
|
+
githubDeployRoleArn: deployRole,
|
|
1272
|
+
cloudFormationExecutionRoleArn: executionRole
|
|
1273
|
+
},
|
|
1274
|
+
githubEnvironmentInstructions: [
|
|
1275
|
+
[
|
|
1276
|
+
"gh",
|
|
1277
|
+
"variable",
|
|
1278
|
+
"set",
|
|
1279
|
+
"AWS_DEPLOY_ROLE_ARN",
|
|
1280
|
+
"--repo",
|
|
1281
|
+
`${owner}/${repository}`,
|
|
1282
|
+
"--env",
|
|
1283
|
+
github?.environment ?? "",
|
|
1284
|
+
"--body",
|
|
1285
|
+
deployRole
|
|
1286
|
+
],
|
|
1287
|
+
[
|
|
1288
|
+
"gh",
|
|
1289
|
+
"variable",
|
|
1290
|
+
"set",
|
|
1291
|
+
"AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN",
|
|
1292
|
+
"--repo",
|
|
1293
|
+
`${owner}/${repository}`,
|
|
1294
|
+
"--env",
|
|
1295
|
+
github?.environment ?? "",
|
|
1296
|
+
"--body",
|
|
1297
|
+
executionRole
|
|
1298
|
+
]
|
|
1299
|
+
]
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// packages/aws/src/guided.ts
|
|
1304
|
+
function value(input, label) {
|
|
1305
|
+
if (typeof input !== "string" || !input.trim()) throw new Error(`${label} is required`);
|
|
1306
|
+
return input.trim();
|
|
1307
|
+
}
|
|
1308
|
+
function repositoryParts(repository) {
|
|
1309
|
+
const [owner, name, extra] = repository.split("/");
|
|
1310
|
+
if (!owner || !name || extra) throw new Error("repository must be owner/repository");
|
|
1311
|
+
return [owner, name];
|
|
1312
|
+
}
|
|
1313
|
+
function accountFromArn(arn) {
|
|
1314
|
+
return arn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
1315
|
+
}
|
|
1316
|
+
async function optional(runner, command, args2) {
|
|
1317
|
+
if (!runner) return void 0;
|
|
1318
|
+
try {
|
|
1319
|
+
return await runner.execute(command, args2);
|
|
1320
|
+
} catch {
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function discovered(input) {
|
|
1325
|
+
const identity = await optional(input.runner, "aws", [
|
|
1326
|
+
"sts",
|
|
1327
|
+
"get-caller-identity",
|
|
1328
|
+
"--output",
|
|
1329
|
+
"json"
|
|
1330
|
+
]);
|
|
1331
|
+
const aws = parseOrUnavailable(identity) ?? {};
|
|
1332
|
+
const accountId = typeof aws.Account === "string" ? aws.Account : void 0;
|
|
1333
|
+
const region2 = await optional(input.runner, "aws", ["configure", "get", "region"]);
|
|
1334
|
+
const repo = await optional(input.runner, "gh", ["api", `repos/${input.repository}`]);
|
|
1335
|
+
const github = parseOrUnavailable(repo) ?? {};
|
|
1336
|
+
return {
|
|
1337
|
+
accountId,
|
|
1338
|
+
region: region2?.trim() || void 0,
|
|
1339
|
+
ownerId: typeof github.owner?.id === "number" || typeof github.owner?.id === "string" ? String(github.owner.id) : void 0,
|
|
1340
|
+
repositoryId: typeof github.id === "number" || typeof github.id === "string" ? String(github.id) : void 0
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
async function scaffoldBootstrapManifest(input) {
|
|
1344
|
+
const slug = value(input.slug, "slug");
|
|
1345
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(slug))
|
|
1346
|
+
throw new Error("slug must use lowercase letters, digits, and hyphens");
|
|
1347
|
+
const repository = value(input.repository, "repository");
|
|
1348
|
+
repositoryParts(repository);
|
|
1349
|
+
const environment = value(input.environment, "environment");
|
|
1350
|
+
const providerArn = value(input.providerArn, "github OIDC provider ARN");
|
|
1351
|
+
if (!accountFromArn(providerArn)) throw new Error("github OIDC provider ARN is invalid");
|
|
1352
|
+
const found = await discovered(input);
|
|
1353
|
+
const accountId = input.accountId ?? found.accountId;
|
|
1354
|
+
const region2 = input.region ?? found.region;
|
|
1355
|
+
const ownerId = input.ownerId ?? found.ownerId;
|
|
1356
|
+
const repositoryId = input.repositoryId ?? found.repositoryId;
|
|
1357
|
+
if (!accountId) throw new Error("--account-id is required when AWS identity is unavailable");
|
|
1358
|
+
if (!region2) throw new Error("--region is required when AWS Region is unavailable");
|
|
1359
|
+
if (!ownerId) throw new Error("--github-owner-id is required when GitHub is unavailable");
|
|
1360
|
+
if (!repositoryId)
|
|
1361
|
+
throw new Error("--github-repository-id is required when GitHub is unavailable");
|
|
1362
|
+
return {
|
|
1363
|
+
region: region2,
|
|
1364
|
+
core: {
|
|
1365
|
+
stackName: `${slug}-bootstrap`,
|
|
1366
|
+
artifactBucketName: `${slug}-lambda-artifacts-${accountId}`,
|
|
1367
|
+
applicationStackName: slug,
|
|
1368
|
+
dashboardFunctionName: slug
|
|
1369
|
+
},
|
|
1370
|
+
githubOidc: {
|
|
1371
|
+
stackName: `${slug}-github-bootstrap`,
|
|
1372
|
+
providerArn,
|
|
1373
|
+
repository,
|
|
1374
|
+
ownerId,
|
|
1375
|
+
repositoryId,
|
|
1376
|
+
environment
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function incomplete(config) {
|
|
1381
|
+
const fields = [
|
|
1382
|
+
["region", config.region],
|
|
1383
|
+
["core.stackName", config.core?.stackName],
|
|
1384
|
+
["core.artifactBucketName", config.core?.artifactBucketName],
|
|
1385
|
+
["core.applicationStackName", config.core?.applicationStackName],
|
|
1386
|
+
["core.dashboardFunctionName", config.core?.dashboardFunctionName],
|
|
1387
|
+
["githubOidc.stackName", config.githubOidc?.stackName],
|
|
1388
|
+
["githubOidc.providerArn", config.githubOidc?.providerArn],
|
|
1389
|
+
["githubOidc.repository", config.githubOidc?.repository],
|
|
1390
|
+
["githubOidc.ownerId", config.githubOidc?.ownerId],
|
|
1391
|
+
["githubOidc.repositoryId", config.githubOidc?.repositoryId],
|
|
1392
|
+
["githubOidc.environment", config.githubOidc?.environment]
|
|
1393
|
+
];
|
|
1394
|
+
return fields.filter(([, v]) => typeof v !== "string" || !v).map(([name]) => name);
|
|
1395
|
+
}
|
|
1396
|
+
function parseOrUnavailable(raw) {
|
|
1397
|
+
if (!raw) return void 0;
|
|
1398
|
+
try {
|
|
1399
|
+
return JSON.parse(raw);
|
|
1400
|
+
} catch {
|
|
1401
|
+
return void 0;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function absent(raw) {
|
|
1405
|
+
return /not found|nosuchentity|404/i.test(raw ?? "");
|
|
1406
|
+
}
|
|
1407
|
+
async function bootstrapPreflight(input) {
|
|
1408
|
+
const checks = [];
|
|
1409
|
+
const missing = incomplete(input.config);
|
|
1410
|
+
checks.push(
|
|
1411
|
+
missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
|
|
1412
|
+
);
|
|
1413
|
+
if (missing.length) return { ready: false, checks };
|
|
1414
|
+
const runner = input.runner;
|
|
1415
|
+
const identityRaw = await optional(runner, "aws", [
|
|
1416
|
+
"sts",
|
|
1417
|
+
"get-caller-identity",
|
|
1418
|
+
"--output",
|
|
1419
|
+
"json"
|
|
1420
|
+
]);
|
|
1421
|
+
const identity = parseOrUnavailable(identityRaw);
|
|
1422
|
+
const providerAccount = accountFromArn(input.config.githubOidc?.providerArn ?? "");
|
|
1423
|
+
if (!identity || typeof identity.Account !== "string")
|
|
1424
|
+
checks.push({
|
|
1425
|
+
name: "aws-identity",
|
|
1426
|
+
status: "unverified",
|
|
1427
|
+
detail: "AWS identity could not be read."
|
|
1428
|
+
});
|
|
1429
|
+
else
|
|
1430
|
+
checks.push(
|
|
1431
|
+
identity.Account === providerAccount ? {
|
|
1432
|
+
name: "aws-identity",
|
|
1433
|
+
status: "ready",
|
|
1434
|
+
detail: `AWS account ${identity.Account} matches the provider ARN.`
|
|
1435
|
+
} : {
|
|
1436
|
+
name: "aws-identity",
|
|
1437
|
+
status: "mismatch",
|
|
1438
|
+
detail: `AWS account ${identity.Account} does not match provider account ${providerAccount}.`
|
|
1439
|
+
}
|
|
1440
|
+
);
|
|
1441
|
+
const regionRaw = await optional(runner, "aws", ["configure", "get", "region"]);
|
|
1442
|
+
if (!regionRaw?.trim())
|
|
1443
|
+
checks.push({
|
|
1444
|
+
name: "aws-region",
|
|
1445
|
+
status: "unverified",
|
|
1446
|
+
detail: "AWS Region could not be read."
|
|
1447
|
+
});
|
|
1448
|
+
else
|
|
1449
|
+
checks.push(
|
|
1450
|
+
regionRaw.trim() === input.config.region ? { name: "aws-region", status: "ready", detail: `AWS Region is ${input.config.region}.` } : {
|
|
1451
|
+
name: "aws-region",
|
|
1452
|
+
status: "mismatch",
|
|
1453
|
+
detail: `AWS Region ${regionRaw.trim()} does not match manifest ${input.config.region}.`
|
|
1454
|
+
}
|
|
1455
|
+
);
|
|
1456
|
+
const providerRaw = await optional(runner, "aws", [
|
|
1457
|
+
"iam",
|
|
1458
|
+
"get-open-id-connect-provider",
|
|
1459
|
+
"--open-id-connect-provider-arn",
|
|
1460
|
+
input.config.githubOidc?.providerArn ?? ""
|
|
1461
|
+
]);
|
|
1462
|
+
checks.push(
|
|
1463
|
+
!providerRaw ? {
|
|
1464
|
+
name: "oidc-provider",
|
|
1465
|
+
status: "unverified",
|
|
1466
|
+
detail: "OIDC provider could not be queried."
|
|
1467
|
+
} : absent(providerRaw) ? { name: "oidc-provider", status: "missing", detail: "OIDC provider does not exist." } : { name: "oidc-provider", status: "ready", detail: "OIDC provider exists." }
|
|
1468
|
+
);
|
|
1469
|
+
const repository = input.config.githubOidc?.repository ?? "";
|
|
1470
|
+
const repoRaw = await optional(runner, "gh", ["api", `repos/${repository}`]);
|
|
1471
|
+
const repo = parseOrUnavailable(repoRaw);
|
|
1472
|
+
if (!repoRaw)
|
|
1473
|
+
checks.push({
|
|
1474
|
+
name: "github-repository",
|
|
1475
|
+
status: "unverified",
|
|
1476
|
+
detail: "GitHub repository could not be queried."
|
|
1477
|
+
});
|
|
1478
|
+
else if (absent(repoRaw))
|
|
1479
|
+
checks.push({
|
|
1480
|
+
name: "github-repository",
|
|
1481
|
+
status: "missing",
|
|
1482
|
+
detail: "GitHub repository does not exist or is unavailable."
|
|
1483
|
+
});
|
|
1484
|
+
else if (String(repo?.id) !== input.config.githubOidc?.repositoryId || String(repo?.owner?.id) !== input.config.githubOidc?.ownerId)
|
|
1485
|
+
checks.push({
|
|
1486
|
+
name: "github-repository",
|
|
1487
|
+
status: "mismatch",
|
|
1488
|
+
detail: "GitHub numeric repository identity does not match the manifest."
|
|
1489
|
+
});
|
|
1490
|
+
else
|
|
1491
|
+
checks.push({
|
|
1492
|
+
name: "github-repository",
|
|
1493
|
+
status: "ready",
|
|
1494
|
+
detail: "GitHub repository identity matches the manifest."
|
|
1495
|
+
});
|
|
1496
|
+
const environmentRaw = await optional(runner, "gh", [
|
|
1497
|
+
"api",
|
|
1498
|
+
`repos/${repository}/environments/${input.config.githubOidc?.environment}`
|
|
1499
|
+
]);
|
|
1500
|
+
checks.push(
|
|
1501
|
+
!environmentRaw ? {
|
|
1502
|
+
name: "github-environment",
|
|
1503
|
+
status: "unverified",
|
|
1504
|
+
detail: "GitHub Environment could not be queried."
|
|
1505
|
+
} : absent(environmentRaw) ? {
|
|
1506
|
+
name: "github-environment",
|
|
1507
|
+
status: "missing",
|
|
1508
|
+
detail: "GitHub Environment does not exist."
|
|
1509
|
+
} : {
|
|
1510
|
+
name: "github-environment",
|
|
1511
|
+
status: "ready",
|
|
1512
|
+
detail: "GitHub Environment exists; its policy is administrator-owned context."
|
|
1513
|
+
}
|
|
1514
|
+
);
|
|
1515
|
+
const subject = await (input.provider ?? githubOidcProvider).prerequisite(input.config, runner);
|
|
1516
|
+
checks.push({
|
|
1517
|
+
name: "immutable-subject",
|
|
1518
|
+
status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
|
|
1519
|
+
detail: subject.detail
|
|
1520
|
+
});
|
|
1521
|
+
return {
|
|
1522
|
+
ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
|
|
1523
|
+
checks
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function quote(args2) {
|
|
1527
|
+
return args2.map((arg) => `'${arg.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
|
|
1528
|
+
}
|
|
1529
|
+
async function bootstrapGuide(input) {
|
|
1530
|
+
const handoff = await bootstrapHandoff(input);
|
|
1531
|
+
const lines = [
|
|
1532
|
+
`Phase: ${handoff.phase}`,
|
|
1533
|
+
`Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
|
|
1534
|
+
];
|
|
1535
|
+
if (handoff.expectedOutputs.length)
|
|
1536
|
+
lines.push(`Expected outputs: ${handoff.expectedOutputs.join(", ")}`);
|
|
1537
|
+
if (handoff.prerequisite)
|
|
1538
|
+
lines.push(`Prerequisite: ${handoff.prerequisite.status} \u2014 ${handoff.prerequisite.detail}`);
|
|
1539
|
+
lines.push("", "Commands (copy and run each AWS command yourself):");
|
|
1540
|
+
for (const command of handoff.commands) {
|
|
1541
|
+
if (command.name === "review-change-set")
|
|
1542
|
+
lines.push("PAUSE: review the change set before approval.");
|
|
1543
|
+
if (command.name === "execute-reviewed-change-set")
|
|
1544
|
+
lines.push("PAUSE: execute only after explicit approval.");
|
|
1545
|
+
lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
|
|
1546
|
+
}
|
|
1547
|
+
if (handoff.reviewCheckpoints.length)
|
|
1548
|
+
lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
|
|
1549
|
+
if (input.coreStack !== void 0 && input.githubOidcStack !== void 0) {
|
|
1550
|
+
const verified = await verifyBootstrap({
|
|
1551
|
+
config: input.config,
|
|
1552
|
+
coreStack: input.coreStack,
|
|
1553
|
+
githubOidcStack: input.githubOidcStack
|
|
1554
|
+
});
|
|
1555
|
+
lines.push(
|
|
1556
|
+
"",
|
|
1557
|
+
"Optional GitHub administrator action (after the successful verification above):"
|
|
1558
|
+
);
|
|
1559
|
+
for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
|
|
1560
|
+
}
|
|
1561
|
+
return `${lines.join("\n")}
|
|
1562
|
+
`;
|
|
915
1563
|
}
|
|
916
1564
|
|
|
917
1565
|
// packages/aws/src/index.ts
|
|
@@ -1065,9 +1713,9 @@ var actualDependencies = {
|
|
|
1065
1713
|
nodeVersion: process.versions.node,
|
|
1066
1714
|
packageVersion: ""
|
|
1067
1715
|
};
|
|
1068
|
-
function readParameterValues(
|
|
1069
|
-
if (!Array.isArray(
|
|
1070
|
-
const parameters =
|
|
1716
|
+
function readParameterValues(value2) {
|
|
1717
|
+
if (!Array.isArray(value2)) throw new Error("must contain a JSON parameter array");
|
|
1718
|
+
const parameters = value2.map((entry) => {
|
|
1071
1719
|
if (!entry || typeof entry !== "object" || typeof entry.ParameterKey !== "string" || typeof entry.ParameterValue !== "string")
|
|
1072
1720
|
throw new Error("entries must contain string ParameterKey and ParameterValue fields");
|
|
1073
1721
|
return entry;
|
|
@@ -1200,23 +1848,23 @@ async function installedPackageVersion() {
|
|
|
1200
1848
|
return typeof packageManifest.version === "string" ? packageManifest.version : "";
|
|
1201
1849
|
}
|
|
1202
1850
|
var requiredOption = (name, fallback) => {
|
|
1203
|
-
const
|
|
1204
|
-
if (!
|
|
1205
|
-
return
|
|
1851
|
+
const value2 = option(name, fallback);
|
|
1852
|
+
if (!value2) throw new Error(`${name} is required`);
|
|
1853
|
+
return value2;
|
|
1206
1854
|
};
|
|
1207
|
-
function parameter(key,
|
|
1208
|
-
return { ParameterKey: key, ParameterValue:
|
|
1855
|
+
function parameter(key, value2) {
|
|
1856
|
+
return { ParameterKey: key, ParameterValue: value2 };
|
|
1209
1857
|
}
|
|
1210
1858
|
async function existingParameters(path) {
|
|
1211
1859
|
try {
|
|
1212
1860
|
const parsed = JSON.parse(await readFile5(path, "utf8"));
|
|
1213
1861
|
if (!Array.isArray(parsed)) throw new Error(`${path} must contain a JSON parameter array`);
|
|
1214
|
-
const values = parsed.map((
|
|
1215
|
-
if (!
|
|
1862
|
+
const values = parsed.map((value2) => {
|
|
1863
|
+
if (!value2 || typeof value2 !== "object" || typeof value2.ParameterKey !== "string" || typeof value2.ParameterValue !== "string")
|
|
1216
1864
|
throw new Error(
|
|
1217
1865
|
`${path} entries must contain string ParameterKey and ParameterValue fields`
|
|
1218
1866
|
);
|
|
1219
|
-
return
|
|
1867
|
+
return value2;
|
|
1220
1868
|
});
|
|
1221
1869
|
const keys = values.map(({ ParameterKey }) => ParameterKey);
|
|
1222
1870
|
if (new Set(keys).size !== keys.length) throw new Error(`${path} contains duplicate parameters`);
|
|
@@ -1318,9 +1966,9 @@ try {
|
|
|
1318
1966
|
console.log(JSON.stringify({ published: true, version, assetPath }));
|
|
1319
1967
|
} else if (args[0] === "doctor") {
|
|
1320
1968
|
const parametersPath = option("--parameters", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
|
|
1321
|
-
const
|
|
1969
|
+
const region2 = option("--region", process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1") ?? "us-east-1";
|
|
1322
1970
|
const checks = await runDoctor(
|
|
1323
|
-
{ parametersPath, region },
|
|
1971
|
+
{ parametersPath, region: region2 },
|
|
1324
1972
|
{
|
|
1325
1973
|
async execute(command, commandArgs) {
|
|
1326
1974
|
const { execFile: execFile3 } = await import("node:child_process");
|
|
@@ -1371,109 +2019,225 @@ try {
|
|
|
1371
2019
|
(key) => !packageManaged.has(key) && (includeDefaults || !hasDefault(key) || Object.hasOwn(existingValues, key) || key === "Name" && Boolean(functionName))
|
|
1372
2020
|
).map((key) => {
|
|
1373
2021
|
const definition = definitions[key];
|
|
1374
|
-
const
|
|
1375
|
-
if (
|
|
1376
|
-
return parameter(key, String(
|
|
2022
|
+
const value2 = key === "LambdaArtifactBucket" ? artifactBucket : key === "Name" && functionName ? functionName : existingValues[key] ?? definition?.Default;
|
|
2023
|
+
if (value2 === void 0) throw new Error(`No value for CloudFormation parameter ${key}`);
|
|
2024
|
+
return parameter(key, String(value2));
|
|
1377
2025
|
});
|
|
1378
2026
|
await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
|
|
1379
2027
|
`);
|
|
1380
2028
|
console.log(JSON.stringify({ output }));
|
|
1381
2029
|
} else if (args[0] === "bootstrap") {
|
|
1382
2030
|
const action = args[1];
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
2031
|
+
if (action === "init") {
|
|
2032
|
+
const output = requiredOption("--output");
|
|
2033
|
+
try {
|
|
2034
|
+
await readFile5(output, "utf8");
|
|
2035
|
+
throw new Error(`Refusing to overwrite existing manifest: ${output}`);
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
2038
|
+
throw error;
|
|
2039
|
+
}
|
|
2040
|
+
const runner = {
|
|
2041
|
+
async execute(command, commandArgs) {
|
|
2042
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
2043
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
2044
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
const manifest = await scaffoldBootstrapManifest({
|
|
2048
|
+
slug: requiredOption("--slug"),
|
|
2049
|
+
repository: requiredOption("--repository"),
|
|
2050
|
+
environment: requiredOption("--environment"),
|
|
2051
|
+
providerArn: requiredOption("--github-oidc-provider-arn"),
|
|
2052
|
+
region: option("--region"),
|
|
2053
|
+
accountId: option("--account-id"),
|
|
2054
|
+
ownerId: option("--github-owner-id"),
|
|
2055
|
+
repositoryId: option("--github-repository-id"),
|
|
2056
|
+
runner
|
|
2057
|
+
});
|
|
2058
|
+
await writeFile3(output, `${JSON.stringify(manifest, null, 2)}
|
|
2059
|
+
`, { flag: "wx" });
|
|
2060
|
+
console.log(JSON.stringify({ output, manifest }));
|
|
2061
|
+
} else {
|
|
2062
|
+
const config = await bootstrapConfig();
|
|
2063
|
+
if (action === "preflight") {
|
|
2064
|
+
requiredOption("--config");
|
|
2065
|
+
const runner = {
|
|
2066
|
+
async execute(command, commandArgs) {
|
|
2067
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
2068
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
2069
|
+
try {
|
|
2070
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
if (error && typeof error === "object" && "stderr" in error) {
|
|
2073
|
+
const stderr = error.stderr;
|
|
2074
|
+
if (typeof stderr === "string" && /not found|404|nosuchentity/i.test(stderr))
|
|
2075
|
+
return stderr;
|
|
2076
|
+
}
|
|
2077
|
+
throw error;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
const result = await bootstrapPreflight({ config, runner });
|
|
2082
|
+
if (option("--format") === "text")
|
|
2083
|
+
for (const check of result.checks)
|
|
2084
|
+
console.log(`${check.status.toUpperCase()} ${check.name}: ${check.detail}`);
|
|
2085
|
+
else console.log(JSON.stringify(result));
|
|
2086
|
+
if (!result.ready) process.exitCode = 1;
|
|
2087
|
+
} else if (action === "guide") {
|
|
2088
|
+
const configPath = requiredOption("--config");
|
|
2089
|
+
const coreStackPath = option("--core-stack-json");
|
|
2090
|
+
const githubStackPath = option("--github-oidc-stack-json");
|
|
2091
|
+
const runner = {
|
|
2092
|
+
async execute(command, commandArgs) {
|
|
2093
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
2094
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
2095
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
process.stdout.write(
|
|
2099
|
+
await bootstrapGuide({
|
|
2100
|
+
config,
|
|
2101
|
+
configPath,
|
|
2102
|
+
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
2103
|
+
coreStackPath,
|
|
2104
|
+
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
2105
|
+
runner
|
|
2106
|
+
})
|
|
2107
|
+
);
|
|
2108
|
+
} else if (action === "handoff") {
|
|
2109
|
+
const configPath = requiredOption("--config");
|
|
2110
|
+
const coreStackPath = option("--core-stack-json");
|
|
2111
|
+
const githubStackPath = option("--github-oidc-stack-json");
|
|
2112
|
+
const runner = {
|
|
2113
|
+
async execute(command, commandArgs) {
|
|
2114
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
2115
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
2116
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
console.log(
|
|
2120
|
+
JSON.stringify(
|
|
2121
|
+
await bootstrapHandoff({
|
|
2122
|
+
config,
|
|
2123
|
+
configPath,
|
|
2124
|
+
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
2125
|
+
coreStackPath,
|
|
2126
|
+
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
2127
|
+
runner
|
|
2128
|
+
})
|
|
1402
2129
|
)
|
|
1403
2130
|
);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
2131
|
+
} else if (action === "verify") {
|
|
2132
|
+
requiredOption("--config");
|
|
2133
|
+
const coreStackPath = requiredOption("--core-stack-json");
|
|
2134
|
+
const githubStackPath = requiredOption("--github-oidc-stack-json");
|
|
2135
|
+
console.log(
|
|
2136
|
+
JSON.stringify(
|
|
2137
|
+
await verifyBootstrap({
|
|
2138
|
+
config,
|
|
2139
|
+
coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
|
|
2140
|
+
githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
|
|
2141
|
+
})
|
|
2142
|
+
)
|
|
1412
2143
|
);
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
2144
|
+
} else if (action === "template") {
|
|
2145
|
+
const kind = bootstrapKind();
|
|
2146
|
+
console.log(
|
|
2147
|
+
JSON.stringify({
|
|
2148
|
+
kind,
|
|
2149
|
+
template: await bootstrapTemplatePath(kind),
|
|
2150
|
+
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
|
|
2151
|
+
})
|
|
2152
|
+
);
|
|
2153
|
+
} else if (action === "parameters") {
|
|
2154
|
+
const kind = bootstrapKind();
|
|
2155
|
+
const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
|
|
2156
|
+
let coreOutputs = {};
|
|
2157
|
+
const coreStackPath = option("--core-stack-json");
|
|
2158
|
+
if (kind === "github-oidc" && coreStackPath) {
|
|
2159
|
+
coreOutputs = coreBootstrapOutputs(
|
|
2160
|
+
deployedBootstrapStack(
|
|
2161
|
+
JSON.parse(await readFile5(coreStackPath, "utf8")),
|
|
2162
|
+
bootstrapContractVersion(await bootstrapTemplate("core"))
|
|
2163
|
+
)
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
const supplied = requiredBootstrapParameters(kind, bootstrapValues(config, coreOutputs));
|
|
2167
|
+
let parameters = supplied;
|
|
2168
|
+
const deployedStackPath = option("--deployed-stack-json");
|
|
2169
|
+
if (deployedStackPath) {
|
|
2170
|
+
const stack = deployedBootstrapStack(
|
|
2171
|
+
JSON.parse(await readFile5(deployedStackPath, "utf8")),
|
|
2172
|
+
bootstrapContractVersion(await bootstrapTemplate(kind))
|
|
2173
|
+
);
|
|
2174
|
+
parameters = mergeBootstrapParameters(supplied, stack.Parameters ?? []);
|
|
2175
|
+
}
|
|
2176
|
+
await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
|
|
1416
2177
|
`);
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
2178
|
+
console.log(
|
|
2179
|
+
JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
|
|
2180
|
+
);
|
|
2181
|
+
} else if (action === "status") {
|
|
2182
|
+
const kind = bootstrapKind();
|
|
2183
|
+
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
2184
|
+
const region2 = option("--region", config.region);
|
|
2185
|
+
const awsCommand = [
|
|
2186
|
+
"aws",
|
|
2187
|
+
"cloudformation",
|
|
2188
|
+
"describe-stacks",
|
|
2189
|
+
"--stack-name",
|
|
2190
|
+
stackName2,
|
|
2191
|
+
...region2 ? ["--region", region2] : [],
|
|
2192
|
+
"--no-cli-pager"
|
|
2193
|
+
];
|
|
2194
|
+
console.log(
|
|
2195
|
+
JSON.stringify({
|
|
2196
|
+
kind,
|
|
2197
|
+
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind)),
|
|
2198
|
+
awsCommand,
|
|
2199
|
+
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
2200
|
+
})
|
|
2201
|
+
);
|
|
2202
|
+
} else if (action === "change-set") {
|
|
2203
|
+
const kind = bootstrapKind();
|
|
2204
|
+
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
2205
|
+
const changeSetName = requiredOption("--change-set-name");
|
|
2206
|
+
const parametersPath = requiredOption("--parameters");
|
|
2207
|
+
const region2 = option("--region", config.region);
|
|
2208
|
+
await existingParameters(parametersPath);
|
|
2209
|
+
const awsCommand = [
|
|
2210
|
+
"aws",
|
|
2211
|
+
"cloudformation",
|
|
2212
|
+
"create-change-set",
|
|
2213
|
+
"--stack-name",
|
|
2214
|
+
stackName2,
|
|
2215
|
+
"--change-set-name",
|
|
2216
|
+
changeSetName,
|
|
2217
|
+
"--change-set-type",
|
|
2218
|
+
option("--change-set-type", "UPDATE") ?? "UPDATE",
|
|
2219
|
+
"--template-body",
|
|
2220
|
+
`file://${await bootstrapTemplatePath(kind)}`,
|
|
2221
|
+
"--parameters",
|
|
2222
|
+
`file://${parametersPath}`,
|
|
2223
|
+
"--capabilities",
|
|
2224
|
+
"CAPABILITY_NAMED_IAM",
|
|
2225
|
+
...region2 ? ["--region", region2] : [],
|
|
2226
|
+
"--no-cli-pager"
|
|
2227
|
+
];
|
|
2228
|
+
console.log(
|
|
2229
|
+
JSON.stringify({
|
|
2230
|
+
kind,
|
|
2231
|
+
reviewRequired: true,
|
|
2232
|
+
awsCommand,
|
|
2233
|
+
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
2234
|
+
})
|
|
2235
|
+
);
|
|
2236
|
+
} else {
|
|
2237
|
+
throw new Error(
|
|
2238
|
+
"Usage: ze-great-dashboard-aws bootstrap init|preflight|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
1477
2241
|
}
|
|
1478
2242
|
} else if (args[0] !== "package")
|
|
1479
2243
|
throw new Error(
|