@biffo/cli 0.207.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +86 -1
  2. 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 ? 6 : 5;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.207.2",
3
+ "version": "0.208.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",