@biffo/cli 0.34.0 → 0.34.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/core.version +1 -1
- package/dist/index.js +340 -3
- package/package.json +1 -1
package/core.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.34.
|
|
1
|
+
0.34.1
|
package/dist/index.js
CHANGED
|
@@ -697,12 +697,22 @@ var GitHubAdapter = class {
|
|
|
697
697
|
log.info(`Created branch ${branch} from ${from}`);
|
|
698
698
|
}
|
|
699
699
|
/**
|
|
700
|
-
* Read a file's decoded UTF-8 content at `ref
|
|
701
|
-
* (404) or is not a regular file (a
|
|
700
|
+
* Read a file's decoded UTF-8 content at `ref` (the repo's default branch when
|
|
701
|
+
* omitted), or `undefined` if it is absent (404) or is not a regular file (a
|
|
702
|
+
* directory or submodule).
|
|
703
|
+
*
|
|
704
|
+
* Public because `biffo teardown` reads the sibling registry and the sibling
|
|
705
|
+
* marker file straight from GitHub (issue #306) — teardown must work from a
|
|
706
|
+
* machine that never cloned the project.
|
|
702
707
|
*/
|
|
703
708
|
async getFileContent(org, repo, path, ref) {
|
|
704
709
|
try {
|
|
705
|
-
const { data } = await this.octokit.repos.getContent({
|
|
710
|
+
const { data } = await this.octokit.repos.getContent({
|
|
711
|
+
owner: org,
|
|
712
|
+
repo,
|
|
713
|
+
path,
|
|
714
|
+
...ref ? { ref } : {}
|
|
715
|
+
});
|
|
706
716
|
if (Array.isArray(data) || data.type !== "file" || typeof data.content !== "string") {
|
|
707
717
|
return void 0;
|
|
708
718
|
}
|
|
@@ -906,6 +916,37 @@ var GitHubAdapter = class {
|
|
|
906
916
|
}
|
|
907
917
|
}
|
|
908
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Does this repository exist (and is it visible to our token)?
|
|
921
|
+
*
|
|
922
|
+
* Used by `biffo teardown` to tell "sibling registered, repo since deleted"
|
|
923
|
+
* apart from "sibling repo still live", which decide different teardown paths.
|
|
924
|
+
*/
|
|
925
|
+
async repoExists(org, repo) {
|
|
926
|
+
try {
|
|
927
|
+
await this.octokit.repos.get({ owner: org, repo });
|
|
928
|
+
return true;
|
|
929
|
+
} catch (err) {
|
|
930
|
+
if (err.status === 404) return false;
|
|
931
|
+
throw err;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Head branch names of the repo's open pull requests.
|
|
936
|
+
*
|
|
937
|
+
* Teardown uses this to spot siblings whose registration PR has not merged
|
|
938
|
+
* yet (branch `biffo/register-sibling-<name>`) — they exist as real repos and
|
|
939
|
+
* real AWS resources, but nothing has landed in the merged registry.
|
|
940
|
+
*/
|
|
941
|
+
async listOpenPullRequests(org, repo) {
|
|
942
|
+
const prs = await this.octokit.paginate(this.octokit.pulls.list, {
|
|
943
|
+
owner: org,
|
|
944
|
+
repo,
|
|
945
|
+
state: "open",
|
|
946
|
+
per_page: 100
|
|
947
|
+
});
|
|
948
|
+
return prs.map((pr) => ({ number: pr.number, headRef: pr.head.ref }));
|
|
949
|
+
}
|
|
909
950
|
/**
|
|
910
951
|
* Read a repository Actions variable's value, or `undefined` if it isn't set.
|
|
911
952
|
* A 404 means the variable doesn't exist on the repo (not an error) — used to
|
|
@@ -5814,6 +5855,129 @@ import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STS
|
|
|
5814
5855
|
import chalk20 from "chalk";
|
|
5815
5856
|
import { Command as Command22 } from "commander";
|
|
5816
5857
|
import inquirer8 from "inquirer";
|
|
5858
|
+
|
|
5859
|
+
// src/lib/sibling-teardown.ts
|
|
5860
|
+
var SiblingResolutionError = class extends Error {
|
|
5861
|
+
constructor(message) {
|
|
5862
|
+
super(message);
|
|
5863
|
+
this.name = "SiblingResolutionError";
|
|
5864
|
+
}
|
|
5865
|
+
};
|
|
5866
|
+
var REGISTRY_ENVIRONMENTS = ["dev", "staging", "prod"];
|
|
5867
|
+
function registryPath(environment) {
|
|
5868
|
+
return `infra/environments/${environment}/siblings.auto.tfvars.json`;
|
|
5869
|
+
}
|
|
5870
|
+
var REGISTRATION_BRANCH_PREFIX = "biffo/register-sibling-";
|
|
5871
|
+
function parseSiblingBucketDomain(domain) {
|
|
5872
|
+
const host = /^(?<bucket>[a-z0-9][a-z0-9.-]*)\.s3(?:\.[a-z0-9-]+)?\.amazonaws\.com$/.exec(domain);
|
|
5873
|
+
const bucket = host?.groups?.["bucket"];
|
|
5874
|
+
if (!bucket) return null;
|
|
5875
|
+
const parts = /^(?<project>.+)-(?<env>dev|staging|prod)-site-(?<account>\d{12})$/.exec(bucket);
|
|
5876
|
+
const project = parts?.groups?.["project"];
|
|
5877
|
+
const env = parts?.groups?.["env"];
|
|
5878
|
+
const account = parts?.groups?.["account"];
|
|
5879
|
+
if (!project || !env || !account) return null;
|
|
5880
|
+
return { projectName: project, environment: env, accountId: account };
|
|
5881
|
+
}
|
|
5882
|
+
function parseRegistry(contents) {
|
|
5883
|
+
if (contents === void 0 || contents.trim() === "") return [];
|
|
5884
|
+
let parsed;
|
|
5885
|
+
try {
|
|
5886
|
+
parsed = JSON.parse(contents);
|
|
5887
|
+
} catch {
|
|
5888
|
+
throw new SiblingResolutionError(
|
|
5889
|
+
"A siblings.auto.tfvars.json in the core repo is not valid JSON. Refusing to tear down while the sibling registry cannot be read \u2014 fix or remove the file and re-run."
|
|
5890
|
+
);
|
|
5891
|
+
}
|
|
5892
|
+
const origins = parsed.sibling_origins;
|
|
5893
|
+
if (origins === void 0) return [];
|
|
5894
|
+
if (!Array.isArray(origins)) {
|
|
5895
|
+
throw new SiblingResolutionError(
|
|
5896
|
+
"sibling_origins in the core repo is not a list. Refusing to tear down while the sibling registry cannot be read."
|
|
5897
|
+
);
|
|
5898
|
+
}
|
|
5899
|
+
return origins;
|
|
5900
|
+
}
|
|
5901
|
+
function collectSiblings(sources) {
|
|
5902
|
+
const byPrefix = /* @__PURE__ */ new Map();
|
|
5903
|
+
for (const source of sources) {
|
|
5904
|
+
for (const entry of source.entries) {
|
|
5905
|
+
if (!entry?.name || !entry.bucket_regional_domain) {
|
|
5906
|
+
throw new SiblingResolutionError(
|
|
5907
|
+
`A sibling entry in ${registryPath(source.environment)} is missing name or bucket_regional_domain. Refusing to tear down while a registered sibling cannot be identified \u2014 it would be left behind, still billing.`
|
|
5908
|
+
);
|
|
5909
|
+
}
|
|
5910
|
+
const parsed = parseSiblingBucketDomain(entry.bucket_regional_domain);
|
|
5911
|
+
if (!parsed) {
|
|
5912
|
+
throw new SiblingResolutionError(
|
|
5913
|
+
`Could not work out which project owns the sibling "${entry.name}" from its bucket "${entry.bucket_regional_domain}" (in ${registryPath(source.environment)}).
|
|
5914
|
+
Refusing to guess \u2014 tear that sibling down by hand, remove its entry, then re-run.`
|
|
5915
|
+
);
|
|
5916
|
+
}
|
|
5917
|
+
const existing = byPrefix.get(entry.name);
|
|
5918
|
+
if (existing) {
|
|
5919
|
+
if (existing.projectName !== parsed.projectName) {
|
|
5920
|
+
throw new SiblingResolutionError(
|
|
5921
|
+
`The sibling "${entry.name}" maps to two different projects across environments ("${existing.projectName}" and "${parsed.projectName}"). Refusing to guess which repo to delete \u2014 resolve the registry by hand and re-run.`
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
if (!existing.environments.includes(parsed.environment)) {
|
|
5925
|
+
existing.environments.push(parsed.environment);
|
|
5926
|
+
}
|
|
5927
|
+
if (source.pendingRegistrationPr === void 0) {
|
|
5928
|
+
existing.registered = true;
|
|
5929
|
+
delete existing.pendingRegistrationPr;
|
|
5930
|
+
}
|
|
5931
|
+
continue;
|
|
5932
|
+
}
|
|
5933
|
+
byPrefix.set(entry.name, {
|
|
5934
|
+
pathPrefix: entry.name,
|
|
5935
|
+
projectName: parsed.projectName,
|
|
5936
|
+
environments: [parsed.environment],
|
|
5937
|
+
accountId: parsed.accountId,
|
|
5938
|
+
registered: source.pendingRegistrationPr === void 0,
|
|
5939
|
+
...source.pendingRegistrationPr !== void 0 ? { pendingRegistrationPr: source.pendingRegistrationPr } : {}
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
return [...byPrefix.values()].sort((a, b) => a.pathPrefix.localeCompare(b.pathPrefix));
|
|
5944
|
+
}
|
|
5945
|
+
var SIBLING_MARKER_FILE = "biffo.sibling.json";
|
|
5946
|
+
function markerMatches(marker, coreProjectName, sibling) {
|
|
5947
|
+
if (!marker) return false;
|
|
5948
|
+
if (marker.core_project !== coreProjectName) return false;
|
|
5949
|
+
return marker.path_prefix === sibling.pathPrefix || marker.name === sibling.projectName;
|
|
5950
|
+
}
|
|
5951
|
+
async function resolveSiblingRepos(github, coreOrg, coreProjectName, siblings) {
|
|
5952
|
+
const resolved = [];
|
|
5953
|
+
for (const sibling of siblings) {
|
|
5954
|
+
const repo = sibling.projectName;
|
|
5955
|
+
if (!await github.repoExists(coreOrg, repo)) {
|
|
5956
|
+
resolved.push({ ...sibling, org: coreOrg, repo, repoState: "gone" });
|
|
5957
|
+
continue;
|
|
5958
|
+
}
|
|
5959
|
+
const raw = await github.getFileContent(coreOrg, repo, SIBLING_MARKER_FILE);
|
|
5960
|
+
let marker = null;
|
|
5961
|
+
if (raw !== void 0) {
|
|
5962
|
+
try {
|
|
5963
|
+
marker = JSON.parse(raw);
|
|
5964
|
+
} catch {
|
|
5965
|
+
marker = null;
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
if (!markerMatches(marker, coreProjectName, sibling)) {
|
|
5969
|
+
throw new SiblingResolutionError(
|
|
5970
|
+
`Refusing to tear down: ${coreOrg}/${repo} is registered as the sibling "${sibling.pathPrefix}" of "${coreProjectName}", but that repo does not carry a matching ${SIBLING_MARKER_FILE}.
|
|
5971
|
+
It may be an unrelated repo that happens to share the name. Deleting a repo cannot be undone, so nothing has been deleted.
|
|
5972
|
+
Delete ${coreOrg}/${repo} by hand if it really is the sibling, remove its entry from the core repo's siblings.auto.tfvars.json, then re-run biffo teardown.`
|
|
5973
|
+
);
|
|
5974
|
+
}
|
|
5975
|
+
resolved.push({ ...sibling, org: coreOrg, repo, repoState: "present" });
|
|
5976
|
+
}
|
|
5977
|
+
return resolved;
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
// src/commands/teardown.ts
|
|
5817
5981
|
var teardownCommand = new Command22("teardown").description(
|
|
5818
5982
|
"Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
|
|
5819
5983
|
).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(
|
|
@@ -5912,6 +6076,21 @@ var teardownCommand = new Command22("teardown").description(
|
|
|
5912
6076
|
const allDeployed = options.skipDestroy ? [] : await aws.listDeployedEnvironments(stateBucket).catch(() => []);
|
|
5913
6077
|
const deployedEnvs = allDeployed.filter((e) => e !== "global");
|
|
5914
6078
|
const hasGlobal = allDeployed.includes("global");
|
|
6079
|
+
let siblings = [];
|
|
6080
|
+
if (org && repo) {
|
|
6081
|
+
try {
|
|
6082
|
+
siblings = await discoverSiblings(github, org, repo, projectName);
|
|
6083
|
+
if (!options.skipDestroy) {
|
|
6084
|
+
await assertSiblingsAreDestroyable(github, siblings);
|
|
6085
|
+
}
|
|
6086
|
+
} catch (err) {
|
|
6087
|
+
if (err instanceof SiblingResolutionError) {
|
|
6088
|
+
log.error(err.message);
|
|
6089
|
+
process.exit(1);
|
|
6090
|
+
}
|
|
6091
|
+
throw err;
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
5915
6094
|
console.log(chalk20.red.bold(" This will permanently delete:\n"));
|
|
5916
6095
|
if (deployedEnvs.length > 0 || hasGlobal) {
|
|
5917
6096
|
console.log(chalk20.red(" Infrastructure (via GitHub Actions terraform destroy):"));
|
|
@@ -5925,6 +6104,9 @@ var teardownCommand = new Command22("teardown").description(
|
|
|
5925
6104
|
}
|
|
5926
6105
|
console.log();
|
|
5927
6106
|
}
|
|
6107
|
+
for (const line of formatSiblingPlan(siblings, options.skipDestroy === true)) {
|
|
6108
|
+
console.log(line);
|
|
6109
|
+
}
|
|
5928
6110
|
console.log(chalk20.red(" Biffo resources:"));
|
|
5929
6111
|
console.log(` ${chalk20.red("\u2717")} GitHub repository ${chalk20.bold(`${org}/${repo}`)}`);
|
|
5930
6112
|
console.log(
|
|
@@ -5934,11 +6116,22 @@ var teardownCommand = new Command22("teardown").description(
|
|
|
5934
6116
|
` ${chalk20.red("\u2717")} S3 bucket ${chalk20.bold(stateBucket)} (all versions)`
|
|
5935
6117
|
);
|
|
5936
6118
|
console.log(` ${chalk20.red("\u2717")} Local session file`);
|
|
6119
|
+
if (options.skipDestroy) {
|
|
6120
|
+
console.log();
|
|
6121
|
+
console.log(
|
|
6122
|
+
chalk20.yellow(
|
|
6123
|
+
" --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."
|
|
6124
|
+
)
|
|
6125
|
+
);
|
|
6126
|
+
}
|
|
5937
6127
|
console.log();
|
|
5938
6128
|
if (!await confirmTeardown(projectName, options)) {
|
|
5939
6129
|
log.warn("Teardown cancelled \u2014 project name did not match");
|
|
5940
6130
|
return;
|
|
5941
6131
|
}
|
|
6132
|
+
if (!options.skipDestroy && siblings.length > 0) {
|
|
6133
|
+
await destroySiblingInfrastructure(github, siblings);
|
|
6134
|
+
}
|
|
5942
6135
|
if (deployedEnvs.length > 0) {
|
|
5943
6136
|
const actionsUrl = `https://github.com/${org}/${repo}/actions`;
|
|
5944
6137
|
for (const env of deployedEnvs) {
|
|
@@ -6006,6 +6199,32 @@ Destroying ${env} infrastructure (20\u201330 min for first run)...`);
|
|
|
6006
6199
|
log.warn("destroy-global.yml not found \u2014 skipping global infrastructure teardown");
|
|
6007
6200
|
}
|
|
6008
6201
|
}
|
|
6202
|
+
for (const sibling of siblings) {
|
|
6203
|
+
if (sibling.repoState === "gone") {
|
|
6204
|
+
log.warn(
|
|
6205
|
+
`Sibling "${sibling.pathPrefix}" (${sibling.org}/${sibling.repo}) no longer has a repo \u2014 leaving its IAM role and Terraform state bucket in place so its infrastructure can still be reclaimed:
|
|
6206
|
+
IAM role biffo-github-actions-${sibling.projectName}
|
|
6207
|
+
S3 bucket ${sibling.projectName}-terraform-state-${sibling.accountId}`
|
|
6208
|
+
);
|
|
6209
|
+
continue;
|
|
6210
|
+
}
|
|
6211
|
+
await github.deleteRepo(sibling.org, sibling.repo).catch((err) => {
|
|
6212
|
+
log.warn(
|
|
6213
|
+
`Could not delete sibling repo ${sibling.org}/${sibling.repo} (skipping): ${err.message}`
|
|
6214
|
+
);
|
|
6215
|
+
});
|
|
6216
|
+
await aws.teardownOidcRole(sibling.projectName).catch((err) => {
|
|
6217
|
+
log.warn(
|
|
6218
|
+
`Could not delete sibling IAM role for ${sibling.projectName} (skipping): ${err.message}`
|
|
6219
|
+
);
|
|
6220
|
+
});
|
|
6221
|
+
await aws.teardownTerraformBackend(sibling.projectName).catch((err) => {
|
|
6222
|
+
log.warn(
|
|
6223
|
+
`Could not delete sibling state bucket for ${sibling.projectName} (skipping): ${err.message}`
|
|
6224
|
+
);
|
|
6225
|
+
});
|
|
6226
|
+
log.success(`Sibling ${sibling.org}/${sibling.repo} removed`);
|
|
6227
|
+
}
|
|
6009
6228
|
await github.deleteRepo(org, repo).catch((err) => {
|
|
6010
6229
|
log.warn(`Could not delete repo (skipping): ${err.message}`);
|
|
6011
6230
|
});
|
|
@@ -6021,6 +6240,124 @@ Destroying ${env} infrastructure (20\u201330 min for first run)...`);
|
|
|
6021
6240
|
console.log(" All biffo resources have been removed.\n");
|
|
6022
6241
|
}
|
|
6023
6242
|
);
|
|
6243
|
+
async function destroySiblingInfrastructure(github, siblings) {
|
|
6244
|
+
for (const sibling of siblings) {
|
|
6245
|
+
if (sibling.repoState !== "present") continue;
|
|
6246
|
+
const actionsUrl = `https://github.com/${sibling.org}/${sibling.repo}/actions`;
|
|
6247
|
+
for (const env of sibling.environments) {
|
|
6248
|
+
const envBranch = { dev: "dev", staging: "staging", prod: "main" };
|
|
6249
|
+
const branch = envBranch[env] ?? "dev";
|
|
6250
|
+
log.info(`
|
|
6251
|
+
Destroying sibling ${sibling.org}/${sibling.repo} (${env})...`);
|
|
6252
|
+
log.info(` Watch live: ${actionsUrl}`);
|
|
6253
|
+
const baselineId = await github.getLatestWorkflowRunId(
|
|
6254
|
+
sibling.org,
|
|
6255
|
+
sibling.repo,
|
|
6256
|
+
SIBLING_DESTROY_WORKFLOW
|
|
6257
|
+
);
|
|
6258
|
+
await github.triggerWorkflow(
|
|
6259
|
+
sibling.org,
|
|
6260
|
+
sibling.repo,
|
|
6261
|
+
SIBLING_DESTROY_WORKFLOW,
|
|
6262
|
+
{ environment: env },
|
|
6263
|
+
branch
|
|
6264
|
+
);
|
|
6265
|
+
const result = await github.waitForWorkflowRun(
|
|
6266
|
+
sibling.org,
|
|
6267
|
+
sibling.repo,
|
|
6268
|
+
SIBLING_DESTROY_WORKFLOW,
|
|
6269
|
+
baselineId,
|
|
6270
|
+
36e5,
|
|
6271
|
+
3e4,
|
|
6272
|
+
branch
|
|
6273
|
+
);
|
|
6274
|
+
if (result.conclusion !== "success") {
|
|
6275
|
+
log.error(
|
|
6276
|
+
`Sibling destroy ${result.conclusion ?? "failed"} for ${sibling.org}/${sibling.repo} (${env}).`
|
|
6277
|
+
);
|
|
6278
|
+
log.error(` Run details: ${actionsUrl}/runs/${result.id}`);
|
|
6279
|
+
log.error(
|
|
6280
|
+
" Nothing else has been destroyed or deleted \u2014 this project and every other sibling are still intact.\n Fix the sibling and re-run biffo teardown, or use --skip-destroy."
|
|
6281
|
+
);
|
|
6282
|
+
process.exit(1);
|
|
6283
|
+
}
|
|
6284
|
+
log.success(`Sibling ${sibling.org}/${sibling.repo} (${env}) infrastructure destroyed`);
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
var SIBLING_DESTROY_WORKFLOW = "destroy-infra.yml";
|
|
6289
|
+
var SIBLING_DESTROY_WORKFLOW_PATH = `.github/workflows/${SIBLING_DESTROY_WORKFLOW}`;
|
|
6290
|
+
async function discoverSiblings(github, coreOrg, coreRepo, coreProjectName) {
|
|
6291
|
+
const sources = [];
|
|
6292
|
+
for (const env of REGISTRY_ENVIRONMENTS) {
|
|
6293
|
+
const contents = await github.getFileContent(coreOrg, coreRepo, registryPath(env));
|
|
6294
|
+
sources.push({ environment: env, entries: parseRegistry(contents) });
|
|
6295
|
+
}
|
|
6296
|
+
const openPrs = await github.listOpenPullRequests(coreOrg, coreRepo);
|
|
6297
|
+
for (const pr of openPrs) {
|
|
6298
|
+
if (!pr.headRef.startsWith(REGISTRATION_BRANCH_PREFIX)) continue;
|
|
6299
|
+
for (const env of REGISTRY_ENVIRONMENTS) {
|
|
6300
|
+
const contents = await github.getFileContent(coreOrg, coreRepo, registryPath(env), pr.headRef);
|
|
6301
|
+
sources.push({
|
|
6302
|
+
environment: env,
|
|
6303
|
+
entries: parseRegistry(contents),
|
|
6304
|
+
pendingRegistrationPr: pr.number
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
const discovered = collectSiblings(sources);
|
|
6309
|
+
return resolveSiblingRepos(github, coreOrg, coreProjectName, discovered);
|
|
6310
|
+
}
|
|
6311
|
+
async function assertSiblingsAreDestroyable(github, siblings) {
|
|
6312
|
+
const missing = [];
|
|
6313
|
+
for (const sibling of siblings) {
|
|
6314
|
+
if (sibling.repoState !== "present") continue;
|
|
6315
|
+
const workflow = await github.getFileContent(
|
|
6316
|
+
sibling.org,
|
|
6317
|
+
sibling.repo,
|
|
6318
|
+
SIBLING_DESTROY_WORKFLOW_PATH
|
|
6319
|
+
);
|
|
6320
|
+
if (workflow === void 0) missing.push(`${sibling.org}/${sibling.repo}`);
|
|
6321
|
+
}
|
|
6322
|
+
if (missing.length > 0) {
|
|
6323
|
+
throw new SiblingResolutionError(
|
|
6324
|
+
`These sibling repos have no ${SIBLING_DESTROY_WORKFLOW_PATH}, so their AWS resources cannot be destroyed:
|
|
6325
|
+
` + missing.map((r) => ` ${r}`).join("\n") + "\n Nothing has been deleted. Either add that workflow to each repo (copy it from biffo's sibling skeleton), destroy those siblings by hand, or re-run with --skip-destroy to delete the repos and leave their infrastructure standing."
|
|
6326
|
+
);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
function formatSiblingPlan(siblings, skipDestroy) {
|
|
6330
|
+
if (siblings.length === 0) return [];
|
|
6331
|
+
const lines = [chalk20.red(` Sibling apps (${siblings.length}) \u2014 ADR-0007:`)];
|
|
6332
|
+
for (const s of siblings) {
|
|
6333
|
+
const envs = s.environments.join(", ");
|
|
6334
|
+
if (s.repoState === "gone") {
|
|
6335
|
+
lines.push(
|
|
6336
|
+
` ${chalk20.yellow("!")} ${chalk20.bold(`${s.org}/${s.repo}`)} \u2014 repo already deleted; its ${envs} infrastructure CANNOT be destroyed and will be left standing`
|
|
6337
|
+
);
|
|
6338
|
+
continue;
|
|
6339
|
+
}
|
|
6340
|
+
lines.push(
|
|
6341
|
+
` ${chalk20.red("\u2717")} GitHub repository ${chalk20.bold(`${s.org}/${s.repo}`)} (routed at /${s.pathPrefix})`
|
|
6342
|
+
);
|
|
6343
|
+
lines.push(
|
|
6344
|
+
` ${chalk20.red("\u2717")} ${skipDestroy ? "infrastructure NOT destroyed (--skip-destroy)" : `${envs} infrastructure \u2014 S3 site bucket, Lambda, API Gateway`}`
|
|
6345
|
+
);
|
|
6346
|
+
lines.push(
|
|
6347
|
+
` ${chalk20.red("\u2717")} IAM role ${chalk20.bold(`biffo-github-actions-${s.projectName}`)}`
|
|
6348
|
+
);
|
|
6349
|
+
lines.push(
|
|
6350
|
+
` ${chalk20.red("\u2717")} S3 bucket ${chalk20.bold(`${s.projectName}-terraform-state-${s.accountId}`)}`
|
|
6351
|
+
);
|
|
6352
|
+
if (s.pendingRegistrationPr !== void 0) {
|
|
6353
|
+
lines.push(
|
|
6354
|
+
chalk20.dim(` registration PR #${s.pendingRegistrationPr} is still open \u2014 never routed`)
|
|
6355
|
+
);
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
6358
|
+
lines.push("");
|
|
6359
|
+
return lines;
|
|
6360
|
+
}
|
|
6024
6361
|
function pickSoleProjectConfig() {
|
|
6025
6362
|
const configs = listProjectConfigs();
|
|
6026
6363
|
if (configs.length > 1 && isNonInteractive()) {
|