@biffo/cli 0.198.3 → 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 +150 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1167,6 +1167,53 @@ function injectToken(repoUrl, token) {
|
|
|
1167
1167
|
// src/adapters/source-control/github/index.ts
|
|
1168
1168
|
import { execSync } from "child_process";
|
|
1169
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
|
|
1170
1217
|
var DEFAULT_STATUS_CHECKS = [
|
|
1171
1218
|
"JS (lint, types, test, audit)",
|
|
1172
1219
|
"Python (lint, types, test, security)",
|
|
@@ -1566,49 +1613,96 @@ var GitHubAdapter = class {
|
|
|
1566
1613
|
await this.octokit.repos.update({ owner: org, repo, default_branch: branch });
|
|
1567
1614
|
log.info(`Default branch set to ${branch}`);
|
|
1568
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
|
+
*/
|
|
1569
1632
|
async configureBranchProtection(config, protectionIntervalMs = 3e3, statusChecks = DEFAULT_STATUS_CHECKS) {
|
|
1570
1633
|
const { org, repo } = config.source_control.config;
|
|
1571
1634
|
const branches = ["dev", "staging", "main"];
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
)
|
|
1603
|
-
|
|
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));
|
|
1604
1684
|
}
|
|
1605
|
-
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1606
|
-
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1607
|
-
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1608
1685
|
}
|
|
1609
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;
|
|
1610
1697
|
}
|
|
1611
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
|
+
});
|
|
1612
1706
|
}
|
|
1613
1707
|
/**
|
|
1614
1708
|
* Protect a single branch with caller-supplied required checks (#803).
|
|
@@ -5688,11 +5782,15 @@ async function runSiblingCreateCommand(name, options) {
|
|
|
5688
5782
|
const aws = new AwsAdapter(config);
|
|
5689
5783
|
const coreAws = new AwsAdapter(coreConfig);
|
|
5690
5784
|
const git = new GitAdapter();
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
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
|
+
}
|
|
5696
5794
|
const { org, repo } = githubRepo(config);
|
|
5697
5795
|
const pathPrefix = resolvePathPrefix(config);
|
|
5698
5796
|
log.success("\nSibling repo created successfully!");
|
|
@@ -5826,6 +5924,7 @@ async function runSiblingCreate(github, aws, coreAws, git, config, session, opti
|
|
|
5826
5924
|
} else {
|
|
5827
5925
|
log.step(8, totalSteps, "Already registered with the core project \u2014 skipping");
|
|
5828
5926
|
}
|
|
5927
|
+
reportBranchProtectionSummary();
|
|
5829
5928
|
deleteSiblingSession(config.project.name);
|
|
5830
5929
|
}
|
|
5831
5930
|
function resolvePathPrefix(config) {
|
|
@@ -6242,12 +6341,16 @@ var initCommand = new Command12("init").description("Scaffold a new project from
|
|
|
6242
6341
|
githubToken ??= await resolveGithubToken3(options.yes === true || Boolean(options.config));
|
|
6243
6342
|
const github = new GitHubAdapter(githubToken);
|
|
6244
6343
|
const aws = new AwsAdapter(config);
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
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
|
+
}
|
|
6251
6354
|
const { org, repo } = config.source_control.config;
|
|
6252
6355
|
const appRepo = rootSiblingProjectName(config.project.name);
|
|
6253
6356
|
log.success("\nProject initialised successfully!");
|
|
@@ -6385,6 +6488,7 @@ async function runInit(github, aws, config, session, appSibling) {
|
|
|
6385
6488
|
log.step(6, totalSteps, "Application sibling already created \u2014 skipping");
|
|
6386
6489
|
}
|
|
6387
6490
|
}
|
|
6491
|
+
reportBranchProtectionSummary();
|
|
6388
6492
|
deleteSession(config.project.name);
|
|
6389
6493
|
saveProjectConfig(config);
|
|
6390
6494
|
}
|