@biffo/cli 0.199.2 → 0.199.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.
Files changed (2) hide show
  1. package/dist/index.js +132 -58
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1222,16 +1222,37 @@ function formatBranchProtectionSummary(outcomes) {
1222
1222
  ];
1223
1223
  for (const outcome of unprotected) {
1224
1224
  const left = outcome.unprotectedBranches.join(", ") || "unknown";
1225
- 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";
1226
- lines.push(` ${outcome.org}/${outcome.repo} \u2014 unprotected: ${left} (${why})`);
1225
+ lines.push(
1226
+ ` ${outcome.org}/${outcome.repo} \u2014 unprotected: ${left} (${whyUnprotected(outcome)})`
1227
+ );
1227
1228
  if (outcome.reason) lines.push(` ${outcome.reason}`);
1228
1229
  }
1229
1230
  lines.push(
1230
- " Direct pushes, force-pushes and merges with red or missing checks are all allowed on those branches right now.",
1231
- " Fix it with: biffo check branch-protection --fix (after upgrading the plan, or making the repo public)"
1231
+ " Direct pushes, force-pushes and merges with red or missing checks are all allowed on those branches right now."
1232
+ );
1233
+ if (unprotected.some((o) => o.status === "skipped-no-contexts")) {
1234
+ lines.push(
1235
+ " 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."
1236
+ );
1237
+ }
1238
+ const has403 = unprotected.some((o) => o.status === "skipped-403");
1239
+ lines.push(
1240
+ " Fix it with: biffo check branch-protection --fix " + (has403 ? "(after upgrading the plan, or making the repo public)" : "(it derives the required checks from the runs the repo has actually reported)")
1232
1241
  );
1233
1242
  return lines;
1234
1243
  }
1244
+ function whyUnprotected(outcome) {
1245
+ switch (outcome.status) {
1246
+ case "skipped-403":
1247
+ return "GitHub returned 403 \u2014 the org's plan does not allow branch protection on this repo";
1248
+ case "skipped-no-contexts":
1249
+ return "the required status checks could not be determined, and protection requiring nothing reads as configured while gating on nothing";
1250
+ case "failed":
1251
+ return "branch protection failed";
1252
+ default:
1253
+ return "branch protection incomplete";
1254
+ }
1255
+ }
1235
1256
  function reportBranchProtectionSummary() {
1236
1257
  const outcomes = pending.splice(0, pending.length);
1237
1258
  const lines = formatBranchProtectionSummary(outcomes);
@@ -1754,51 +1775,91 @@ var GitHubAdapter = class {
1754
1775
  * `statusChecks` must be non-empty. Protection that requires nothing is worse
1755
1776
  * than none: it reads as configured while gating on nothing, so callers that
1756
1777
  * could not determine the contexts must not silently land here.
1778
+ *
1779
+ * Like `configureBranchProtection`, this **returns and records** what actually
1780
+ * happened (#1001). It used to return `Promise<void>` and swallow the 403 with
1781
+ * two log warnings, so `biffo plugin create --org` printed its next-steps
1782
+ * block and exited 0 with nothing anywhere recording that the repo it had just
1783
+ * created had an unprotected `dev` — the identical defect #715 described for
1784
+ * the deployable path, on the path #999 deliberately left out of scope.
1757
1785
  */
1758
1786
  async protectSingleBranch(org, repo, branch, statusChecks, protectionIntervalMs = 3e3) {
1759
1787
  if (statusChecks.length === 0) {
1760
- throw new Error(
1761
- `Refusing to protect ${org}/${repo}@${branch} with no required status checks \u2014 that reads as protected while gating on nothing. Determine the workflow's job contexts first.`
1762
- );
1788
+ const message = `Refusing to protect ${org}/${repo}@${branch} with no required status checks \u2014 that reads as protected while gating on nothing. Determine the workflow's job contexts first.`;
1789
+ recordBranchProtectionOutcome({
1790
+ status: "skipped-no-contexts",
1791
+ org,
1792
+ repo,
1793
+ protectedBranches: [],
1794
+ unprotectedBranches: [branch],
1795
+ reason: message
1796
+ });
1797
+ throw new Error(message);
1763
1798
  }
1764
- log.info(`Waiting for ${branch} branch to be ready...`);
1765
- await this.waitForBranch(org, repo, branch);
1766
- log.info(`Configuring branch protection on ${branch}...`);
1767
- const params = {
1768
- owner: org,
1769
- repo,
1770
- branch,
1771
- required_status_checks: { strict: true, contexts: statusChecks },
1772
- enforce_admins: false,
1773
- required_pull_request_reviews: {
1774
- required_approving_review_count: 0,
1775
- dismiss_stale_reviews: false
1776
- },
1777
- restrictions: null,
1778
- required_linear_history: true,
1779
- allow_force_pushes: false,
1780
- allow_deletions: false
1781
- };
1782
- const deadline = Date.now() + 3e4;
1783
- while (true) {
1784
- try {
1785
- await this.octokit.repos.updateBranchProtection(params);
1786
- break;
1787
- } catch (err) {
1788
- const status = err.status;
1789
- if (status === 403) {
1790
- log.warn(`Branch protection unavailable for ${org}/${repo}: ${err.message}`);
1791
- log.warn(" Add it later via GitHub once the plan allows it, or make the repo public.");
1792
- return;
1799
+ try {
1800
+ log.info(`Waiting for ${branch} branch to be ready...`);
1801
+ await this.waitForBranch(org, repo, branch);
1802
+ log.info(`Configuring branch protection on ${branch}...`);
1803
+ const params = {
1804
+ owner: org,
1805
+ repo,
1806
+ branch,
1807
+ required_status_checks: { strict: true, contexts: statusChecks },
1808
+ enforce_admins: false,
1809
+ required_pull_request_reviews: {
1810
+ required_approving_review_count: 0,
1811
+ dismiss_stale_reviews: false
1812
+ },
1813
+ restrictions: null,
1814
+ required_linear_history: true,
1815
+ allow_force_pushes: false,
1816
+ allow_deletions: false
1817
+ };
1818
+ const deadline = Date.now() + 3e4;
1819
+ while (true) {
1820
+ try {
1821
+ await this.octokit.repos.updateBranchProtection(params);
1822
+ break;
1823
+ } catch (err) {
1824
+ const status = err.status;
1825
+ if (status === 403) {
1826
+ log.warn(`Branch protection unavailable for ${org}/${repo}: ${err.message}`);
1827
+ log.warn(" Add it later via GitHub once the plan allows it, or make the repo public.");
1828
+ return recordBranchProtectionOutcome({
1829
+ status: "skipped-403",
1830
+ org,
1831
+ repo,
1832
+ protectedBranches: [],
1833
+ unprotectedBranches: [branch],
1834
+ reason: err.message
1835
+ });
1836
+ }
1837
+ if (status !== 404 || Date.now() >= deadline) throw err;
1838
+ log.info("Branch protection endpoint not yet ready, retrying...");
1839
+ await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
1793
1840
  }
1794
- if (status !== 404 || Date.now() >= deadline) throw err;
1795
- log.info("Branch protection endpoint not yet ready, retrying...");
1796
- await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
1797
1841
  }
1842
+ } catch (err) {
1843
+ recordBranchProtectionOutcome({
1844
+ status: "failed",
1845
+ org,
1846
+ repo,
1847
+ protectedBranches: [],
1848
+ unprotectedBranches: [branch],
1849
+ reason: err.message
1850
+ });
1851
+ throw err;
1798
1852
  }
1799
1853
  log.success(
1800
1854
  `Branch protection configured on ${branch} (${statusChecks.length} required check(s))`
1801
1855
  );
1856
+ return recordBranchProtectionOutcome({
1857
+ status: "applied",
1858
+ org,
1859
+ repo,
1860
+ protectedBranches: [branch],
1861
+ unprotectedBranches: []
1862
+ });
1802
1863
  }
1803
1864
  async createEnvironments(config) {
1804
1865
  const { org, repo } = config.source_control.config;
@@ -7182,25 +7243,29 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
7182
7243
  async (name, options) => {
7183
7244
  const cwd = options.cwd ? resolve11(options.cwd) : process.cwd();
7184
7245
  try {
7185
- await runPluginCreate(
7186
- name,
7187
- {
7188
- firstParty: options.firstParty ?? false,
7189
- standalone: options.standalone ?? false,
7190
- ...options.org ? { org: options.org } : {},
7191
- ...options.runnerLabel ? { runnerLabel: options.runnerLabel } : {},
7192
- register: options.register !== false,
7193
- ...options.skeleton ? { skeletonRoot: resolve11(options.skeleton) } : {},
7194
- dryRun: options.dryRun ?? false,
7195
- commit: options.commit !== false,
7196
- cwd
7197
- },
7198
- {
7199
- git: new GitAdapter(),
7200
- makeGitHub: (token) => new GitHubAdapter(token),
7201
- resolveToken: resolveGithubToken3
7202
- }
7203
- );
7246
+ try {
7247
+ await runPluginCreate(
7248
+ name,
7249
+ {
7250
+ firstParty: options.firstParty ?? false,
7251
+ standalone: options.standalone ?? false,
7252
+ ...options.org ? { org: options.org } : {},
7253
+ ...options.runnerLabel ? { runnerLabel: options.runnerLabel } : {},
7254
+ register: options.register !== false,
7255
+ ...options.skeleton ? { skeletonRoot: resolve11(options.skeleton) } : {},
7256
+ dryRun: options.dryRun ?? false,
7257
+ commit: options.commit !== false,
7258
+ cwd
7259
+ },
7260
+ {
7261
+ git: new GitAdapter(),
7262
+ makeGitHub: (token) => new GitHubAdapter(token),
7263
+ resolveToken: resolveGithubToken3
7264
+ }
7265
+ );
7266
+ } finally {
7267
+ reportBranchProtectionSummary();
7268
+ }
7204
7269
  } catch (err) {
7205
7270
  log.error(err.message);
7206
7271
  process.exit(1);
@@ -7216,6 +7281,7 @@ async function runPluginCreate(name, options, deps) {
7216
7281
  );
7217
7282
  }
7218
7283
  await runStandaloneCreate(names, options, deps);
7284
+ reportBranchProtectionSummary();
7219
7285
  return;
7220
7286
  }
7221
7287
  const isInstance = existsSync23(join23(options.cwd, INSTANCE_CORE_FILE));
@@ -7341,6 +7407,14 @@ async function createAndPushStandaloneRepo(org, names, destDir, options, deps) {
7341
7407
  log.warn(
7342
7408
  `Could not determine required status checks from ${ciPath} \u2014 skipping branch protection. Configure it manually on dev once you know the CI job names.`
7343
7409
  );
7410
+ recordBranchProtectionOutcome({
7411
+ status: "skipped-no-contexts",
7412
+ org,
7413
+ repo: names.dist,
7414
+ protectedBranches: [],
7415
+ unprotectedBranches: ["dev"],
7416
+ reason: `No status-check contexts could be derived from ${ciPath}, so there was nothing to require. Protection with an empty context list reads as configured while admitting every PR, so none was applied.`
7417
+ });
7344
7418
  } else {
7345
7419
  await github.protectSingleBranch(org, names.dist, "dev", contexts);
7346
7420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.199.2",
3
+ "version": "0.199.4",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",