@biffo/cli 0.207.1 → 0.208.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 +86 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1249,6 +1249,11 @@ function formatBranchProtectionSummary(outcomes) {
|
|
|
1249
1249
|
lines.push(
|
|
1250
1250
|
" Direct pushes, force-pushes and merges with red or missing checks are all allowed on those branches right now."
|
|
1251
1251
|
);
|
|
1252
|
+
if (unprotected.some((o) => o.status === "unsealed")) {
|
|
1253
|
+
lines.push(
|
|
1254
|
+
" An unsealed branch enforces its rules on everyone except a repo admin \u2014 which, on a solo or agent-driven estate, is everyone who merges."
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1252
1257
|
if (unprotected.some((o) => o.status === "skipped-no-contexts")) {
|
|
1253
1258
|
lines.push(
|
|
1254
1259
|
" Where the required checks could not be determined, the fix is the CI job names, not the plan: make the workflow report named jobs, let one run complete, then backfill."
|
|
@@ -1268,6 +1273,8 @@ function whyUnprotected(outcome) {
|
|
|
1268
1273
|
return "the required status checks could not be determined, and protection requiring nothing reads as configured while gating on nothing";
|
|
1269
1274
|
case "failed":
|
|
1270
1275
|
return "branch protection failed";
|
|
1276
|
+
case "unsealed":
|
|
1277
|
+
return "protection was applied but does not bind admins, so every required check on it is advisory for whoever merges";
|
|
1271
1278
|
default:
|
|
1272
1279
|
return "branch protection incomplete";
|
|
1273
1280
|
}
|
|
@@ -1776,6 +1783,76 @@ var GitHubAdapter = class {
|
|
|
1776
1783
|
unprotectedBranches: []
|
|
1777
1784
|
});
|
|
1778
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Bind the protection to admins too — the last thing a scaffolding run does
|
|
1788
|
+
* to a repo (#1058).
|
|
1789
|
+
*
|
|
1790
|
+
* ## Why this is a separate call and not a field above
|
|
1791
|
+
*
|
|
1792
|
+
* `configureBranchProtection` deliberately applies `enforce_admins: false`,
|
|
1793
|
+
* and that is **correct while the scaffold is still running**: `biffo init` is
|
|
1794
|
+
* resumable and step-checkpointed, so a resumed run may need `commitFiles` to
|
|
1795
|
+
* write to branches an earlier attempt already protected. The init token is
|
|
1796
|
+
* the repo's own creator, so the bypass is what lets that succeed.
|
|
1797
|
+
*
|
|
1798
|
+
* The defect was never that value — it was that nothing ever *closed* it. A
|
|
1799
|
+
* scaffold-time bypass with no expiry became the estate's permanent state on
|
|
1800
|
+
* 26 of 27 branches, every one of them showing a full required-check list that
|
|
1801
|
+
* bound nobody, and every audit calling them fine (#1052).
|
|
1802
|
+
*
|
|
1803
|
+
* So the lifetime is made explicit instead: open during the scaffold, closed
|
|
1804
|
+
* at the end of it. Sealing is **the last thing the run does to the repo**,
|
|
1805
|
+
* after every git-object write, which is the property that keeps resumability
|
|
1806
|
+
* intact — a run interrupted before the seal leaves the branches unsealed and
|
|
1807
|
+
* therefore still writable on resume, and a run that reached the seal has
|
|
1808
|
+
* nothing left to write. `cli/src/commands/init.test.ts` asserts that
|
|
1809
|
+
* ordering, because it is an invariant of the step list rather than of this
|
|
1810
|
+
* function.
|
|
1811
|
+
*
|
|
1812
|
+
* Idempotent: `PUT .../enforce_admins` on an already-sealed branch is a no-op,
|
|
1813
|
+
* so a resumed or re-run scaffold re-seals harmlessly.
|
|
1814
|
+
*
|
|
1815
|
+
* Failures are recorded, never thrown. A repo that ends the run protected but
|
|
1816
|
+
* unsealed is a real finding and appears in the end-of-run summary as one —
|
|
1817
|
+
* but it is strictly better than the pre-#1052 state, and aborting a completed
|
|
1818
|
+
* scaffold over it would trade a reported weakness for an unreported mess.
|
|
1819
|
+
*/
|
|
1820
|
+
async sealBranchProtection(org, repo, branches = ["dev", "staging", "main"]) {
|
|
1821
|
+
const sealed = [];
|
|
1822
|
+
const unsealed = [];
|
|
1823
|
+
let reason;
|
|
1824
|
+
for (const branch of branches) {
|
|
1825
|
+
try {
|
|
1826
|
+
await this.octokit.repos.setAdminBranchProtection({ owner: org, repo, branch });
|
|
1827
|
+
sealed.push(branch);
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
const status = err.status;
|
|
1830
|
+
unsealed.push(branch);
|
|
1831
|
+
if (status !== 404) {
|
|
1832
|
+
reason ??= err.message;
|
|
1833
|
+
log.warn(`Could not bind branch protection to admins on ${branch}: ${reason}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (unsealed.length === 0) {
|
|
1838
|
+
log.success(`Branch protection now binds admins on ${sealed.join(", ")}`);
|
|
1839
|
+
return recordBranchProtectionOutcome({
|
|
1840
|
+
status: "applied",
|
|
1841
|
+
org,
|
|
1842
|
+
repo,
|
|
1843
|
+
protectedBranches: sealed,
|
|
1844
|
+
unprotectedBranches: []
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
return recordBranchProtectionOutcome({
|
|
1848
|
+
status: "unsealed",
|
|
1849
|
+
org,
|
|
1850
|
+
repo,
|
|
1851
|
+
protectedBranches: sealed,
|
|
1852
|
+
unprotectedBranches: unsealed,
|
|
1853
|
+
...reason ? { reason } : {}
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1779
1856
|
/**
|
|
1780
1857
|
* Protect a single branch with caller-supplied required checks (#803).
|
|
1781
1858
|
*
|
|
@@ -6226,6 +6303,7 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
|
|
|
6226
6303
|
await github.setDefaultBranch(org, repo, "dev");
|
|
6227
6304
|
await github.configureBranchProtection(config);
|
|
6228
6305
|
await github.createEnvironments(config);
|
|
6306
|
+
await github.sealBranchProtection(org, repo);
|
|
6229
6307
|
await github.enableVulnerabilityAlerts(org, repo);
|
|
6230
6308
|
await github.setRepoVariable(org, repo, "PROJECT_NAME", config.project.name);
|
|
6231
6309
|
const pathPrefix = resolvePathPrefix(config);
|
|
@@ -6532,7 +6610,7 @@ function resolveConfigFileSession(config, awsAccountId, awsRegion, fresh) {
|
|
|
6532
6610
|
return session;
|
|
6533
6611
|
}
|
|
6534
6612
|
async function runInit(github, aws, config, session, appSibling) {
|
|
6535
|
-
const totalSteps = appSibling ?
|
|
6613
|
+
const totalSteps = appSibling ? 7 : 6;
|
|
6536
6614
|
if (!session.completedSteps.includes("verify_credentials")) {
|
|
6537
6615
|
log.step(1, totalSteps, "Verifying AWS credentials...");
|
|
6538
6616
|
await aws.verifyCredentials();
|
|
@@ -6641,6 +6719,13 @@ async function runInit(github, aws, config, session, appSibling) {
|
|
|
6641
6719
|
log.step(6, totalSteps, "Application sibling already created \u2014 skipping");
|
|
6642
6720
|
}
|
|
6643
6721
|
}
|
|
6722
|
+
if (!hasCompleted(session, "github_protection_sealed")) {
|
|
6723
|
+
log.step(7, totalSteps, "Binding branch protection to admins...");
|
|
6724
|
+
await github.sealBranchProtection(org, repo);
|
|
6725
|
+
markStepComplete(session, "github_protection_sealed");
|
|
6726
|
+
} else {
|
|
6727
|
+
log.step(7, totalSteps, "Branch protection already binds admins \u2014 skipping");
|
|
6728
|
+
}
|
|
6644
6729
|
reportBranchProtectionSummary();
|
|
6645
6730
|
deleteSession(config.project.name);
|
|
6646
6731
|
saveProjectConfig(config);
|