@biffo/cli 0.198.2 → 0.198.4
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 +195 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1070,6 +1070,15 @@ var GitAdapter = class {
|
|
|
1070
1070
|
async createBranch(cwd, branch) {
|
|
1071
1071
|
await execa2("git", ["switch", "-c", branch], { cwd });
|
|
1072
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Switch to an existing branch. Fails if it does not exist, and — deliberately
|
|
1075
|
+
* — if the switch would discard uncommitted work: this is the undo half of
|
|
1076
|
+
* `createBranch` (#984), so it must never be able to destroy the tree it is
|
|
1077
|
+
* putting back.
|
|
1078
|
+
*/
|
|
1079
|
+
async switchBranch(cwd, branch) {
|
|
1080
|
+
await execa2("git", ["switch", branch], { cwd });
|
|
1081
|
+
}
|
|
1073
1082
|
/**
|
|
1074
1083
|
* Push the current HEAD to `branch` on the remote. When `token` is given and
|
|
1075
1084
|
* the remote is HTTPS, it's embedded in the push URL for auth (SSH/file
|
|
@@ -1158,6 +1167,53 @@ function injectToken(repoUrl, token) {
|
|
|
1158
1167
|
// src/adapters/source-control/github/index.ts
|
|
1159
1168
|
import { execSync } from "child_process";
|
|
1160
1169
|
import { Octokit } from "@octokit/rest";
|
|
1170
|
+
|
|
1171
|
+
// src/lib/branch-protection-outcome.ts
|
|
1172
|
+
var pending = [];
|
|
1173
|
+
function recordBranchProtectionOutcome(outcome) {
|
|
1174
|
+
pending.push(outcome);
|
|
1175
|
+
return outcome;
|
|
1176
|
+
}
|
|
1177
|
+
function isUnprotected(outcome) {
|
|
1178
|
+
return outcome.status !== "applied" || outcome.unprotectedBranches.length > 0;
|
|
1179
|
+
}
|
|
1180
|
+
function formatBranchProtectionSummary(outcomes) {
|
|
1181
|
+
if (outcomes.length === 0) return [];
|
|
1182
|
+
const unprotected = outcomes.filter(isUnprotected);
|
|
1183
|
+
if (unprotected.length === 0) {
|
|
1184
|
+
return [
|
|
1185
|
+
`Branch protection applied to ${outcomes.map((o) => `${o.org}/${o.repo}`).sort().join(", ")}`
|
|
1186
|
+
];
|
|
1187
|
+
}
|
|
1188
|
+
const lines = [
|
|
1189
|
+
`Branch protection was NOT fully applied \u2014 ${unprotected.length} of ${outcomes.length} repositor${outcomes.length === 1 ? "y" : "ies"} created by this run ${unprotected.length === 1 ? "is" : "are"} unprotected:`
|
|
1190
|
+
];
|
|
1191
|
+
for (const outcome of unprotected) {
|
|
1192
|
+
const left = outcome.unprotectedBranches.join(", ") || "unknown";
|
|
1193
|
+
const why = outcome.status === "skipped-403" ? "GitHub returned 403 \u2014 the org's plan does not allow branch protection on this repo" : outcome.status === "failed" ? "branch protection failed" : "branch protection incomplete";
|
|
1194
|
+
lines.push(` ${outcome.org}/${outcome.repo} \u2014 unprotected: ${left} (${why})`);
|
|
1195
|
+
if (outcome.reason) lines.push(` ${outcome.reason}`);
|
|
1196
|
+
}
|
|
1197
|
+
lines.push(
|
|
1198
|
+
" Direct pushes, force-pushes and merges with red or missing checks are all allowed on those branches right now.",
|
|
1199
|
+
" Fix it with: biffo check branch-protection --fix (after upgrading the plan, or making the repo public)"
|
|
1200
|
+
);
|
|
1201
|
+
return lines;
|
|
1202
|
+
}
|
|
1203
|
+
function reportBranchProtectionSummary() {
|
|
1204
|
+
const outcomes = pending.splice(0, pending.length);
|
|
1205
|
+
const lines = formatBranchProtectionSummary(outcomes);
|
|
1206
|
+
if (lines.length === 0) return outcomes;
|
|
1207
|
+
if (outcomes.some(isUnprotected)) {
|
|
1208
|
+
log.error(lines[0]);
|
|
1209
|
+
for (const line of lines.slice(1)) log.error(line);
|
|
1210
|
+
} else {
|
|
1211
|
+
log.success(lines[0]);
|
|
1212
|
+
}
|
|
1213
|
+
return outcomes;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/adapters/source-control/github/index.ts
|
|
1161
1217
|
var DEFAULT_STATUS_CHECKS = [
|
|
1162
1218
|
"JS (lint, types, test, audit)",
|
|
1163
1219
|
"Python (lint, types, test, security)",
|
|
@@ -1557,49 +1613,96 @@ var GitHubAdapter = class {
|
|
|
1557
1613
|
await this.octokit.repos.update({ owner: org, repo, default_branch: branch });
|
|
1558
1614
|
log.info(`Default branch set to ${branch}`);
|
|
1559
1615
|
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Protect `dev`, `staging` and `main`, and **say what actually happened**.
|
|
1618
|
+
*
|
|
1619
|
+
* This used to return `Promise<void>`, which made the 403 path (GitHub
|
|
1620
|
+
* refusing branch protection on a private org repo whose plan does not
|
|
1621
|
+
* include it) indistinguishable from success at every call site: the method
|
|
1622
|
+
* logged two warnings and returned, and the scaffold went on to report a
|
|
1623
|
+
* repo created. Nothing durable recorded that protection had been skipped,
|
|
1624
|
+
* so the only trace was a log line in the middle of a long provisioning
|
|
1625
|
+
* transcript. Three repos — including a live core platform — ran completely
|
|
1626
|
+
* unprotected for three weeks on the strength of that (#715, #737 item 2).
|
|
1627
|
+
*
|
|
1628
|
+
* The returned outcome is also pushed onto the run-scoped collector in
|
|
1629
|
+
* `lib/branch-protection-outcome.ts`, so callers that do nothing with the
|
|
1630
|
+
* return value still get named in the end-of-run summary.
|
|
1631
|
+
*/
|
|
1560
1632
|
async configureBranchProtection(config, protectionIntervalMs = 3e3, statusChecks = DEFAULT_STATUS_CHECKS) {
|
|
1561
1633
|
const { org, repo } = config.source_control.config;
|
|
1562
1634
|
const branches = ["dev", "staging", "main"];
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
)
|
|
1594
|
-
|
|
1635
|
+
const applied = [];
|
|
1636
|
+
const remaining = () => branches.filter((b) => !applied.includes(b));
|
|
1637
|
+
try {
|
|
1638
|
+
for (const branch of branches) {
|
|
1639
|
+
log.info(`Waiting for ${branch} branch to be ready...`);
|
|
1640
|
+
await this.waitForBranch(org, repo, branch);
|
|
1641
|
+
log.info(`Configuring branch protection on ${branch}...`);
|
|
1642
|
+
const params = {
|
|
1643
|
+
owner: org,
|
|
1644
|
+
repo,
|
|
1645
|
+
branch,
|
|
1646
|
+
required_status_checks: { strict: true, contexts: statusChecks },
|
|
1647
|
+
enforce_admins: false,
|
|
1648
|
+
required_pull_request_reviews: {
|
|
1649
|
+
required_approving_review_count: 0,
|
|
1650
|
+
dismiss_stale_reviews: false
|
|
1651
|
+
},
|
|
1652
|
+
restrictions: null,
|
|
1653
|
+
required_linear_history: true,
|
|
1654
|
+
allow_force_pushes: false,
|
|
1655
|
+
allow_deletions: false
|
|
1656
|
+
};
|
|
1657
|
+
const deadline = Date.now() + 3e4;
|
|
1658
|
+
while (true) {
|
|
1659
|
+
try {
|
|
1660
|
+
await this.octokit.repos.updateBranchProtection(params);
|
|
1661
|
+
applied.push(branch);
|
|
1662
|
+
break;
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
const status = err.status;
|
|
1665
|
+
if (status === 403) {
|
|
1666
|
+
log.warn(
|
|
1667
|
+
`Branch protection unavailable for ${org}/${repo}: ${err.message}`
|
|
1668
|
+
);
|
|
1669
|
+
log.warn(
|
|
1670
|
+
" This usually means the organization is on a plan that only supports branch protection on public repos (GitHub Team/Enterprise is required for private org repos). Skipping branch protection \u2014 add it later via GitHub once the plan allows it, or make the repo public."
|
|
1671
|
+
);
|
|
1672
|
+
return recordBranchProtectionOutcome({
|
|
1673
|
+
status: "skipped-403",
|
|
1674
|
+
org,
|
|
1675
|
+
repo,
|
|
1676
|
+
protectedBranches: applied,
|
|
1677
|
+
unprotectedBranches: remaining(),
|
|
1678
|
+
reason: err.message
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1682
|
+
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1683
|
+
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1595
1684
|
}
|
|
1596
|
-
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1597
|
-
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1598
|
-
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1599
1685
|
}
|
|
1600
1686
|
}
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
recordBranchProtectionOutcome({
|
|
1689
|
+
status: "failed",
|
|
1690
|
+
org,
|
|
1691
|
+
repo,
|
|
1692
|
+
protectedBranches: applied,
|
|
1693
|
+
unprotectedBranches: remaining(),
|
|
1694
|
+
reason: err.message
|
|
1695
|
+
});
|
|
1696
|
+
throw err;
|
|
1601
1697
|
}
|
|
1602
1698
|
log.success("Branch protection configured on dev, staging, and main");
|
|
1699
|
+
return recordBranchProtectionOutcome({
|
|
1700
|
+
status: "applied",
|
|
1701
|
+
org,
|
|
1702
|
+
repo,
|
|
1703
|
+
protectedBranches: applied,
|
|
1704
|
+
unprotectedBranches: []
|
|
1705
|
+
});
|
|
1603
1706
|
}
|
|
1604
1707
|
/**
|
|
1605
1708
|
* Protect a single branch with caller-supplied required checks (#803).
|
|
@@ -2937,8 +3040,44 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
|
|
|
2937
3040
|
throw new Error(`${options.cwd} is not a git repository.`);
|
|
2938
3041
|
}
|
|
2939
3042
|
const branch = upgradeBranchName(fromVersion, toVersion);
|
|
3043
|
+
const callerBranch = await git.currentBranch(options.cwd);
|
|
2940
3044
|
log.step(1, 4, `Creating branch ${branch}`);
|
|
2941
3045
|
await git.createBranch(options.cwd, branch);
|
|
3046
|
+
try {
|
|
3047
|
+
await buildCommitAndOpenPr(
|
|
3048
|
+
options,
|
|
3049
|
+
deps,
|
|
3050
|
+
plan,
|
|
3051
|
+
migrations,
|
|
3052
|
+
fromVersion,
|
|
3053
|
+
toVersion,
|
|
3054
|
+
breaking,
|
|
3055
|
+
theirsDir,
|
|
3056
|
+
coreVersionCleanup,
|
|
3057
|
+
branch,
|
|
3058
|
+
token
|
|
3059
|
+
);
|
|
3060
|
+
} finally {
|
|
3061
|
+
await restoreCallerBranch(git, options.cwd, callerBranch, branch);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
async function restoreCallerBranch(git, cwd, callerBranch, upgradeBranch) {
|
|
3065
|
+
if (callerBranch === "HEAD" || callerBranch === "") {
|
|
3066
|
+
log.warn(
|
|
3067
|
+
`Left ${cwd} on ${upgradeBranch}: HEAD was detached on entry, so there is no branch to restore (#984).`
|
|
3068
|
+
);
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
try {
|
|
3072
|
+
await git.switchBranch(cwd, callerBranch);
|
|
3073
|
+
} catch {
|
|
3074
|
+
log.warn(
|
|
3075
|
+
`Could not switch ${cwd} back to ${callerBranch} \u2014 it is still on ${upgradeBranch}. Restore it with \`git switch ${callerBranch}\` (#984).`
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
async function buildCommitAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking, theirsDir, coreVersionCleanup, branch, token) {
|
|
3080
|
+
const { git } = deps;
|
|
2942
3081
|
const applied = applyUpgradePlan(options.cwd, plan, theirsDir);
|
|
2943
3082
|
const carried = applyMigrationCarry(options.cwd, migrations);
|
|
2944
3083
|
writeInstanceCoreVersion(options.cwd, toVersion);
|
|
@@ -5643,11 +5782,15 @@ async function runSiblingCreateCommand(name, options) {
|
|
|
5643
5782
|
const aws = new AwsAdapter(config);
|
|
5644
5783
|
const coreAws = new AwsAdapter(coreConfig);
|
|
5645
5784
|
const git = new GitAdapter();
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5785
|
+
try {
|
|
5786
|
+
await runSiblingCreate(github, aws, coreAws, git, config, session, {
|
|
5787
|
+
coreConfig,
|
|
5788
|
+
skeletonRoot: options.templateRoot,
|
|
5789
|
+
githubToken: token
|
|
5790
|
+
});
|
|
5791
|
+
} finally {
|
|
5792
|
+
reportBranchProtectionSummary();
|
|
5793
|
+
}
|
|
5651
5794
|
const { org, repo } = githubRepo(config);
|
|
5652
5795
|
const pathPrefix = resolvePathPrefix(config);
|
|
5653
5796
|
log.success("\nSibling repo created successfully!");
|
|
@@ -5781,6 +5924,7 @@ async function runSiblingCreate(github, aws, coreAws, git, config, session, opti
|
|
|
5781
5924
|
} else {
|
|
5782
5925
|
log.step(8, totalSteps, "Already registered with the core project \u2014 skipping");
|
|
5783
5926
|
}
|
|
5927
|
+
reportBranchProtectionSummary();
|
|
5784
5928
|
deleteSiblingSession(config.project.name);
|
|
5785
5929
|
}
|
|
5786
5930
|
function resolvePathPrefix(config) {
|
|
@@ -6197,12 +6341,16 @@ var initCommand = new Command12("init").description("Scaffold a new project from
|
|
|
6197
6341
|
githubToken ??= await resolveGithubToken3(options.yes === true || Boolean(options.config));
|
|
6198
6342
|
const github = new GitHubAdapter(githubToken);
|
|
6199
6343
|
const aws = new AwsAdapter(config);
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6344
|
+
try {
|
|
6345
|
+
await runInit(github, aws, config, session, {
|
|
6346
|
+
git: new GitAdapter(),
|
|
6347
|
+
awsFor: (siblingConfig) => new AwsAdapter(siblingConfig),
|
|
6348
|
+
skeletonRoot: defaultSiblingTemplateRoot(),
|
|
6349
|
+
githubToken
|
|
6350
|
+
});
|
|
6351
|
+
} finally {
|
|
6352
|
+
reportBranchProtectionSummary();
|
|
6353
|
+
}
|
|
6206
6354
|
const { org, repo } = config.source_control.config;
|
|
6207
6355
|
const appRepo = rootSiblingProjectName(config.project.name);
|
|
6208
6356
|
log.success("\nProject initialised successfully!");
|
|
@@ -6340,6 +6488,7 @@ async function runInit(github, aws, config, session, appSibling) {
|
|
|
6340
6488
|
log.step(6, totalSteps, "Application sibling already created \u2014 skipping");
|
|
6341
6489
|
}
|
|
6342
6490
|
}
|
|
6491
|
+
reportBranchProtectionSummary();
|
|
6343
6492
|
deleteSession(config.project.name);
|
|
6344
6493
|
saveProjectConfig(config);
|
|
6345
6494
|
}
|