@biffo/cli 0.166.1 → 0.167.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 +289 -36
- 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 Command26 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/core.ts
|
|
7
7
|
import { Command as Command4 } from "commander";
|
|
@@ -948,6 +948,47 @@ var GitAdapter = class {
|
|
|
948
948
|
async fetchPrune(cwd, remote = "origin") {
|
|
949
949
|
await execa2("git", ["fetch", "--quiet", "--prune", remote], { cwd, reject: false });
|
|
950
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Worktrees other than the primary, with the branch each is on (#797).
|
|
953
|
+
*
|
|
954
|
+
* `--porcelain` rather than the human format: the latter's alignment and
|
|
955
|
+
* annotations vary, and this has to survive paths with spaces.
|
|
956
|
+
*/
|
|
957
|
+
async listWorktrees(cwd) {
|
|
958
|
+
const { stdout, exitCode } = await execa2("git", ["worktree", "list", "--porcelain"], {
|
|
959
|
+
cwd,
|
|
960
|
+
reject: false
|
|
961
|
+
});
|
|
962
|
+
if (exitCode !== 0) return [];
|
|
963
|
+
const out = [];
|
|
964
|
+
let path = "";
|
|
965
|
+
for (const line of stdout.split("\n")) {
|
|
966
|
+
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
967
|
+
else if (line.startsWith("branch ")) {
|
|
968
|
+
const branch = line.slice("branch ".length).replace("refs/heads/", "");
|
|
969
|
+
if (out.length > 0 || path !== cwd) out.push({ path, branch });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return out.filter((w) => w.path !== cwd);
|
|
973
|
+
}
|
|
974
|
+
/** How many commits `branch` is behind `base`; null when it cannot be measured. */
|
|
975
|
+
async countBehind(cwd, branch, base) {
|
|
976
|
+
const { stdout, exitCode } = await execa2("git", ["rev-list", "--count", `${branch}..${base}`], {
|
|
977
|
+
cwd,
|
|
978
|
+
reject: false
|
|
979
|
+
});
|
|
980
|
+
if (exitCode !== 0) return null;
|
|
981
|
+
const n = Number.parseInt(stdout.trim(), 10);
|
|
982
|
+
return Number.isNaN(n) ? null : n;
|
|
983
|
+
}
|
|
984
|
+
/** A file's contents at a ref, or null when it is absent there. */
|
|
985
|
+
async showFileAtRef(cwd, ref, path) {
|
|
986
|
+
const { stdout, exitCode } = await execa2("git", ["show", `${ref}:${path}`], {
|
|
987
|
+
cwd,
|
|
988
|
+
reject: false
|
|
989
|
+
});
|
|
990
|
+
return exitCode === 0 ? stdout : null;
|
|
991
|
+
}
|
|
951
992
|
/** Every local branch with its upstream and tracking state (#758). */
|
|
952
993
|
async listBranchRefs(cwd) {
|
|
953
994
|
const { stdout, exitCode } = await execa2(
|
|
@@ -1280,7 +1321,7 @@ var GitHubAdapter = class {
|
|
|
1280
1321
|
} catch (err) {
|
|
1281
1322
|
if (err.status !== 404) throw err;
|
|
1282
1323
|
}
|
|
1283
|
-
await new Promise((
|
|
1324
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1284
1325
|
}
|
|
1285
1326
|
throw new Error(
|
|
1286
1327
|
`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.`
|
|
@@ -1295,7 +1336,7 @@ var GitHubAdapter = class {
|
|
|
1295
1336
|
} catch (err) {
|
|
1296
1337
|
if (err.status !== 404) throw err;
|
|
1297
1338
|
}
|
|
1298
|
-
await new Promise((
|
|
1339
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1299
1340
|
}
|
|
1300
1341
|
throw new Error(
|
|
1301
1342
|
`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.`
|
|
@@ -1500,7 +1541,7 @@ var GitHubAdapter = class {
|
|
|
1500
1541
|
}
|
|
1501
1542
|
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1502
1543
|
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1503
|
-
await new Promise((
|
|
1544
|
+
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1504
1545
|
}
|
|
1505
1546
|
}
|
|
1506
1547
|
}
|
|
@@ -1563,7 +1604,7 @@ var GitHubAdapter = class {
|
|
|
1563
1604
|
}
|
|
1564
1605
|
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1565
1606
|
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1566
|
-
await new Promise((
|
|
1607
|
+
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1567
1608
|
}
|
|
1568
1609
|
}
|
|
1569
1610
|
log.success(
|
|
@@ -1818,7 +1859,7 @@ var GitHubAdapter = class {
|
|
|
1818
1859
|
} catch (err) {
|
|
1819
1860
|
if (err.status !== 404 || Date.now() >= deadline) throw err;
|
|
1820
1861
|
log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
|
|
1821
|
-
await new Promise((
|
|
1862
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1822
1863
|
}
|
|
1823
1864
|
}
|
|
1824
1865
|
}
|
|
@@ -1837,7 +1878,7 @@ var GitHubAdapter = class {
|
|
|
1837
1878
|
} catch (err) {
|
|
1838
1879
|
if (err.status !== 404 || Date.now() >= deadline) throw err;
|
|
1839
1880
|
log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
|
|
1840
|
-
await new Promise((
|
|
1881
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1841
1882
|
}
|
|
1842
1883
|
}
|
|
1843
1884
|
}
|
|
@@ -1861,7 +1902,7 @@ var GitHubAdapter = class {
|
|
|
1861
1902
|
} else {
|
|
1862
1903
|
log.info(" Waiting for run to be queued...");
|
|
1863
1904
|
}
|
|
1864
|
-
await new Promise((
|
|
1905
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1865
1906
|
}
|
|
1866
1907
|
throw new Error(
|
|
1867
1908
|
`Workflow ${workflowId} did not complete within ${timeoutMs / 1e3 / 60} minutes`
|
|
@@ -3332,7 +3373,7 @@ var AwsAdapter = class {
|
|
|
3332
3373
|
const code = err.Code;
|
|
3333
3374
|
if (code === "OperationAborted" && attempt < maxAttempts) {
|
|
3334
3375
|
log.info(` Waiting for S3 to release "${bucketName}"... (${attempt}/${maxAttempts})`);
|
|
3335
|
-
await new Promise((
|
|
3376
|
+
await new Promise((resolve19) => setTimeout(resolve19, retryDelayMs));
|
|
3336
3377
|
} else if (code === "OperationAborted") {
|
|
3337
3378
|
return false;
|
|
3338
3379
|
} else {
|
|
@@ -4566,6 +4607,7 @@ async function readRemoteCoreVersion(github, org, repo, ref) {
|
|
|
4566
4607
|
}
|
|
4567
4608
|
} catch {
|
|
4568
4609
|
}
|
|
4610
|
+
return null;
|
|
4569
4611
|
}
|
|
4570
4612
|
const inherited = await github.getFileContent(org, repo, CORE_VERSION_FILE, ref);
|
|
4571
4613
|
if (inherited) {
|
|
@@ -8678,8 +8720,8 @@ async function runOwnershipCheck(argv) {
|
|
|
8678
8720
|
const { stdout } = await execa6("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
8679
8721
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
8680
8722
|
if (messageFile) {
|
|
8681
|
-
const { readFileSync:
|
|
8682
|
-
if (
|
|
8723
|
+
const { readFileSync: readFileSync26, existsSync: existsSync35 } = await import("fs");
|
|
8724
|
+
if (existsSync35(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
|
|
8683
8725
|
}
|
|
8684
8726
|
} else {
|
|
8685
8727
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -9071,20 +9113,230 @@ function rawArgsAfter(subcommand) {
|
|
|
9071
9113
|
return at === -1 ? [] : process.argv.slice(at + 1);
|
|
9072
9114
|
}
|
|
9073
9115
|
|
|
9116
|
+
// src/commands/doctor.ts
|
|
9117
|
+
import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
|
|
9118
|
+
import { join as join34, resolve as resolve18 } from "path";
|
|
9119
|
+
import chalk21 from "chalk";
|
|
9120
|
+
import { Command as Command24 } from "commander";
|
|
9121
|
+
|
|
9122
|
+
// src/lib/doctor.ts
|
|
9123
|
+
function checkCheckoutCurrency(facts) {
|
|
9124
|
+
const findings = [];
|
|
9125
|
+
if (facts.currentBranch === "HEAD" || facts.currentBranch === "") {
|
|
9126
|
+
findings.push({
|
|
9127
|
+
check: "checkout-detached",
|
|
9128
|
+
severity: "error",
|
|
9129
|
+
detail: "The primary checkout has a detached HEAD, so nothing read from it can be attributed to a branch.",
|
|
9130
|
+
remedy: `git switch ${facts.integrationBranch}`
|
|
9131
|
+
});
|
|
9132
|
+
return findings;
|
|
9133
|
+
}
|
|
9134
|
+
if (facts.currentBranch !== facts.integrationBranch) {
|
|
9135
|
+
findings.push({
|
|
9136
|
+
check: "checkout-off-integration",
|
|
9137
|
+
severity: "error",
|
|
9138
|
+
detail: `The primary checkout is on '${facts.currentBranch}', not '${facts.integrationBranch}'. Anything read from it \u2014 versions, migrations, whether a feature exists \u2014 describes that branch, not the repo. This is how an audit gets run against dead code (AGENTS.md \xA72).`,
|
|
9139
|
+
remedy: `git switch ${facts.integrationBranch} && git pull (do the work in a worktree instead)`
|
|
9140
|
+
});
|
|
9141
|
+
}
|
|
9142
|
+
if (facts.hasUpstream && facts.behind > 0) {
|
|
9143
|
+
const diverged = facts.ahead > 0 ? `, and ${String(facts.ahead)} ahead (diverged)` : "";
|
|
9144
|
+
findings.push({
|
|
9145
|
+
check: "checkout-behind",
|
|
9146
|
+
severity: "error",
|
|
9147
|
+
detail: `The primary checkout is ${String(facts.behind)} commit(s) behind its upstream${diverged}. Every file read from it may be stale, including the ones that look like authoritative state.`,
|
|
9148
|
+
remedy: "git pull --ff-only"
|
|
9149
|
+
});
|
|
9150
|
+
}
|
|
9151
|
+
return findings;
|
|
9152
|
+
}
|
|
9153
|
+
function checkCoreVersionCurrency(facts) {
|
|
9154
|
+
if (facts.localCoreVersion === null || facts.remoteCoreVersion === null) return [];
|
|
9155
|
+
if (facts.localCoreVersion === facts.remoteCoreVersion) return [];
|
|
9156
|
+
return [
|
|
9157
|
+
{
|
|
9158
|
+
check: "core-version-stale",
|
|
9159
|
+
severity: "error",
|
|
9160
|
+
detail: `This checkout records core ${facts.localCoreVersion}, but ${facts.integrationBranch} carries ${facts.remoteCoreVersion}. Sizing an upgrade or reasoning about which fixes this instance has from the local number will be wrong.`,
|
|
9161
|
+
remedy: "git pull --ff-only, then re-read biffo.core.json"
|
|
9162
|
+
}
|
|
9163
|
+
];
|
|
9164
|
+
}
|
|
9165
|
+
function checkFossilCoreVersion(facts) {
|
|
9166
|
+
if (facts.fossilCoreVersion === null || facts.localCoreVersion === null) return [];
|
|
9167
|
+
if (facts.fossilCoreVersion === facts.localCoreVersion) return [];
|
|
9168
|
+
return [
|
|
9169
|
+
{
|
|
9170
|
+
check: "fossil-core-version",
|
|
9171
|
+
severity: "warn",
|
|
9172
|
+
detail: `core.version says ${facts.fossilCoreVersion} while biffo.core.json says ${facts.localCoreVersion}. core.version is inherited from \`biffo init\` and has not been maintained since; biffo.core.json is the authority. Reading the wrong one is #788.`,
|
|
9173
|
+
remedy: "Ignore core.version. `biffo core upgrade` removes it when it can prove it is inherited, and deliberately keeps it when it may have been repurposed."
|
|
9174
|
+
}
|
|
9175
|
+
];
|
|
9176
|
+
}
|
|
9177
|
+
function checkStaleBranches(facts) {
|
|
9178
|
+
const gone = facts.branches.filter((b) => b.name !== facts.currentBranch && b.track.includes("gone")).map((b) => b.name);
|
|
9179
|
+
if (gone.length === 0) return [];
|
|
9180
|
+
const upgradeOnes = gone.filter((n) => n.startsWith(UPGRADE_BRANCH_PREFIX)).length;
|
|
9181
|
+
const note = upgradeOnes > 0 ? ` (${String(upgradeOnes)} from core upgrades)` : "";
|
|
9182
|
+
return [
|
|
9183
|
+
{
|
|
9184
|
+
check: "stale-branches",
|
|
9185
|
+
severity: "warn",
|
|
9186
|
+
detail: `${String(gone.length)} local branch(es) have a gone upstream${note} \u2014 their remote copy was deleted, which happens on merge. Squash merges mean \`git branch -d\` refuses them and \`--merged\` never lists them, so they accumulate silently (#758).`,
|
|
9187
|
+
remedy: `git branch -D ${gone.slice(0, 3).join(" ")}${gone.length > 3 ? " \u2026" : ""}`
|
|
9188
|
+
}
|
|
9189
|
+
];
|
|
9190
|
+
}
|
|
9191
|
+
function checkWorktrees(facts, behindThreshold = 50) {
|
|
9192
|
+
const findings = [];
|
|
9193
|
+
const goneBranches = new Set(
|
|
9194
|
+
facts.branches.filter((b) => b.track.includes("gone")).map((b) => b.name)
|
|
9195
|
+
);
|
|
9196
|
+
const merged = facts.worktrees.filter((w) => goneBranches.has(w.branch));
|
|
9197
|
+
if (merged.length > 0) {
|
|
9198
|
+
findings.push({
|
|
9199
|
+
check: "worktree-merged",
|
|
9200
|
+
severity: "warn",
|
|
9201
|
+
detail: `${String(merged.length)} worktree(s) sit on a branch whose remote copy is gone, i.e. whose PR has merged: ${merged.map((w) => w.path).join(", ")}.`,
|
|
9202
|
+
remedy: "git worktree remove <path>"
|
|
9203
|
+
});
|
|
9204
|
+
}
|
|
9205
|
+
const ancient = facts.worktrees.filter(
|
|
9206
|
+
(w) => !goneBranches.has(w.branch) && w.behind !== null && w.behind >= behindThreshold
|
|
9207
|
+
);
|
|
9208
|
+
if (ancient.length > 0) {
|
|
9209
|
+
findings.push({
|
|
9210
|
+
check: "worktree-stale",
|
|
9211
|
+
severity: "warn",
|
|
9212
|
+
detail: `${String(ancient.length)} worktree(s) are more than ${String(behindThreshold)} commits behind ${facts.integrationBranch}: ${ancient.map((w) => `${w.path} (${String(w.behind)})`).join(", ")}. Reading one of these describes the repo as it was, not as it is.`,
|
|
9213
|
+
remedy: "git worktree remove <path>, or rebase it onto the integration branch"
|
|
9214
|
+
});
|
|
9215
|
+
}
|
|
9216
|
+
return findings;
|
|
9217
|
+
}
|
|
9218
|
+
function runDoctorChecks(facts) {
|
|
9219
|
+
return [
|
|
9220
|
+
...checkCheckoutCurrency(facts),
|
|
9221
|
+
...checkCoreVersionCurrency(facts),
|
|
9222
|
+
...checkFossilCoreVersion(facts),
|
|
9223
|
+
...checkStaleBranches(facts),
|
|
9224
|
+
...checkWorktrees(facts)
|
|
9225
|
+
];
|
|
9226
|
+
}
|
|
9227
|
+
|
|
9228
|
+
// src/commands/doctor.ts
|
|
9229
|
+
var INTEGRATION_BRANCH = "dev";
|
|
9230
|
+
var doctorCommand = new Command24("doctor").description(
|
|
9231
|
+
"Report repo-state conditions that make everything read from this checkout unreliable"
|
|
9232
|
+
).option("--cwd <path>", "Repo root to inspect (defaults to the current directory)").option("--no-fetch", "Skip the fetch; report against refs as they already are locally").action(async (options) => {
|
|
9233
|
+
const cwd = options.cwd ? resolve18(options.cwd) : process.cwd();
|
|
9234
|
+
try {
|
|
9235
|
+
const findings = await runDoctor({ cwd, fetch: options.fetch !== false });
|
|
9236
|
+
printFindings(findings);
|
|
9237
|
+
if (findings.some((f) => f.severity === "error")) process.exit(1);
|
|
9238
|
+
} catch (err) {
|
|
9239
|
+
log.error(err.message);
|
|
9240
|
+
process.exit(1);
|
|
9241
|
+
}
|
|
9242
|
+
});
|
|
9243
|
+
async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
9244
|
+
const { git } = deps;
|
|
9245
|
+
if (!await git.isGitRepo(options.cwd)) {
|
|
9246
|
+
throw new Error(`${options.cwd} is not a git repository.`);
|
|
9247
|
+
}
|
|
9248
|
+
if (options.fetch) await git.fetchPrune(options.cwd);
|
|
9249
|
+
const currentBranch = await git.currentBranch(options.cwd);
|
|
9250
|
+
const { ahead, behind, hasUpstream } = await git.aheadBehind(options.cwd);
|
|
9251
|
+
const branches = await git.listBranchRefs(options.cwd);
|
|
9252
|
+
const worktreePaths = await git.listWorktrees(options.cwd);
|
|
9253
|
+
const worktrees = await Promise.all(
|
|
9254
|
+
worktreePaths.map(async (w) => ({
|
|
9255
|
+
...w,
|
|
9256
|
+
behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH}`)
|
|
9257
|
+
}))
|
|
9258
|
+
);
|
|
9259
|
+
const facts = {
|
|
9260
|
+
currentBranch,
|
|
9261
|
+
integrationBranch: INTEGRATION_BRANCH,
|
|
9262
|
+
ahead,
|
|
9263
|
+
behind,
|
|
9264
|
+
hasUpstream,
|
|
9265
|
+
localCoreVersion: readLocalCoreVersion(options.cwd),
|
|
9266
|
+
remoteCoreVersion: parseCoreRecord(
|
|
9267
|
+
await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH}`, INSTANCE_CORE_FILE)
|
|
9268
|
+
),
|
|
9269
|
+
fossilCoreVersion: readFossil(options.cwd),
|
|
9270
|
+
branches,
|
|
9271
|
+
worktrees
|
|
9272
|
+
};
|
|
9273
|
+
return runDoctorChecks(facts);
|
|
9274
|
+
}
|
|
9275
|
+
function readLocalCoreVersion(cwd) {
|
|
9276
|
+
const path = join34(cwd, INSTANCE_CORE_FILE);
|
|
9277
|
+
if (!existsSync34(path)) return null;
|
|
9278
|
+
try {
|
|
9279
|
+
return parseCoreRecord(readFileSync25(path, "utf8"));
|
|
9280
|
+
} catch {
|
|
9281
|
+
return null;
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9284
|
+
function parseCoreRecord(contents) {
|
|
9285
|
+
if (contents === null) return null;
|
|
9286
|
+
try {
|
|
9287
|
+
const parsed = JSON.parse(contents);
|
|
9288
|
+
return typeof parsed.version === "string" ? parsed.version : null;
|
|
9289
|
+
} catch {
|
|
9290
|
+
return null;
|
|
9291
|
+
}
|
|
9292
|
+
}
|
|
9293
|
+
function readFossil(cwd) {
|
|
9294
|
+
const path = join34(cwd, CORE_VERSION_FILE);
|
|
9295
|
+
if (!existsSync34(path)) return null;
|
|
9296
|
+
try {
|
|
9297
|
+
const value = readFileSync25(path, "utf8").trim();
|
|
9298
|
+
return value === "" ? null : value;
|
|
9299
|
+
} catch {
|
|
9300
|
+
return null;
|
|
9301
|
+
}
|
|
9302
|
+
}
|
|
9303
|
+
function printFindings(findings) {
|
|
9304
|
+
if (findings.length === 0) {
|
|
9305
|
+
log.success("No findings \u2014 this checkout can be trusted.");
|
|
9306
|
+
return;
|
|
9307
|
+
}
|
|
9308
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
9309
|
+
const warnings = findings.filter((f) => f.severity === "warn");
|
|
9310
|
+
console.log("");
|
|
9311
|
+
for (const f of findings) {
|
|
9312
|
+
const tag = f.severity === "error" ? chalk21.red("error") : chalk21.yellow(" warn");
|
|
9313
|
+
console.log(` ${tag} ${chalk21.bold(f.check)}`);
|
|
9314
|
+
console.log(` ${f.detail}`);
|
|
9315
|
+
console.log(chalk21.dim(` fix: ${f.remedy}`));
|
|
9316
|
+
console.log("");
|
|
9317
|
+
}
|
|
9318
|
+
console.log(
|
|
9319
|
+
chalk21.dim(
|
|
9320
|
+
` ${String(errors.length)} error(s), ${String(warnings.length)} warning(s). Errors mean values read from this checkout may be wrong.
|
|
9321
|
+
`
|
|
9322
|
+
)
|
|
9323
|
+
);
|
|
9324
|
+
}
|
|
9325
|
+
|
|
9074
9326
|
// src/commands/teardown.ts
|
|
9075
9327
|
import { execSync as execSync7 } from "child_process";
|
|
9076
9328
|
import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
|
|
9077
|
-
import
|
|
9078
|
-
import { Command as
|
|
9329
|
+
import chalk22 from "chalk";
|
|
9330
|
+
import { Command as Command25 } from "commander";
|
|
9079
9331
|
import inquirer8 from "inquirer";
|
|
9080
|
-
var teardownCommand = new
|
|
9332
|
+
var teardownCommand = new Command25("teardown").description(
|
|
9081
9333
|
"Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
|
|
9082
9334
|
).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(
|
|
9083
9335
|
"--confirm <name>",
|
|
9084
9336
|
"Pre-confirm by supplying the project name (the scriptable form of the typed confirmation)"
|
|
9085
9337
|
).option("-y, --yes", "Skip the typed project-name confirmation entirely").action(
|
|
9086
9338
|
async (options) => {
|
|
9087
|
-
console.log(
|
|
9339
|
+
console.log(chalk22.bold("\n Biffo \u2014 Teardown\n"));
|
|
9088
9340
|
const githubToken = resolveGithubToken4();
|
|
9089
9341
|
const sts = new STSClient3({});
|
|
9090
9342
|
const { Account: accountId } = await sts.send(new GetCallerIdentityCommand3({}));
|
|
@@ -9106,7 +9358,7 @@ var teardownCommand = new Command24("teardown").description(
|
|
|
9106
9358
|
adminEmail = session.config.admin?.email ?? "noop@example.com";
|
|
9107
9359
|
adminUsername = session.config.admin?.username ?? "noop";
|
|
9108
9360
|
domain = session.config.project.domain ?? "";
|
|
9109
|
-
console.log(
|
|
9361
|
+
console.log(chalk22.yellow(" Loaded session for: ") + chalk22.bold(projectName));
|
|
9110
9362
|
if (org && repo) console.log(` Repository: ${org}/${repo}`);
|
|
9111
9363
|
console.log();
|
|
9112
9364
|
} else if (savedConfig) {
|
|
@@ -9119,7 +9371,7 @@ var teardownCommand = new Command24("teardown").description(
|
|
|
9119
9371
|
adminEmail = savedConfig.admin.email;
|
|
9120
9372
|
adminUsername = savedConfig.admin.username;
|
|
9121
9373
|
domain = resolveDnsConfig(savedConfig).domain;
|
|
9122
|
-
console.log(
|
|
9374
|
+
console.log(chalk22.yellow(" Loaded config for: ") + chalk22.bold(projectName));
|
|
9123
9375
|
console.log(` Repository: ${org}/${repo}`);
|
|
9124
9376
|
console.log();
|
|
9125
9377
|
} else {
|
|
@@ -9190,35 +9442,35 @@ var teardownCommand = new Command24("teardown").description(
|
|
|
9190
9442
|
throw err;
|
|
9191
9443
|
}
|
|
9192
9444
|
}
|
|
9193
|
-
console.log(
|
|
9445
|
+
console.log(chalk22.red.bold(" This will permanently delete:\n"));
|
|
9194
9446
|
if (deployedEnvs.length > 0 || hasGlobal) {
|
|
9195
|
-
console.log(
|
|
9447
|
+
console.log(chalk22.red(" Infrastructure (via GitHub Actions terraform destroy):"));
|
|
9196
9448
|
for (const env of deployedEnvs) {
|
|
9197
9449
|
console.log(
|
|
9198
|
-
` ${
|
|
9450
|
+
` ${chalk22.red("\u2717")} ${env} \u2014 VPC, RDS, Lambda, Cognito, CloudFront, EventBridge`
|
|
9199
9451
|
);
|
|
9200
9452
|
}
|
|
9201
9453
|
if (hasGlobal) {
|
|
9202
|
-
console.log(` ${
|
|
9454
|
+
console.log(` ${chalk22.red("\u2717")} global \u2014 Route 53 hosted zone, ACM certificate`);
|
|
9203
9455
|
}
|
|
9204
9456
|
console.log();
|
|
9205
9457
|
}
|
|
9206
9458
|
for (const line of formatSiblingPlan(siblings, options.skipDestroy === true)) {
|
|
9207
9459
|
console.log(line);
|
|
9208
9460
|
}
|
|
9209
|
-
console.log(
|
|
9210
|
-
console.log(` ${
|
|
9461
|
+
console.log(chalk22.red(" Biffo resources:"));
|
|
9462
|
+
console.log(` ${chalk22.red("\u2717")} GitHub repository ${chalk22.bold(`${org}/${repo}`)}`);
|
|
9211
9463
|
console.log(
|
|
9212
|
-
` ${
|
|
9464
|
+
` ${chalk22.red("\u2717")} IAM role ${chalk22.bold(`biffo-github-actions-${projectName}`)}`
|
|
9213
9465
|
);
|
|
9214
9466
|
console.log(
|
|
9215
|
-
` ${
|
|
9467
|
+
` ${chalk22.red("\u2717")} S3 bucket ${chalk22.bold(stateBucket)} (all versions)`
|
|
9216
9468
|
);
|
|
9217
|
-
console.log(` ${
|
|
9469
|
+
console.log(` ${chalk22.red("\u2717")} Local session file`);
|
|
9218
9470
|
if (options.skipDestroy) {
|
|
9219
9471
|
console.log();
|
|
9220
9472
|
console.log(
|
|
9221
|
-
|
|
9473
|
+
chalk22.yellow(
|
|
9222
9474
|
" --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."
|
|
9223
9475
|
)
|
|
9224
9476
|
);
|
|
@@ -9406,30 +9658,30 @@ async function assertSiblingsAreDestroyable(github, siblings) {
|
|
|
9406
9658
|
}
|
|
9407
9659
|
function formatSiblingPlan(siblings, skipDestroy) {
|
|
9408
9660
|
if (siblings.length === 0) return [];
|
|
9409
|
-
const lines = [
|
|
9661
|
+
const lines = [chalk22.red(` Sibling apps (${siblings.length}) \u2014 ADR-0007:`)];
|
|
9410
9662
|
for (const s of siblings) {
|
|
9411
9663
|
const envs = s.environments.join(", ");
|
|
9412
9664
|
if (s.repoState === "gone") {
|
|
9413
9665
|
lines.push(
|
|
9414
|
-
` ${
|
|
9666
|
+
` ${chalk22.yellow("!")} ${chalk22.bold(`${s.org}/${s.repo}`)} \u2014 repo already deleted; its ${envs} infrastructure CANNOT be destroyed and will be left standing`
|
|
9415
9667
|
);
|
|
9416
9668
|
continue;
|
|
9417
9669
|
}
|
|
9418
9670
|
lines.push(
|
|
9419
|
-
` ${
|
|
9671
|
+
` ${chalk22.red("\u2717")} GitHub repository ${chalk22.bold(`${s.org}/${s.repo}`)} ` + (s.pathPrefix === ROOT_SIBLING_NAME ? "(the application, routed at /)" : `(routed at /${s.pathPrefix})`)
|
|
9420
9672
|
);
|
|
9421
9673
|
lines.push(
|
|
9422
|
-
` ${
|
|
9674
|
+
` ${chalk22.red("\u2717")} ${skipDestroy ? "infrastructure NOT destroyed (--skip-destroy)" : `${envs} infrastructure \u2014 S3 site bucket, Lambda, API Gateway`}`
|
|
9423
9675
|
);
|
|
9424
9676
|
lines.push(
|
|
9425
|
-
` ${
|
|
9677
|
+
` ${chalk22.red("\u2717")} IAM role ${chalk22.bold(`biffo-github-actions-${s.projectName}`)}`
|
|
9426
9678
|
);
|
|
9427
9679
|
lines.push(
|
|
9428
|
-
` ${
|
|
9680
|
+
` ${chalk22.red("\u2717")} S3 bucket ${chalk22.bold(`${s.projectName}-terraform-state-${s.accountId}`)}`
|
|
9429
9681
|
);
|
|
9430
9682
|
if (s.pendingRegistrationPr !== void 0) {
|
|
9431
9683
|
lines.push(
|
|
9432
|
-
|
|
9684
|
+
chalk22.dim(` registration PR #${s.pendingRegistrationPr} is still open \u2014 never routed`)
|
|
9433
9685
|
);
|
|
9434
9686
|
}
|
|
9435
9687
|
}
|
|
@@ -9467,7 +9719,7 @@ async function confirmTeardown(projectName, options) {
|
|
|
9467
9719
|
{
|
|
9468
9720
|
type: "input",
|
|
9469
9721
|
name: "confirm",
|
|
9470
|
-
message: `Type ${
|
|
9722
|
+
message: `Type ${chalk22.bold(projectName)} to confirm:`
|
|
9471
9723
|
}
|
|
9472
9724
|
]);
|
|
9473
9725
|
return confirm === projectName;
|
|
@@ -9485,7 +9737,7 @@ function resolveGithubToken4() {
|
|
|
9485
9737
|
}
|
|
9486
9738
|
|
|
9487
9739
|
// src/index.ts
|
|
9488
|
-
var program = new
|
|
9740
|
+
var program = new Command26();
|
|
9489
9741
|
function cliVersion() {
|
|
9490
9742
|
try {
|
|
9491
9743
|
return getLatestCoreVersion();
|
|
@@ -9503,6 +9755,7 @@ program.addCommand(dataCommand);
|
|
|
9503
9755
|
program.addCommand(coreCommand);
|
|
9504
9756
|
program.addCommand(siblingCommand);
|
|
9505
9757
|
program.addCommand(checkCommand);
|
|
9758
|
+
program.addCommand(doctorCommand);
|
|
9506
9759
|
registerNonInteractive(program);
|
|
9507
9760
|
program.parseAsync().catch((err) => {
|
|
9508
9761
|
if (err instanceof NonInteractiveError) {
|