@biffo/cli 0.80.0 → 0.82.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/index.js +318 -42
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command25 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/core.ts
|
|
7
7
|
import { Command as Command4 } from "commander";
|
|
@@ -819,7 +819,7 @@ var GitHubAdapter = class {
|
|
|
819
819
|
} catch (err) {
|
|
820
820
|
if (err.status !== 404) throw err;
|
|
821
821
|
}
|
|
822
|
-
await new Promise((
|
|
822
|
+
await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
|
|
823
823
|
}
|
|
824
824
|
throw new Error(
|
|
825
825
|
`Branch "${branch}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
|
|
@@ -834,7 +834,7 @@ var GitHubAdapter = class {
|
|
|
834
834
|
} catch (err) {
|
|
835
835
|
if (err.status !== 404) throw err;
|
|
836
836
|
}
|
|
837
|
-
await new Promise((
|
|
837
|
+
await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
|
|
838
838
|
}
|
|
839
839
|
throw new Error(
|
|
840
840
|
`Ref "${ref}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
|
|
@@ -1039,7 +1039,7 @@ var GitHubAdapter = class {
|
|
|
1039
1039
|
}
|
|
1040
1040
|
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1041
1041
|
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1042
|
-
await new Promise((
|
|
1042
|
+
await new Promise((resolve18) => setTimeout(resolve18, protectionIntervalMs));
|
|
1043
1043
|
}
|
|
1044
1044
|
}
|
|
1045
1045
|
}
|
|
@@ -1082,6 +1082,28 @@ var GitHubAdapter = class {
|
|
|
1082
1082
|
}
|
|
1083
1083
|
}
|
|
1084
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Read an environment-scoped Actions variable's value, or `null` if it isn't
|
|
1087
|
+
* set (a 404). The counterpart to `setEnvVariable`, used by
|
|
1088
|
+
* `biffo sibling check-identity` to read each sibling's baked-in
|
|
1089
|
+
* `CORE_COGNITO_USER_POOL_ID` and compare it against the core's live pool
|
|
1090
|
+
* (#400/#496). A 404 means the variable was never set on that environment,
|
|
1091
|
+
* which is a real, reportable state — not an error — so it maps to `null`.
|
|
1092
|
+
*/
|
|
1093
|
+
async getEnvVariable(org, repo, env, name) {
|
|
1094
|
+
try {
|
|
1095
|
+
const { data } = await this.octokit.request(
|
|
1096
|
+
"GET /repos/{owner}/{repo}/environments/{environment_name}/variables/{variable_name}",
|
|
1097
|
+
{ owner: org, repo, environment_name: env, variable_name: name }
|
|
1098
|
+
);
|
|
1099
|
+
return data.value;
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
if (err.status === 404) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
throw err;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1085
1107
|
async setRepoVariable(org, repo, name, value) {
|
|
1086
1108
|
log.info(`Setting variable: ${name}`);
|
|
1087
1109
|
try {
|
|
@@ -1243,7 +1265,7 @@ var GitHubAdapter = class {
|
|
|
1243
1265
|
} catch (err) {
|
|
1244
1266
|
if (err.status !== 404 || Date.now() >= deadline) throw err;
|
|
1245
1267
|
log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
|
|
1246
|
-
await new Promise((
|
|
1268
|
+
await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
|
|
1247
1269
|
}
|
|
1248
1270
|
}
|
|
1249
1271
|
}
|
|
@@ -1262,7 +1284,7 @@ var GitHubAdapter = class {
|
|
|
1262
1284
|
} catch (err) {
|
|
1263
1285
|
if (err.status !== 404 || Date.now() >= deadline) throw err;
|
|
1264
1286
|
log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
|
|
1265
|
-
await new Promise((
|
|
1287
|
+
await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
|
|
1266
1288
|
}
|
|
1267
1289
|
}
|
|
1268
1290
|
}
|
|
@@ -1286,7 +1308,7 @@ var GitHubAdapter = class {
|
|
|
1286
1308
|
} else {
|
|
1287
1309
|
log.info(" Waiting for run to be queued...");
|
|
1288
1310
|
}
|
|
1289
|
-
await new Promise((
|
|
1311
|
+
await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
|
|
1290
1312
|
}
|
|
1291
1313
|
throw new Error(
|
|
1292
1314
|
`Workflow ${workflowId} did not complete within ${timeoutMs / 1e3 / 60} minutes`
|
|
@@ -2831,7 +2853,7 @@ var AwsAdapter = class {
|
|
|
2831
2853
|
const code = err.Code;
|
|
2832
2854
|
if (code === "OperationAborted" && attempt < maxAttempts) {
|
|
2833
2855
|
log.info(` Waiting for S3 to release "${bucketName}"... (${attempt}/${maxAttempts})`);
|
|
2834
|
-
await new Promise((
|
|
2856
|
+
await new Promise((resolve18) => setTimeout(resolve18, retryDelayMs));
|
|
2835
2857
|
} else if (code === "OperationAborted") {
|
|
2836
2858
|
return false;
|
|
2837
2859
|
} else {
|
|
@@ -7258,14 +7280,268 @@ pluginCommand.addCommand(pluginSyncMigrationsCommand);
|
|
|
7258
7280
|
pluginCommand.addCommand(pluginInfoCommand);
|
|
7259
7281
|
|
|
7260
7282
|
// src/commands/sibling.ts
|
|
7283
|
+
import { Command as Command22 } from "commander";
|
|
7284
|
+
|
|
7285
|
+
// src/commands/sibling-check-identity.ts
|
|
7286
|
+
import { existsSync as existsSync29, readFileSync as readFileSync22 } from "fs";
|
|
7287
|
+
import { resolve as resolve17 } from "path";
|
|
7288
|
+
import chalk20 from "chalk";
|
|
7261
7289
|
import { Command as Command21 } from "commander";
|
|
7262
|
-
|
|
7290
|
+
|
|
7291
|
+
// src/lib/sibling-identity-check.ts
|
|
7292
|
+
function checkSiblingIdentity(envs) {
|
|
7293
|
+
const findings = [];
|
|
7294
|
+
for (const env of envs) {
|
|
7295
|
+
if (env.publishedDoc === null) {
|
|
7296
|
+
findings.push({
|
|
7297
|
+
environment: env.environment,
|
|
7298
|
+
subject: "published-document",
|
|
7299
|
+
kind: "published-doc-unreachable",
|
|
7300
|
+
expected: env.livePoolId,
|
|
7301
|
+
actual: null
|
|
7302
|
+
});
|
|
7303
|
+
} else {
|
|
7304
|
+
const docPool = env.publishedDoc.userPoolId ?? null;
|
|
7305
|
+
if (docPool !== env.livePoolId) {
|
|
7306
|
+
findings.push({
|
|
7307
|
+
environment: env.environment,
|
|
7308
|
+
subject: "published-document",
|
|
7309
|
+
kind: "published-doc-stale",
|
|
7310
|
+
expected: env.livePoolId,
|
|
7311
|
+
actual: docPool
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
for (const sibling of env.siblings) {
|
|
7316
|
+
if (sibling.coreCognitoUserPoolId === null) {
|
|
7317
|
+
findings.push({
|
|
7318
|
+
environment: env.environment,
|
|
7319
|
+
subject: sibling.projectName,
|
|
7320
|
+
kind: "sibling-var-missing",
|
|
7321
|
+
expected: env.livePoolId,
|
|
7322
|
+
actual: null
|
|
7323
|
+
});
|
|
7324
|
+
} else if (sibling.coreCognitoUserPoolId !== env.livePoolId) {
|
|
7325
|
+
findings.push({
|
|
7326
|
+
environment: env.environment,
|
|
7327
|
+
subject: sibling.projectName,
|
|
7328
|
+
kind: "sibling-backend-stale",
|
|
7329
|
+
expected: env.livePoolId,
|
|
7330
|
+
actual: sibling.coreCognitoUserPoolId
|
|
7331
|
+
});
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7335
|
+
return { ok: findings.length === 0, findings };
|
|
7336
|
+
}
|
|
7337
|
+
|
|
7338
|
+
// src/commands/sibling-check-identity.ts
|
|
7339
|
+
var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
|
|
7340
|
+
var SIBLING_CORE_POOL_VAR = "CORE_COGNITO_USER_POOL_ID";
|
|
7341
|
+
var siblingCheckIdentityCommand = new Command21("check-identity").description(
|
|
7342
|
+
"Detect when a core's Cognito pool has drifted from its published identity document or any sibling's baked-in CORE_COGNITO_USER_POOL_ID (#400). Run from the core repo; exits non-zero on drift so a scheduled/CI run goes red."
|
|
7343
|
+
).option("--env <environment>", "Only check this environment (default: dev, staging, prod)").option("-p, --project <name>", "Project name (overrides biffo.config.json in current directory)").option("-c, --config <path>", "Path to biffo.config.json").action(async (options) => {
|
|
7344
|
+
if (options.env && !VALID_ENVIRONMENTS2.includes(options.env)) {
|
|
7345
|
+
log.error(
|
|
7346
|
+
`Unknown environment: ${options.env}. Must be one of: ${VALID_ENVIRONMENTS2.join(", ")}`
|
|
7347
|
+
);
|
|
7348
|
+
process.exit(1);
|
|
7349
|
+
}
|
|
7350
|
+
const environments = options.env ? [options.env] : [...VALID_ENVIRONMENTS2];
|
|
7351
|
+
const config = await resolveConfig4(options);
|
|
7352
|
+
const { org, repo } = config.source_control.config;
|
|
7353
|
+
const awsConfig2 = config.cloud.config;
|
|
7354
|
+
const stateBucket = awsConfig2.tf_state_bucket ?? `${config.project.name}-terraform-state-${awsConfig2.account_id}`;
|
|
7355
|
+
const token = await resolveGithubToken3(true);
|
|
7356
|
+
const deps = {
|
|
7357
|
+
aws: new AwsAdapter(config),
|
|
7358
|
+
github: new GitHubAdapter(token),
|
|
7359
|
+
fetchIdentityDoc: fetchPublishedIdentity
|
|
7360
|
+
};
|
|
7361
|
+
console.log(chalk20.bold(`
|
|
7362
|
+
Biffo \u2014 Sibling identity check (${config.project.name})
|
|
7363
|
+
`));
|
|
7364
|
+
let result;
|
|
7365
|
+
try {
|
|
7366
|
+
result = await runCheckIdentity(deps, {
|
|
7367
|
+
coreOrg: org,
|
|
7368
|
+
coreRepo: repo,
|
|
7369
|
+
coreProjectName: config.project.name,
|
|
7370
|
+
stateBucket,
|
|
7371
|
+
environments
|
|
7372
|
+
});
|
|
7373
|
+
} catch (err) {
|
|
7374
|
+
if (err instanceof SiblingResolutionError) {
|
|
7375
|
+
log.error(`Could not enumerate siblings: ${err.message}`);
|
|
7376
|
+
} else {
|
|
7377
|
+
log.error(`Identity check failed: ${err.message}`);
|
|
7378
|
+
}
|
|
7379
|
+
process.exit(1);
|
|
7380
|
+
}
|
|
7381
|
+
printIdentityReport(result);
|
|
7382
|
+
if (!result.ok) process.exit(1);
|
|
7383
|
+
});
|
|
7384
|
+
async function runCheckIdentity(deps, params) {
|
|
7385
|
+
const siblings = await discoverSiblings(
|
|
7386
|
+
deps.github,
|
|
7387
|
+
params.coreOrg,
|
|
7388
|
+
params.coreRepo,
|
|
7389
|
+
params.coreProjectName
|
|
7390
|
+
);
|
|
7391
|
+
const envInputs = [];
|
|
7392
|
+
const skipped = [];
|
|
7393
|
+
for (const environment of params.environments) {
|
|
7394
|
+
let outputs;
|
|
7395
|
+
try {
|
|
7396
|
+
outputs = await deps.aws.readTerraformOutputs(
|
|
7397
|
+
params.stateBucket,
|
|
7398
|
+
`${environment}/terraform.tfstate`
|
|
7399
|
+
);
|
|
7400
|
+
} catch {
|
|
7401
|
+
skipped.push({
|
|
7402
|
+
environment,
|
|
7403
|
+
reason: "no Terraform state (core not deployed to this environment)"
|
|
7404
|
+
});
|
|
7405
|
+
continue;
|
|
7406
|
+
}
|
|
7407
|
+
const livePoolId = outputs["cognito_user_pool_id"];
|
|
7408
|
+
const portalUrl = outputs["portal_url"];
|
|
7409
|
+
if (!livePoolId || !portalUrl) {
|
|
7410
|
+
skipped.push({
|
|
7411
|
+
environment,
|
|
7412
|
+
reason: "Terraform outputs missing cognito_user_pool_id or portal_url"
|
|
7413
|
+
});
|
|
7414
|
+
continue;
|
|
7415
|
+
}
|
|
7416
|
+
const publishedDoc = await deps.fetchIdentityDoc(portalUrl);
|
|
7417
|
+
const envSiblings = siblings.filter(
|
|
7418
|
+
(s) => s.registered && s.repoState !== "gone" && s.environments.includes(environment)
|
|
7419
|
+
);
|
|
7420
|
+
const siblingInputs = await Promise.all(
|
|
7421
|
+
envSiblings.map(async (s) => ({
|
|
7422
|
+
projectName: s.projectName,
|
|
7423
|
+
coreCognitoUserPoolId: await deps.github.getEnvVariable(
|
|
7424
|
+
s.org,
|
|
7425
|
+
s.repo,
|
|
7426
|
+
environment,
|
|
7427
|
+
SIBLING_CORE_POOL_VAR
|
|
7428
|
+
)
|
|
7429
|
+
}))
|
|
7430
|
+
);
|
|
7431
|
+
envInputs.push({ environment, livePoolId, publishedDoc, siblings: siblingInputs });
|
|
7432
|
+
}
|
|
7433
|
+
const { ok, findings } = checkSiblingIdentity(envInputs);
|
|
7434
|
+
return { ok, findings, skipped };
|
|
7435
|
+
}
|
|
7436
|
+
var FINDING_LABEL = {
|
|
7437
|
+
"published-doc-unreachable": "published identity document unreachable",
|
|
7438
|
+
"published-doc-stale": "published identity document is stale",
|
|
7439
|
+
"sibling-var-missing": `sibling backend has no ${SIBLING_CORE_POOL_VAR}`,
|
|
7440
|
+
"sibling-backend-stale": `sibling backend ${SIBLING_CORE_POOL_VAR} is stale`
|
|
7441
|
+
};
|
|
7442
|
+
function printIdentityReport(result) {
|
|
7443
|
+
for (const s of result.skipped) {
|
|
7444
|
+
log.warn(`${s.environment}: skipped \u2014 ${s.reason}`);
|
|
7445
|
+
}
|
|
7446
|
+
if (result.ok) {
|
|
7447
|
+
log.success(
|
|
7448
|
+
chalk20.green(
|
|
7449
|
+
"\u2713 identity consistent \u2014 every published document and sibling backend matches the live pool"
|
|
7450
|
+
)
|
|
7451
|
+
);
|
|
7452
|
+
return;
|
|
7453
|
+
}
|
|
7454
|
+
log.error(chalk20.red(`\u2718 ${String(result.findings.length)} identity drift finding(s):`));
|
|
7455
|
+
for (const f of result.findings) {
|
|
7456
|
+
console.error(
|
|
7457
|
+
chalk20.red(
|
|
7458
|
+
` [${f.environment}] ${f.subject}: ${FINDING_LABEL[f.kind]}
|
|
7459
|
+
expected (live pool): ${f.expected}
|
|
7460
|
+
found: ${f.actual ?? "(unset)"}`
|
|
7461
|
+
)
|
|
7462
|
+
);
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
async function fetchPublishedIdentity(portalUrl) {
|
|
7466
|
+
try {
|
|
7467
|
+
const base = portalUrl.replace(/\/+$/, "");
|
|
7468
|
+
const res = await fetch(`${base}/.well-known/biffo-identity.json`, {
|
|
7469
|
+
cache: "no-store"
|
|
7470
|
+
});
|
|
7471
|
+
if (!res.ok) return null;
|
|
7472
|
+
const data = await res.json();
|
|
7473
|
+
return { userPoolId: data?.userPoolId ?? null };
|
|
7474
|
+
} catch {
|
|
7475
|
+
return null;
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7478
|
+
async function resolveConfig4(options) {
|
|
7479
|
+
if (options.config) {
|
|
7480
|
+
const raw = JSON.parse(readFileSync22(resolve17(options.config), "utf8"));
|
|
7481
|
+
const result = BiffoConfigSchema.safeParse(raw);
|
|
7482
|
+
if (!result.success) {
|
|
7483
|
+
log.error(`Invalid config at ${options.config}:`);
|
|
7484
|
+
result.error.issues.forEach((i) => log.error(` ${i.path.join(".")} \u2014 ${i.message}`));
|
|
7485
|
+
process.exit(1);
|
|
7486
|
+
}
|
|
7487
|
+
return result.data;
|
|
7488
|
+
}
|
|
7489
|
+
if (options.project) {
|
|
7490
|
+
const cfg = loadProjectConfig(options.project);
|
|
7491
|
+
if (!cfg) {
|
|
7492
|
+
log.error(
|
|
7493
|
+
`Project "${options.project}" not found in ~/.biffo/projects/. Run biffo init first or pass --config <path>.`
|
|
7494
|
+
);
|
|
7495
|
+
process.exit(1);
|
|
7496
|
+
}
|
|
7497
|
+
return cfg;
|
|
7498
|
+
}
|
|
7499
|
+
const localConfigPath = resolve17(process.cwd(), "biffo.config.json");
|
|
7500
|
+
if (existsSync29(localConfigPath)) {
|
|
7501
|
+
const raw = JSON.parse(readFileSync22(localConfigPath, "utf8"));
|
|
7502
|
+
const result = BiffoConfigSchema.safeParse(raw);
|
|
7503
|
+
if (result.success) return result.data;
|
|
7504
|
+
if (isTemplatePlaceholderConfig(raw)) {
|
|
7505
|
+
log.warn(
|
|
7506
|
+
`Ignoring ${localConfigPath} \u2014 it is the unsubstituted Biffo template placeholder, not this project's config.`
|
|
7507
|
+
);
|
|
7508
|
+
} else {
|
|
7509
|
+
log.error(`Invalid config at ${localConfigPath}:`);
|
|
7510
|
+
result.error.issues.forEach((i) => log.error(` ${i.path.join(".")} \u2014 ${i.message}`));
|
|
7511
|
+
log.error("Refusing to fall back to a saved project while a local config file is present.");
|
|
7512
|
+
log.error("Fix biffo.config.json, or pass --project <name> / --config <path> explicitly.");
|
|
7513
|
+
process.exit(1);
|
|
7514
|
+
}
|
|
7515
|
+
}
|
|
7516
|
+
const projects = listProjectConfigs();
|
|
7517
|
+
if (projects.length === 0) {
|
|
7518
|
+
log.error(
|
|
7519
|
+
"No biffo.config.json found in the current directory and no projects in ~/.biffo/projects/."
|
|
7520
|
+
);
|
|
7521
|
+
log.error("Run biffo init first, or pass --project <name> or --config <path>.");
|
|
7522
|
+
log.error(
|
|
7523
|
+
"A Biffo instance's resolved config is not committed to its repo \u2014 it lives in ~/.biffo/projects/ on the machine that ran biffo init. On a second machine, copy it there or pass it with --config <path>."
|
|
7524
|
+
);
|
|
7525
|
+
process.exit(1);
|
|
7526
|
+
}
|
|
7527
|
+
if (projects.length === 1) {
|
|
7528
|
+
log.info(`Using project: ${projects[0].project.name}`);
|
|
7529
|
+
return projects[0];
|
|
7530
|
+
}
|
|
7531
|
+
log.error("Multiple projects found in ~/.biffo/projects/. Pass --project <name> to choose one:");
|
|
7532
|
+
for (const p of projects) log.error(` ${p.project.name}`);
|
|
7533
|
+
process.exit(1);
|
|
7534
|
+
}
|
|
7535
|
+
|
|
7536
|
+
// src/commands/sibling.ts
|
|
7537
|
+
var siblingCommand = new Command22("sibling").description(
|
|
7263
7538
|
"Create and manage sibling apps that share a Biffo core project (ADR-0007)"
|
|
7264
7539
|
);
|
|
7265
7540
|
siblingCommand.addCommand(siblingCreateCommand);
|
|
7541
|
+
siblingCommand.addCommand(siblingCheckIdentityCommand);
|
|
7266
7542
|
|
|
7267
7543
|
// src/commands/check.ts
|
|
7268
|
-
import { Command as
|
|
7544
|
+
import { Command as Command23 } from "commander";
|
|
7269
7545
|
|
|
7270
7546
|
// src/scripts/check-core-ownership.ts
|
|
7271
7547
|
import { execa as execa5 } from "execa";
|
|
@@ -7291,8 +7567,8 @@ async function runOwnershipCheck(argv) {
|
|
|
7291
7567
|
const { stdout } = await execa5("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
7292
7568
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
7293
7569
|
if (messageFile) {
|
|
7294
|
-
const { readFileSync:
|
|
7295
|
-
if (
|
|
7570
|
+
const { readFileSync: readFileSync24, existsSync: existsSync31 } = await import("fs");
|
|
7571
|
+
if (existsSync31(messageFile)) commitMessage = readFileSync24(messageFile, "utf8");
|
|
7296
7572
|
}
|
|
7297
7573
|
} else {
|
|
7298
7574
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -7396,7 +7672,7 @@ ${BOLD}If the divergence is deliberate${OFF}
|
|
|
7396
7672
|
import { execa as execa6 } from "execa";
|
|
7397
7673
|
|
|
7398
7674
|
// src/lib/plugin-terraform-guard.ts
|
|
7399
|
-
import { existsSync as
|
|
7675
|
+
import { existsSync as existsSync30, readFileSync as readFileSync23, readdirSync as readdirSync12 } from "fs";
|
|
7400
7676
|
import { dirname as dirname9, join as join30, relative as relative6, sep as sep3 } from "path";
|
|
7401
7677
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
7402
7678
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
@@ -7424,7 +7700,7 @@ function findPluginManifests(root) {
|
|
|
7424
7700
|
function readSubscriptions(absManifestPath) {
|
|
7425
7701
|
let parsed;
|
|
7426
7702
|
try {
|
|
7427
|
-
parsed = JSON.parse(
|
|
7703
|
+
parsed = JSON.parse(readFileSync23(absManifestPath, "utf8"));
|
|
7428
7704
|
} catch {
|
|
7429
7705
|
return null;
|
|
7430
7706
|
}
|
|
@@ -7439,14 +7715,14 @@ function readSubscriptions(absManifestPath) {
|
|
|
7439
7715
|
}
|
|
7440
7716
|
function checkPluginTerraform(root) {
|
|
7441
7717
|
const violations = [];
|
|
7442
|
-
const coreManifest =
|
|
7718
|
+
const coreManifest = existsSync30(join30(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
7443
7719
|
for (const manifest of findPluginManifests(root)) {
|
|
7444
7720
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
7445
7721
|
const absManifest = join30(root, manifest);
|
|
7446
7722
|
const subscriptions = readSubscriptions(absManifest);
|
|
7447
7723
|
if (subscriptions === null) continue;
|
|
7448
7724
|
const pluginDir2 = dirname9(absManifest);
|
|
7449
|
-
if (
|
|
7725
|
+
if (existsSync30(join30(pluginDir2, "terraform"))) continue;
|
|
7450
7726
|
const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
|
|
7451
7727
|
violations.push({
|
|
7452
7728
|
manifest,
|
|
@@ -7565,7 +7841,7 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
7565
7841
|
}
|
|
7566
7842
|
|
|
7567
7843
|
// src/commands/check.ts
|
|
7568
|
-
var checkCommand = new
|
|
7844
|
+
var checkCommand = new Command23("check").description(
|
|
7569
7845
|
"Repo guards (ownership, release subject, plugin terraform) \u2014 run in CI and git hooks"
|
|
7570
7846
|
);
|
|
7571
7847
|
checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
|
|
@@ -7585,17 +7861,17 @@ function rawArgsAfter(subcommand) {
|
|
|
7585
7861
|
// src/commands/teardown.ts
|
|
7586
7862
|
import { execSync as execSync7 } from "child_process";
|
|
7587
7863
|
import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
|
|
7588
|
-
import
|
|
7589
|
-
import { Command as
|
|
7864
|
+
import chalk21 from "chalk";
|
|
7865
|
+
import { Command as Command24 } from "commander";
|
|
7590
7866
|
import inquirer8 from "inquirer";
|
|
7591
|
-
var teardownCommand = new
|
|
7867
|
+
var teardownCommand = new Command24("teardown").description(
|
|
7592
7868
|
"Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
|
|
7593
7869
|
).option("--project <name>", "Project name to tear down (reads session if omitted)").option("--skip-destroy", "Skip terraform destroy (only use if infrastructure is already gone)").option(
|
|
7594
7870
|
"--confirm <name>",
|
|
7595
7871
|
"Pre-confirm by supplying the project name (the scriptable form of the typed confirmation)"
|
|
7596
7872
|
).option("-y, --yes", "Skip the typed project-name confirmation entirely").action(
|
|
7597
7873
|
async (options) => {
|
|
7598
|
-
console.log(
|
|
7874
|
+
console.log(chalk21.bold("\n Biffo \u2014 Teardown\n"));
|
|
7599
7875
|
const githubToken = resolveGithubToken4();
|
|
7600
7876
|
const sts = new STSClient3({});
|
|
7601
7877
|
const { Account: accountId } = await sts.send(new GetCallerIdentityCommand3({}));
|
|
@@ -7617,7 +7893,7 @@ var teardownCommand = new Command23("teardown").description(
|
|
|
7617
7893
|
adminEmail = session.config.admin?.email ?? "noop@example.com";
|
|
7618
7894
|
adminUsername = session.config.admin?.username ?? "noop";
|
|
7619
7895
|
domain = session.config.project.domain ?? "";
|
|
7620
|
-
console.log(
|
|
7896
|
+
console.log(chalk21.yellow(" Loaded session for: ") + chalk21.bold(projectName));
|
|
7621
7897
|
if (org && repo) console.log(` Repository: ${org}/${repo}`);
|
|
7622
7898
|
console.log();
|
|
7623
7899
|
} else if (savedConfig) {
|
|
@@ -7630,7 +7906,7 @@ var teardownCommand = new Command23("teardown").description(
|
|
|
7630
7906
|
adminEmail = savedConfig.admin.email;
|
|
7631
7907
|
adminUsername = savedConfig.admin.username;
|
|
7632
7908
|
domain = resolveDnsConfig(savedConfig).domain;
|
|
7633
|
-
console.log(
|
|
7909
|
+
console.log(chalk21.yellow(" Loaded config for: ") + chalk21.bold(projectName));
|
|
7634
7910
|
console.log(` Repository: ${org}/${repo}`);
|
|
7635
7911
|
console.log();
|
|
7636
7912
|
} else {
|
|
@@ -7701,35 +7977,35 @@ var teardownCommand = new Command23("teardown").description(
|
|
|
7701
7977
|
throw err;
|
|
7702
7978
|
}
|
|
7703
7979
|
}
|
|
7704
|
-
console.log(
|
|
7980
|
+
console.log(chalk21.red.bold(" This will permanently delete:\n"));
|
|
7705
7981
|
if (deployedEnvs.length > 0 || hasGlobal) {
|
|
7706
|
-
console.log(
|
|
7982
|
+
console.log(chalk21.red(" Infrastructure (via GitHub Actions terraform destroy):"));
|
|
7707
7983
|
for (const env of deployedEnvs) {
|
|
7708
7984
|
console.log(
|
|
7709
|
-
` ${
|
|
7985
|
+
` ${chalk21.red("\u2717")} ${env} \u2014 VPC, RDS, Lambda, Cognito, CloudFront, EventBridge`
|
|
7710
7986
|
);
|
|
7711
7987
|
}
|
|
7712
7988
|
if (hasGlobal) {
|
|
7713
|
-
console.log(` ${
|
|
7989
|
+
console.log(` ${chalk21.red("\u2717")} global \u2014 Route 53 hosted zone, ACM certificate`);
|
|
7714
7990
|
}
|
|
7715
7991
|
console.log();
|
|
7716
7992
|
}
|
|
7717
7993
|
for (const line of formatSiblingPlan(siblings, options.skipDestroy === true)) {
|
|
7718
7994
|
console.log(line);
|
|
7719
7995
|
}
|
|
7720
|
-
console.log(
|
|
7721
|
-
console.log(` ${
|
|
7996
|
+
console.log(chalk21.red(" Biffo resources:"));
|
|
7997
|
+
console.log(` ${chalk21.red("\u2717")} GitHub repository ${chalk21.bold(`${org}/${repo}`)}`);
|
|
7722
7998
|
console.log(
|
|
7723
|
-
` ${
|
|
7999
|
+
` ${chalk21.red("\u2717")} IAM role ${chalk21.bold(`biffo-github-actions-${projectName}`)}`
|
|
7724
8000
|
);
|
|
7725
8001
|
console.log(
|
|
7726
|
-
` ${
|
|
8002
|
+
` ${chalk21.red("\u2717")} S3 bucket ${chalk21.bold(stateBucket)} (all versions)`
|
|
7727
8003
|
);
|
|
7728
|
-
console.log(` ${
|
|
8004
|
+
console.log(` ${chalk21.red("\u2717")} Local session file`);
|
|
7729
8005
|
if (options.skipDestroy) {
|
|
7730
8006
|
console.log();
|
|
7731
8007
|
console.log(
|
|
7732
|
-
|
|
8008
|
+
chalk21.yellow(
|
|
7733
8009
|
" --skip-destroy: NO terraform destroy runs, for this project or any sibling.\n Everything above is deleted, but any infrastructure still standing is left\n standing \u2014 and, with the state buckets gone, orphaned."
|
|
7734
8010
|
)
|
|
7735
8011
|
);
|
|
@@ -7917,30 +8193,30 @@ async function assertSiblingsAreDestroyable(github, siblings) {
|
|
|
7917
8193
|
}
|
|
7918
8194
|
function formatSiblingPlan(siblings, skipDestroy) {
|
|
7919
8195
|
if (siblings.length === 0) return [];
|
|
7920
|
-
const lines = [
|
|
8196
|
+
const lines = [chalk21.red(` Sibling apps (${siblings.length}) \u2014 ADR-0007:`)];
|
|
7921
8197
|
for (const s of siblings) {
|
|
7922
8198
|
const envs = s.environments.join(", ");
|
|
7923
8199
|
if (s.repoState === "gone") {
|
|
7924
8200
|
lines.push(
|
|
7925
|
-
` ${
|
|
8201
|
+
` ${chalk21.yellow("!")} ${chalk21.bold(`${s.org}/${s.repo}`)} \u2014 repo already deleted; its ${envs} infrastructure CANNOT be destroyed and will be left standing`
|
|
7926
8202
|
);
|
|
7927
8203
|
continue;
|
|
7928
8204
|
}
|
|
7929
8205
|
lines.push(
|
|
7930
|
-
` ${
|
|
8206
|
+
` ${chalk21.red("\u2717")} GitHub repository ${chalk21.bold(`${s.org}/${s.repo}`)} ` + (s.pathPrefix === ROOT_SIBLING_NAME ? "(the application, routed at /)" : `(routed at /${s.pathPrefix})`)
|
|
7931
8207
|
);
|
|
7932
8208
|
lines.push(
|
|
7933
|
-
` ${
|
|
8209
|
+
` ${chalk21.red("\u2717")} ${skipDestroy ? "infrastructure NOT destroyed (--skip-destroy)" : `${envs} infrastructure \u2014 S3 site bucket, Lambda, API Gateway`}`
|
|
7934
8210
|
);
|
|
7935
8211
|
lines.push(
|
|
7936
|
-
` ${
|
|
8212
|
+
` ${chalk21.red("\u2717")} IAM role ${chalk21.bold(`biffo-github-actions-${s.projectName}`)}`
|
|
7937
8213
|
);
|
|
7938
8214
|
lines.push(
|
|
7939
|
-
` ${
|
|
8215
|
+
` ${chalk21.red("\u2717")} S3 bucket ${chalk21.bold(`${s.projectName}-terraform-state-${s.accountId}`)}`
|
|
7940
8216
|
);
|
|
7941
8217
|
if (s.pendingRegistrationPr !== void 0) {
|
|
7942
8218
|
lines.push(
|
|
7943
|
-
|
|
8219
|
+
chalk21.dim(` registration PR #${s.pendingRegistrationPr} is still open \u2014 never routed`)
|
|
7944
8220
|
);
|
|
7945
8221
|
}
|
|
7946
8222
|
}
|
|
@@ -7978,7 +8254,7 @@ async function confirmTeardown(projectName, options) {
|
|
|
7978
8254
|
{
|
|
7979
8255
|
type: "input",
|
|
7980
8256
|
name: "confirm",
|
|
7981
|
-
message: `Type ${
|
|
8257
|
+
message: `Type ${chalk21.bold(projectName)} to confirm:`
|
|
7982
8258
|
}
|
|
7983
8259
|
]);
|
|
7984
8260
|
return confirm === projectName;
|
|
@@ -7996,7 +8272,7 @@ function resolveGithubToken4() {
|
|
|
7996
8272
|
}
|
|
7997
8273
|
|
|
7998
8274
|
// src/index.ts
|
|
7999
|
-
var program = new
|
|
8275
|
+
var program = new Command25();
|
|
8000
8276
|
function cliVersion() {
|
|
8001
8277
|
try {
|
|
8002
8278
|
return getLatestCoreVersion();
|