@continuous-excellence/ze-great-dashboard-aws 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +487 -139
- package/dist/guided.d.ts +32 -0
- package/dist/handoff.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +284 -10
- 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
|
|
@@ -910,8 +910,8 @@ 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
915
|
}
|
|
916
916
|
|
|
917
917
|
// packages/aws/src/handoff.ts
|
|
@@ -945,9 +945,9 @@ function capturedStackIdentity(input) {
|
|
|
945
945
|
...typeof StackId === "string" ? { StackId } : {}
|
|
946
946
|
};
|
|
947
947
|
}
|
|
948
|
-
function required(
|
|
949
|
-
if (!
|
|
950
|
-
return
|
|
948
|
+
function required(value2, name) {
|
|
949
|
+
if (!value2) throw new Error(`Bootstrap manifest is missing ${name}`);
|
|
950
|
+
return value2;
|
|
951
951
|
}
|
|
952
952
|
function region(config) {
|
|
953
953
|
return required(config.region, "region");
|
|
@@ -1100,6 +1100,12 @@ async function bootstrapHandoff(input) {
|
|
|
1100
1100
|
expectedContracts,
|
|
1101
1101
|
templatePath,
|
|
1102
1102
|
parameterPath: "core-bootstrap.json",
|
|
1103
|
+
expectedOutputs: [
|
|
1104
|
+
"BootstrapContractVersion",
|
|
1105
|
+
"ArtifactBucketName",
|
|
1106
|
+
"ApplicationStackName",
|
|
1107
|
+
"CloudFormationExecutionRoleArn"
|
|
1108
|
+
],
|
|
1103
1109
|
requiredCapturedFiles: [],
|
|
1104
1110
|
reviewCheckpoints: [
|
|
1105
1111
|
"Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
|
|
@@ -1130,6 +1136,7 @@ async function bootstrapHandoff(input) {
|
|
|
1130
1136
|
expectedContracts,
|
|
1131
1137
|
templatePath,
|
|
1132
1138
|
parameterPath: "github-oidc-bootstrap.json",
|
|
1139
|
+
expectedOutputs: ["BootstrapContractVersion", "GitHubDeployRoleArn"],
|
|
1133
1140
|
requiredCapturedFiles: [coreCapturePath],
|
|
1134
1141
|
reviewCheckpoints: [
|
|
1135
1142
|
"Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
|
|
@@ -1157,6 +1164,7 @@ async function bootstrapHandoff(input) {
|
|
|
1157
1164
|
return {
|
|
1158
1165
|
phase: "github-environment",
|
|
1159
1166
|
expectedContracts,
|
|
1167
|
+
expectedOutputs: [],
|
|
1160
1168
|
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1161
1169
|
reviewCheckpoints: [
|
|
1162
1170
|
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
@@ -1167,6 +1175,7 @@ async function bootstrapHandoff(input) {
|
|
|
1167
1175
|
return {
|
|
1168
1176
|
phase: "application-gateway",
|
|
1169
1177
|
expectedContracts,
|
|
1178
|
+
expectedOutputs: [],
|
|
1170
1179
|
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1171
1180
|
reviewCheckpoints: [
|
|
1172
1181
|
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
@@ -1227,7 +1236,7 @@ async function verifyBootstrap(input) {
|
|
|
1227
1236
|
coreOutputs.CloudFormationExecutionRoleArn,
|
|
1228
1237
|
"core CloudFormationExecutionRoleArn output"
|
|
1229
1238
|
);
|
|
1230
|
-
for (const [key,
|
|
1239
|
+
for (const [key, value2] of Object.entries({
|
|
1231
1240
|
GitHubOidcProviderArn: github?.providerArn,
|
|
1232
1241
|
GitHubRepository: github?.repository,
|
|
1233
1242
|
GitHubOwnerId: github?.ownerId,
|
|
@@ -1237,7 +1246,7 @@ async function verifyBootstrap(input) {
|
|
|
1237
1246
|
ArtifactBucketName: coreOutputs.ArtifactBucketName,
|
|
1238
1247
|
CloudFormationExecutionRoleArn: executionRole
|
|
1239
1248
|
}))
|
|
1240
|
-
assertEqual(oidcParameters[key],
|
|
1249
|
+
assertEqual(oidcParameters[key], value2, `OIDC ${key}`);
|
|
1241
1250
|
if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
|
|
1242
1251
|
throw new Error("GitHub immutable owner and repository IDs must be numeric");
|
|
1243
1252
|
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
@@ -1291,6 +1300,268 @@ async function verifyBootstrap(input) {
|
|
|
1291
1300
|
};
|
|
1292
1301
|
}
|
|
1293
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
|
+
`;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1294
1565
|
// packages/aws/src/index.ts
|
|
1295
1566
|
var run = promisify(execFile);
|
|
1296
1567
|
function deploymentTemplate(template, values) {
|
|
@@ -1442,9 +1713,9 @@ var actualDependencies = {
|
|
|
1442
1713
|
nodeVersion: process.versions.node,
|
|
1443
1714
|
packageVersion: ""
|
|
1444
1715
|
};
|
|
1445
|
-
function readParameterValues(
|
|
1446
|
-
if (!Array.isArray(
|
|
1447
|
-
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) => {
|
|
1448
1719
|
if (!entry || typeof entry !== "object" || typeof entry.ParameterKey !== "string" || typeof entry.ParameterValue !== "string")
|
|
1449
1720
|
throw new Error("entries must contain string ParameterKey and ParameterValue fields");
|
|
1450
1721
|
return entry;
|
|
@@ -1577,23 +1848,23 @@ async function installedPackageVersion() {
|
|
|
1577
1848
|
return typeof packageManifest.version === "string" ? packageManifest.version : "";
|
|
1578
1849
|
}
|
|
1579
1850
|
var requiredOption = (name, fallback) => {
|
|
1580
|
-
const
|
|
1581
|
-
if (!
|
|
1582
|
-
return
|
|
1851
|
+
const value2 = option(name, fallback);
|
|
1852
|
+
if (!value2) throw new Error(`${name} is required`);
|
|
1853
|
+
return value2;
|
|
1583
1854
|
};
|
|
1584
|
-
function parameter(key,
|
|
1585
|
-
return { ParameterKey: key, ParameterValue:
|
|
1855
|
+
function parameter(key, value2) {
|
|
1856
|
+
return { ParameterKey: key, ParameterValue: value2 };
|
|
1586
1857
|
}
|
|
1587
1858
|
async function existingParameters(path) {
|
|
1588
1859
|
try {
|
|
1589
1860
|
const parsed = JSON.parse(await readFile5(path, "utf8"));
|
|
1590
1861
|
if (!Array.isArray(parsed)) throw new Error(`${path} must contain a JSON parameter array`);
|
|
1591
|
-
const values = parsed.map((
|
|
1592
|
-
if (!
|
|
1862
|
+
const values = parsed.map((value2) => {
|
|
1863
|
+
if (!value2 || typeof value2 !== "object" || typeof value2.ParameterKey !== "string" || typeof value2.ParameterValue !== "string")
|
|
1593
1864
|
throw new Error(
|
|
1594
1865
|
`${path} entries must contain string ParameterKey and ParameterValue fields`
|
|
1595
1866
|
);
|
|
1596
|
-
return
|
|
1867
|
+
return value2;
|
|
1597
1868
|
});
|
|
1598
1869
|
const keys = values.map(({ ParameterKey }) => ParameterKey);
|
|
1599
1870
|
if (new Set(keys).size !== keys.length) throw new Error(`${path} contains duplicate parameters`);
|
|
@@ -1748,20 +2019,24 @@ try {
|
|
|
1748
2019
|
(key) => !packageManaged.has(key) && (includeDefaults || !hasDefault(key) || Object.hasOwn(existingValues, key) || key === "Name" && Boolean(functionName))
|
|
1749
2020
|
).map((key) => {
|
|
1750
2021
|
const definition = definitions[key];
|
|
1751
|
-
const
|
|
1752
|
-
if (
|
|
1753
|
-
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));
|
|
1754
2025
|
});
|
|
1755
2026
|
await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
|
|
1756
2027
|
`);
|
|
1757
2028
|
console.log(JSON.stringify({ output }));
|
|
1758
2029
|
} else if (args[0] === "bootstrap") {
|
|
1759
2030
|
const action = args[1];
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
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
|
+
}
|
|
1765
2040
|
const runner = {
|
|
1766
2041
|
async execute(command, commandArgs) {
|
|
1767
2042
|
const { execFile: execFile3 } = await import("node:child_process");
|
|
@@ -1769,9 +2044,59 @@ try {
|
|
|
1769
2044
|
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
1770
2045
|
}
|
|
1771
2046
|
};
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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({
|
|
1775
2100
|
config,
|
|
1776
2101
|
configPath,
|
|
1777
2102
|
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
@@ -1779,117 +2104,140 @@ try {
|
|
|
1779
2104
|
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
1780
2105
|
runner
|
|
1781
2106
|
})
|
|
1782
|
-
)
|
|
1783
|
-
)
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
})
|
|
1805
|
-
);
|
|
1806
|
-
} else if (action === "parameters") {
|
|
1807
|
-
const kind = bootstrapKind();
|
|
1808
|
-
const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
|
|
1809
|
-
let coreOutputs = {};
|
|
1810
|
-
const coreStackPath = option("--core-stack-json");
|
|
1811
|
-
if (kind === "github-oidc" && coreStackPath) {
|
|
1812
|
-
coreOutputs = coreBootstrapOutputs(
|
|
1813
|
-
deployedBootstrapStack(
|
|
1814
|
-
JSON.parse(await readFile5(coreStackPath, "utf8")),
|
|
1815
|
-
bootstrapContractVersion(await bootstrapTemplate("core"))
|
|
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
|
+
})
|
|
1816
2129
|
)
|
|
1817
2130
|
);
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
+
)
|
|
1826
2143
|
);
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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)}
|
|
1830
2177
|
`);
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
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
|
+
}
|
|
1893
2241
|
}
|
|
1894
2242
|
} else if (args[0] !== "package")
|
|
1895
2243
|
throw new Error(
|
package/dist/guided.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BootstrapConfig } from './bootstrap.js';
|
|
2
|
+
import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
|
|
3
|
+
export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
|
|
4
|
+
export type BootstrapPreflightCheck = {
|
|
5
|
+
name: string;
|
|
6
|
+
status: BootstrapCheckStatus;
|
|
7
|
+
detail: string;
|
|
8
|
+
};
|
|
9
|
+
export type BootstrapPreflight = {
|
|
10
|
+
ready: boolean;
|
|
11
|
+
checks: BootstrapPreflightCheck[];
|
|
12
|
+
};
|
|
13
|
+
export type BootstrapInitInput = {
|
|
14
|
+
slug: string;
|
|
15
|
+
repository: string;
|
|
16
|
+
environment: string;
|
|
17
|
+
providerArn: string;
|
|
18
|
+
region?: string;
|
|
19
|
+
accountId?: string;
|
|
20
|
+
ownerId?: string;
|
|
21
|
+
repositoryId?: string;
|
|
22
|
+
runner?: CommandRunner;
|
|
23
|
+
};
|
|
24
|
+
/** Creates the non-secret manifest data; callers control where (and whether) it is written. */
|
|
25
|
+
export declare function scaffoldBootstrapManifest(input: BootstrapInitInput): Promise<BootstrapConfig>;
|
|
26
|
+
/** Runs only named read-only discovery checks. Network/auth failures remain usable offline. */
|
|
27
|
+
export declare function bootstrapPreflight(input: {
|
|
28
|
+
config: BootstrapConfig;
|
|
29
|
+
runner?: CommandRunner;
|
|
30
|
+
provider?: BootstrapProvider;
|
|
31
|
+
}): Promise<BootstrapPreflight>;
|
|
32
|
+
export declare function bootstrapGuide(input: Parameters<typeof bootstrapHandoff>[0]): Promise<string>;
|
package/dist/handoff.d.ts
CHANGED
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 BootstrapCheckStatus, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
|
|
2
3
|
export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
|
|
3
4
|
export type ReleaseMetadata = {
|
|
4
5
|
dashboardVersion: string;
|
package/dist/index.js
CHANGED
|
@@ -730,7 +730,7 @@ import { z as z2 } from "zod";
|
|
|
730
730
|
|
|
731
731
|
// packages/aws/src/internal-board.ts
|
|
732
732
|
import { z } from "zod";
|
|
733
|
-
var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((
|
|
733
|
+
var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value2) => Number.parseInt(value2, 10) > 0).brand();
|
|
734
734
|
var positionSchema = z.object({
|
|
735
735
|
x: z.number().int().min(0),
|
|
736
736
|
y: z.number().int().min(0),
|
|
@@ -821,8 +821,8 @@ async function assembleRelease(input) {
|
|
|
821
821
|
);
|
|
822
822
|
return { metadata, files };
|
|
823
823
|
}
|
|
824
|
-
function sha256(
|
|
825
|
-
return createHash("sha256").update(
|
|
824
|
+
function sha256(value2) {
|
|
825
|
+
return createHash("sha256").update(value2).digest("hex");
|
|
826
826
|
}
|
|
827
827
|
|
|
828
828
|
// packages/aws/src/bootstrap.ts
|
|
@@ -897,8 +897,8 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
897
897
|
];
|
|
898
898
|
const missing = keys.filter((key) => !values[key]);
|
|
899
899
|
if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
|
|
900
|
-
const
|
|
901
|
-
return [...keys, ...
|
|
900
|
+
const optional2 = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
|
|
901
|
+
return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
902
902
|
}
|
|
903
903
|
|
|
904
904
|
// packages/aws/src/handoff.ts
|
|
@@ -932,9 +932,9 @@ function capturedStackIdentity(input) {
|
|
|
932
932
|
...typeof StackId === "string" ? { StackId } : {}
|
|
933
933
|
};
|
|
934
934
|
}
|
|
935
|
-
function required(
|
|
936
|
-
if (!
|
|
937
|
-
return
|
|
935
|
+
function required(value2, name) {
|
|
936
|
+
if (!value2) throw new Error(`Bootstrap manifest is missing ${name}`);
|
|
937
|
+
return value2;
|
|
938
938
|
}
|
|
939
939
|
function region(config) {
|
|
940
940
|
return required(config.region, "region");
|
|
@@ -1087,6 +1087,12 @@ async function bootstrapHandoff(input) {
|
|
|
1087
1087
|
expectedContracts,
|
|
1088
1088
|
templatePath,
|
|
1089
1089
|
parameterPath: "core-bootstrap.json",
|
|
1090
|
+
expectedOutputs: [
|
|
1091
|
+
"BootstrapContractVersion",
|
|
1092
|
+
"ArtifactBucketName",
|
|
1093
|
+
"ApplicationStackName",
|
|
1094
|
+
"CloudFormationExecutionRoleArn"
|
|
1095
|
+
],
|
|
1090
1096
|
requiredCapturedFiles: [],
|
|
1091
1097
|
reviewCheckpoints: [
|
|
1092
1098
|
"Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
|
|
@@ -1117,6 +1123,7 @@ async function bootstrapHandoff(input) {
|
|
|
1117
1123
|
expectedContracts,
|
|
1118
1124
|
templatePath,
|
|
1119
1125
|
parameterPath: "github-oidc-bootstrap.json",
|
|
1126
|
+
expectedOutputs: ["BootstrapContractVersion", "GitHubDeployRoleArn"],
|
|
1120
1127
|
requiredCapturedFiles: [coreCapturePath],
|
|
1121
1128
|
reviewCheckpoints: [
|
|
1122
1129
|
"Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
|
|
@@ -1144,6 +1151,7 @@ async function bootstrapHandoff(input) {
|
|
|
1144
1151
|
return {
|
|
1145
1152
|
phase: "github-environment",
|
|
1146
1153
|
expectedContracts,
|
|
1154
|
+
expectedOutputs: [],
|
|
1147
1155
|
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1148
1156
|
reviewCheckpoints: [
|
|
1149
1157
|
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
@@ -1154,6 +1162,7 @@ async function bootstrapHandoff(input) {
|
|
|
1154
1162
|
return {
|
|
1155
1163
|
phase: "application-gateway",
|
|
1156
1164
|
expectedContracts,
|
|
1165
|
+
expectedOutputs: [],
|
|
1157
1166
|
requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
|
|
1158
1167
|
reviewCheckpoints: [
|
|
1159
1168
|
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
@@ -1214,7 +1223,7 @@ async function verifyBootstrap(input) {
|
|
|
1214
1223
|
coreOutputs.CloudFormationExecutionRoleArn,
|
|
1215
1224
|
"core CloudFormationExecutionRoleArn output"
|
|
1216
1225
|
);
|
|
1217
|
-
for (const [key,
|
|
1226
|
+
for (const [key, value2] of Object.entries({
|
|
1218
1227
|
GitHubOidcProviderArn: github?.providerArn,
|
|
1219
1228
|
GitHubRepository: github?.repository,
|
|
1220
1229
|
GitHubOwnerId: github?.ownerId,
|
|
@@ -1224,7 +1233,7 @@ async function verifyBootstrap(input) {
|
|
|
1224
1233
|
ArtifactBucketName: coreOutputs.ArtifactBucketName,
|
|
1225
1234
|
CloudFormationExecutionRoleArn: executionRole
|
|
1226
1235
|
}))
|
|
1227
|
-
assertEqual(oidcParameters[key],
|
|
1236
|
+
assertEqual(oidcParameters[key], value2, `OIDC ${key}`);
|
|
1228
1237
|
if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
|
|
1229
1238
|
throw new Error("GitHub immutable owner and repository IDs must be numeric");
|
|
1230
1239
|
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
@@ -1278,6 +1287,268 @@ async function verifyBootstrap(input) {
|
|
|
1278
1287
|
};
|
|
1279
1288
|
}
|
|
1280
1289
|
|
|
1290
|
+
// packages/aws/src/guided.ts
|
|
1291
|
+
function value(input, label) {
|
|
1292
|
+
if (typeof input !== "string" || !input.trim()) throw new Error(`${label} is required`);
|
|
1293
|
+
return input.trim();
|
|
1294
|
+
}
|
|
1295
|
+
function repositoryParts(repository) {
|
|
1296
|
+
const [owner, name, extra] = repository.split("/");
|
|
1297
|
+
if (!owner || !name || extra) throw new Error("repository must be owner/repository");
|
|
1298
|
+
return [owner, name];
|
|
1299
|
+
}
|
|
1300
|
+
function accountFromArn(arn) {
|
|
1301
|
+
return arn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
1302
|
+
}
|
|
1303
|
+
async function optional(runner, command, args) {
|
|
1304
|
+
if (!runner) return void 0;
|
|
1305
|
+
try {
|
|
1306
|
+
return await runner.execute(command, args);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
async function discovered(input) {
|
|
1312
|
+
const identity = await optional(input.runner, "aws", [
|
|
1313
|
+
"sts",
|
|
1314
|
+
"get-caller-identity",
|
|
1315
|
+
"--output",
|
|
1316
|
+
"json"
|
|
1317
|
+
]);
|
|
1318
|
+
const aws = parseOrUnavailable(identity) ?? {};
|
|
1319
|
+
const accountId = typeof aws.Account === "string" ? aws.Account : void 0;
|
|
1320
|
+
const region2 = await optional(input.runner, "aws", ["configure", "get", "region"]);
|
|
1321
|
+
const repo = await optional(input.runner, "gh", ["api", `repos/${input.repository}`]);
|
|
1322
|
+
const github = parseOrUnavailable(repo) ?? {};
|
|
1323
|
+
return {
|
|
1324
|
+
accountId,
|
|
1325
|
+
region: region2?.trim() || void 0,
|
|
1326
|
+
ownerId: typeof github.owner?.id === "number" || typeof github.owner?.id === "string" ? String(github.owner.id) : void 0,
|
|
1327
|
+
repositoryId: typeof github.id === "number" || typeof github.id === "string" ? String(github.id) : void 0
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
async function scaffoldBootstrapManifest(input) {
|
|
1331
|
+
const slug = value(input.slug, "slug");
|
|
1332
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(slug))
|
|
1333
|
+
throw new Error("slug must use lowercase letters, digits, and hyphens");
|
|
1334
|
+
const repository = value(input.repository, "repository");
|
|
1335
|
+
repositoryParts(repository);
|
|
1336
|
+
const environment = value(input.environment, "environment");
|
|
1337
|
+
const providerArn = value(input.providerArn, "github OIDC provider ARN");
|
|
1338
|
+
if (!accountFromArn(providerArn)) throw new Error("github OIDC provider ARN is invalid");
|
|
1339
|
+
const found = await discovered(input);
|
|
1340
|
+
const accountId = input.accountId ?? found.accountId;
|
|
1341
|
+
const region2 = input.region ?? found.region;
|
|
1342
|
+
const ownerId = input.ownerId ?? found.ownerId;
|
|
1343
|
+
const repositoryId = input.repositoryId ?? found.repositoryId;
|
|
1344
|
+
if (!accountId) throw new Error("--account-id is required when AWS identity is unavailable");
|
|
1345
|
+
if (!region2) throw new Error("--region is required when AWS Region is unavailable");
|
|
1346
|
+
if (!ownerId) throw new Error("--github-owner-id is required when GitHub is unavailable");
|
|
1347
|
+
if (!repositoryId)
|
|
1348
|
+
throw new Error("--github-repository-id is required when GitHub is unavailable");
|
|
1349
|
+
return {
|
|
1350
|
+
region: region2,
|
|
1351
|
+
core: {
|
|
1352
|
+
stackName: `${slug}-bootstrap`,
|
|
1353
|
+
artifactBucketName: `${slug}-lambda-artifacts-${accountId}`,
|
|
1354
|
+
applicationStackName: slug,
|
|
1355
|
+
dashboardFunctionName: slug
|
|
1356
|
+
},
|
|
1357
|
+
githubOidc: {
|
|
1358
|
+
stackName: `${slug}-github-bootstrap`,
|
|
1359
|
+
providerArn,
|
|
1360
|
+
repository,
|
|
1361
|
+
ownerId,
|
|
1362
|
+
repositoryId,
|
|
1363
|
+
environment
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function incomplete(config) {
|
|
1368
|
+
const fields = [
|
|
1369
|
+
["region", config.region],
|
|
1370
|
+
["core.stackName", config.core?.stackName],
|
|
1371
|
+
["core.artifactBucketName", config.core?.artifactBucketName],
|
|
1372
|
+
["core.applicationStackName", config.core?.applicationStackName],
|
|
1373
|
+
["core.dashboardFunctionName", config.core?.dashboardFunctionName],
|
|
1374
|
+
["githubOidc.stackName", config.githubOidc?.stackName],
|
|
1375
|
+
["githubOidc.providerArn", config.githubOidc?.providerArn],
|
|
1376
|
+
["githubOidc.repository", config.githubOidc?.repository],
|
|
1377
|
+
["githubOidc.ownerId", config.githubOidc?.ownerId],
|
|
1378
|
+
["githubOidc.repositoryId", config.githubOidc?.repositoryId],
|
|
1379
|
+
["githubOidc.environment", config.githubOidc?.environment]
|
|
1380
|
+
];
|
|
1381
|
+
return fields.filter(([, v]) => typeof v !== "string" || !v).map(([name]) => name);
|
|
1382
|
+
}
|
|
1383
|
+
function parseOrUnavailable(raw) {
|
|
1384
|
+
if (!raw) return void 0;
|
|
1385
|
+
try {
|
|
1386
|
+
return JSON.parse(raw);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return void 0;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function absent(raw) {
|
|
1392
|
+
return /not found|nosuchentity|404/i.test(raw ?? "");
|
|
1393
|
+
}
|
|
1394
|
+
async function bootstrapPreflight(input) {
|
|
1395
|
+
const checks = [];
|
|
1396
|
+
const missing = incomplete(input.config);
|
|
1397
|
+
checks.push(
|
|
1398
|
+
missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
|
|
1399
|
+
);
|
|
1400
|
+
if (missing.length) return { ready: false, checks };
|
|
1401
|
+
const runner = input.runner;
|
|
1402
|
+
const identityRaw = await optional(runner, "aws", [
|
|
1403
|
+
"sts",
|
|
1404
|
+
"get-caller-identity",
|
|
1405
|
+
"--output",
|
|
1406
|
+
"json"
|
|
1407
|
+
]);
|
|
1408
|
+
const identity = parseOrUnavailable(identityRaw);
|
|
1409
|
+
const providerAccount = accountFromArn(input.config.githubOidc?.providerArn ?? "");
|
|
1410
|
+
if (!identity || typeof identity.Account !== "string")
|
|
1411
|
+
checks.push({
|
|
1412
|
+
name: "aws-identity",
|
|
1413
|
+
status: "unverified",
|
|
1414
|
+
detail: "AWS identity could not be read."
|
|
1415
|
+
});
|
|
1416
|
+
else
|
|
1417
|
+
checks.push(
|
|
1418
|
+
identity.Account === providerAccount ? {
|
|
1419
|
+
name: "aws-identity",
|
|
1420
|
+
status: "ready",
|
|
1421
|
+
detail: `AWS account ${identity.Account} matches the provider ARN.`
|
|
1422
|
+
} : {
|
|
1423
|
+
name: "aws-identity",
|
|
1424
|
+
status: "mismatch",
|
|
1425
|
+
detail: `AWS account ${identity.Account} does not match provider account ${providerAccount}.`
|
|
1426
|
+
}
|
|
1427
|
+
);
|
|
1428
|
+
const regionRaw = await optional(runner, "aws", ["configure", "get", "region"]);
|
|
1429
|
+
if (!regionRaw?.trim())
|
|
1430
|
+
checks.push({
|
|
1431
|
+
name: "aws-region",
|
|
1432
|
+
status: "unverified",
|
|
1433
|
+
detail: "AWS Region could not be read."
|
|
1434
|
+
});
|
|
1435
|
+
else
|
|
1436
|
+
checks.push(
|
|
1437
|
+
regionRaw.trim() === input.config.region ? { name: "aws-region", status: "ready", detail: `AWS Region is ${input.config.region}.` } : {
|
|
1438
|
+
name: "aws-region",
|
|
1439
|
+
status: "mismatch",
|
|
1440
|
+
detail: `AWS Region ${regionRaw.trim()} does not match manifest ${input.config.region}.`
|
|
1441
|
+
}
|
|
1442
|
+
);
|
|
1443
|
+
const providerRaw = await optional(runner, "aws", [
|
|
1444
|
+
"iam",
|
|
1445
|
+
"get-open-id-connect-provider",
|
|
1446
|
+
"--open-id-connect-provider-arn",
|
|
1447
|
+
input.config.githubOidc?.providerArn ?? ""
|
|
1448
|
+
]);
|
|
1449
|
+
checks.push(
|
|
1450
|
+
!providerRaw ? {
|
|
1451
|
+
name: "oidc-provider",
|
|
1452
|
+
status: "unverified",
|
|
1453
|
+
detail: "OIDC provider could not be queried."
|
|
1454
|
+
} : absent(providerRaw) ? { name: "oidc-provider", status: "missing", detail: "OIDC provider does not exist." } : { name: "oidc-provider", status: "ready", detail: "OIDC provider exists." }
|
|
1455
|
+
);
|
|
1456
|
+
const repository = input.config.githubOidc?.repository ?? "";
|
|
1457
|
+
const repoRaw = await optional(runner, "gh", ["api", `repos/${repository}`]);
|
|
1458
|
+
const repo = parseOrUnavailable(repoRaw);
|
|
1459
|
+
if (!repoRaw)
|
|
1460
|
+
checks.push({
|
|
1461
|
+
name: "github-repository",
|
|
1462
|
+
status: "unverified",
|
|
1463
|
+
detail: "GitHub repository could not be queried."
|
|
1464
|
+
});
|
|
1465
|
+
else if (absent(repoRaw))
|
|
1466
|
+
checks.push({
|
|
1467
|
+
name: "github-repository",
|
|
1468
|
+
status: "missing",
|
|
1469
|
+
detail: "GitHub repository does not exist or is unavailable."
|
|
1470
|
+
});
|
|
1471
|
+
else if (String(repo?.id) !== input.config.githubOidc?.repositoryId || String(repo?.owner?.id) !== input.config.githubOidc?.ownerId)
|
|
1472
|
+
checks.push({
|
|
1473
|
+
name: "github-repository",
|
|
1474
|
+
status: "mismatch",
|
|
1475
|
+
detail: "GitHub numeric repository identity does not match the manifest."
|
|
1476
|
+
});
|
|
1477
|
+
else
|
|
1478
|
+
checks.push({
|
|
1479
|
+
name: "github-repository",
|
|
1480
|
+
status: "ready",
|
|
1481
|
+
detail: "GitHub repository identity matches the manifest."
|
|
1482
|
+
});
|
|
1483
|
+
const environmentRaw = await optional(runner, "gh", [
|
|
1484
|
+
"api",
|
|
1485
|
+
`repos/${repository}/environments/${input.config.githubOidc?.environment}`
|
|
1486
|
+
]);
|
|
1487
|
+
checks.push(
|
|
1488
|
+
!environmentRaw ? {
|
|
1489
|
+
name: "github-environment",
|
|
1490
|
+
status: "unverified",
|
|
1491
|
+
detail: "GitHub Environment could not be queried."
|
|
1492
|
+
} : absent(environmentRaw) ? {
|
|
1493
|
+
name: "github-environment",
|
|
1494
|
+
status: "missing",
|
|
1495
|
+
detail: "GitHub Environment does not exist."
|
|
1496
|
+
} : {
|
|
1497
|
+
name: "github-environment",
|
|
1498
|
+
status: "ready",
|
|
1499
|
+
detail: "GitHub Environment exists; its policy is administrator-owned context."
|
|
1500
|
+
}
|
|
1501
|
+
);
|
|
1502
|
+
const subject = await (input.provider ?? githubOidcProvider).prerequisite(input.config, runner);
|
|
1503
|
+
checks.push({
|
|
1504
|
+
name: "immutable-subject",
|
|
1505
|
+
status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
|
|
1506
|
+
detail: subject.detail
|
|
1507
|
+
});
|
|
1508
|
+
return {
|
|
1509
|
+
ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
|
|
1510
|
+
checks
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
function quote(args) {
|
|
1514
|
+
return args.map((arg) => `'${arg.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
|
|
1515
|
+
}
|
|
1516
|
+
async function bootstrapGuide(input) {
|
|
1517
|
+
const handoff = await bootstrapHandoff(input);
|
|
1518
|
+
const lines = [
|
|
1519
|
+
`Phase: ${handoff.phase}`,
|
|
1520
|
+
`Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
|
|
1521
|
+
];
|
|
1522
|
+
if (handoff.expectedOutputs.length)
|
|
1523
|
+
lines.push(`Expected outputs: ${handoff.expectedOutputs.join(", ")}`);
|
|
1524
|
+
if (handoff.prerequisite)
|
|
1525
|
+
lines.push(`Prerequisite: ${handoff.prerequisite.status} \u2014 ${handoff.prerequisite.detail}`);
|
|
1526
|
+
lines.push("", "Commands (copy and run each AWS command yourself):");
|
|
1527
|
+
for (const command of handoff.commands) {
|
|
1528
|
+
if (command.name === "review-change-set")
|
|
1529
|
+
lines.push("PAUSE: review the change set before approval.");
|
|
1530
|
+
if (command.name === "execute-reviewed-change-set")
|
|
1531
|
+
lines.push("PAUSE: execute only after explicit approval.");
|
|
1532
|
+
lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
|
|
1533
|
+
}
|
|
1534
|
+
if (handoff.reviewCheckpoints.length)
|
|
1535
|
+
lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
|
|
1536
|
+
if (input.coreStack !== void 0 && input.githubOidcStack !== void 0) {
|
|
1537
|
+
const verified = await verifyBootstrap({
|
|
1538
|
+
config: input.config,
|
|
1539
|
+
coreStack: input.coreStack,
|
|
1540
|
+
githubOidcStack: input.githubOidcStack
|
|
1541
|
+
});
|
|
1542
|
+
lines.push(
|
|
1543
|
+
"",
|
|
1544
|
+
"Optional GitHub administrator action (after the successful verification above):"
|
|
1545
|
+
);
|
|
1546
|
+
for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
|
|
1547
|
+
}
|
|
1548
|
+
return `${lines.join("\n")}
|
|
1549
|
+
`;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1281
1552
|
// packages/aws/src/index.ts
|
|
1282
1553
|
var run = promisify(execFile);
|
|
1283
1554
|
function deploymentTemplate(template, values) {
|
|
@@ -1418,7 +1689,9 @@ async function cloudFormationTemplate() {
|
|
|
1418
1689
|
}
|
|
1419
1690
|
export {
|
|
1420
1691
|
bootstrapContractVersion,
|
|
1692
|
+
bootstrapGuide,
|
|
1421
1693
|
bootstrapHandoff,
|
|
1694
|
+
bootstrapPreflight,
|
|
1422
1695
|
bootstrapTemplate,
|
|
1423
1696
|
bootstrapTemplatePath,
|
|
1424
1697
|
cloudFormationTemplate,
|
|
@@ -1430,5 +1703,6 @@ export {
|
|
|
1430
1703
|
packageLambda,
|
|
1431
1704
|
publishClientAssets,
|
|
1432
1705
|
requiredBootstrapParameters,
|
|
1706
|
+
scaffoldBootstrapManifest,
|
|
1433
1707
|
verifyBootstrap
|
|
1434
1708
|
};
|