@biffo/cli 0.199.1 → 0.199.3

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 +175 -69
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -216,6 +216,34 @@ function latestCoreVersionFromTags(repo, git = defaultTagRunner, options = {}) {
216
216
  var CORE_TAG_PREFIX = "core-v";
217
217
  var defaultTagRunner = (args) => execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
218
218
 
219
+ // src/lib/git-tracked-files.ts
220
+ import { execFileSync as execFileSync2 } from "child_process";
221
+ import { realpathSync } from "fs";
222
+ var defaultGit = (args) => execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
223
+ function gitTrackedFiles(root, git = defaultGit) {
224
+ let top;
225
+ try {
226
+ top = git(["-C", root, "rev-parse", "--show-toplevel"]).trim();
227
+ } catch {
228
+ return null;
229
+ }
230
+ if (!top) return null;
231
+ try {
232
+ if (realpathSync(top) !== realpathSync(root)) return null;
233
+ } catch {
234
+ return null;
235
+ }
236
+ let out;
237
+ try {
238
+ out = git(["-C", root, "ls-files", "-z"]);
239
+ } catch {
240
+ return null;
241
+ }
242
+ const files = out.split("\0").filter((p) => p !== "");
243
+ if (files.length === 0) return null;
244
+ return new Set(files);
245
+ }
246
+
219
247
  // src/lib/core-manifest.ts
220
248
  var CORE_MANIFEST_FILE = "core-manifest.json";
221
249
  var CoreManifestSchema2 = z2.object({
@@ -315,8 +343,9 @@ function isTemplateOwned(relPath, manifest) {
315
343
  function toPosix(p) {
316
344
  return sep === "/" ? p : p.split(sep).join("/");
317
345
  }
318
- function listTemplateOwnedFiles(root, manifest) {
346
+ function listTemplateOwnedFiles(root, manifest, options = {}) {
319
347
  const out = [];
348
+ const tracked = options.trackedOnly ? options.git ? gitTrackedFiles(root, options.git) : gitTrackedFiles(root) : null;
320
349
  function walk(dir) {
321
350
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
322
351
  if (entry.isDirectory() && HARD_EXCLUDED_DIRS.has(entry.name)) continue;
@@ -325,6 +354,7 @@ function listTemplateOwnedFiles(root, manifest) {
325
354
  walk(abs);
326
355
  } else if (entry.isFile()) {
327
356
  const rel = toPosix(relative(root, abs));
357
+ if (tracked && !tracked.has(rel)) continue;
328
358
  if (isTemplateOwned(rel, manifest)) out.push(rel);
329
359
  }
330
360
  }
@@ -335,10 +365,11 @@ function listTemplateOwnedFiles(root, manifest) {
335
365
  function sameFile(a, b) {
336
366
  return readFileSync2(a).equals(readFileSync2(b));
337
367
  }
338
- function computeCoreDiff(templateRoot, instanceRoot, manifest, baseRoot) {
339
- const templateFiles = new Set(listTemplateOwnedFiles(templateRoot, manifest));
368
+ function computeCoreDiff(templateRoot, instanceRoot, manifest, baseRoot, options = {}) {
369
+ const trackedOnly = options.git ? { trackedOnly: true, git: options.git } : { trackedOnly: true };
370
+ const templateFiles = new Set(listTemplateOwnedFiles(templateRoot, manifest, trackedOnly));
340
371
  const instanceFiles = new Set(listTemplateOwnedFiles(instanceRoot, manifest));
341
- const baseFiles = baseRoot ? new Set(listTemplateOwnedFiles(baseRoot, manifest)) : void 0;
372
+ const baseFiles = baseRoot ? new Set(listTemplateOwnedFiles(baseRoot, manifest, trackedOnly)) : void 0;
342
373
  const diff = {
343
374
  added: [],
344
375
  removed: [],
@@ -687,9 +718,10 @@ var EMPTY_SUMMARY = () => ({
687
718
  });
688
719
  async function planCoreUpgrade(options) {
689
720
  const mergeFile = options.mergeFile ?? gitMergeFile;
690
- const base = new Set(listTemplateOwnedFiles(options.baseDir, options.manifest));
721
+ const trackedOnly = { trackedOnly: true };
722
+ const base = new Set(listTemplateOwnedFiles(options.baseDir, options.manifest, trackedOnly));
691
723
  const ours = new Set(listTemplateOwnedFiles(options.oursDir, options.manifest));
692
- const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest));
724
+ const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest, trackedOnly));
693
725
  const divergentPrefixes = readDivergenceConfig(options.oursDir).warnOnly.map((e) => e.prefix);
694
726
  const isDeclaredDivergent = (path) => divergentPrefixes.some((prefix) => path.startsWith(prefix));
695
727
  const paths = [.../* @__PURE__ */ new Set([...base, ...ours, ...theirs])].sort();
@@ -1190,16 +1222,37 @@ function formatBranchProtectionSummary(outcomes) {
1190
1222
  ];
1191
1223
  for (const outcome of unprotected) {
1192
1224
  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})`);
1225
+ lines.push(
1226
+ ` ${outcome.org}/${outcome.repo} \u2014 unprotected: ${left} (${whyUnprotected(outcome)})`
1227
+ );
1195
1228
  if (outcome.reason) lines.push(` ${outcome.reason}`);
1196
1229
  }
1197
1230
  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)"
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)")
1200
1241
  );
1201
1242
  return lines;
1202
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
+ }
1203
1256
  function reportBranchProtectionSummary() {
1204
1257
  const outcomes = pending.splice(0, pending.length);
1205
1258
  const lines = formatBranchProtectionSummary(outcomes);
@@ -1722,51 +1775,91 @@ var GitHubAdapter = class {
1722
1775
  * `statusChecks` must be non-empty. Protection that requires nothing is worse
1723
1776
  * than none: it reads as configured while gating on nothing, so callers that
1724
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.
1725
1785
  */
1726
1786
  async protectSingleBranch(org, repo, branch, statusChecks, protectionIntervalMs = 3e3) {
1727
1787
  if (statusChecks.length === 0) {
1728
- throw new Error(
1729
- `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.`
1730
- );
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);
1731
1798
  }
1732
- log.info(`Waiting for ${branch} branch to be ready...`);
1733
- await this.waitForBranch(org, repo, branch);
1734
- log.info(`Configuring branch protection on ${branch}...`);
1735
- const params = {
1736
- owner: org,
1737
- repo,
1738
- branch,
1739
- required_status_checks: { strict: true, contexts: statusChecks },
1740
- enforce_admins: false,
1741
- required_pull_request_reviews: {
1742
- required_approving_review_count: 0,
1743
- dismiss_stale_reviews: false
1744
- },
1745
- restrictions: null,
1746
- required_linear_history: true,
1747
- allow_force_pushes: false,
1748
- allow_deletions: false
1749
- };
1750
- const deadline = Date.now() + 3e4;
1751
- while (true) {
1752
- try {
1753
- await this.octokit.repos.updateBranchProtection(params);
1754
- break;
1755
- } catch (err) {
1756
- const status = err.status;
1757
- if (status === 403) {
1758
- log.warn(`Branch protection unavailable for ${org}/${repo}: ${err.message}`);
1759
- log.warn(" Add it later via GitHub once the plan allows it, or make the repo public.");
1760
- 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));
1761
1840
  }
1762
- if (status !== 404 || Date.now() >= deadline) throw err;
1763
- log.info("Branch protection endpoint not yet ready, retrying...");
1764
- await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
1765
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;
1766
1852
  }
1767
1853
  log.success(
1768
1854
  `Branch protection configured on ${branch} (${statusChecks.length} required check(s))`
1769
1855
  );
1856
+ return recordBranchProtectionOutcome({
1857
+ status: "applied",
1858
+ org,
1859
+ repo,
1860
+ protectedBranches: [branch],
1861
+ unprotectedBranches: []
1862
+ });
1770
1863
  }
1771
1864
  async createEnvironments(config) {
1772
1865
  const { org, repo } = config.source_control.config;
@@ -2401,7 +2494,7 @@ function applyMigrationCarry(instanceDir, plan) {
2401
2494
  }
2402
2495
 
2403
2496
  // src/lib/core-template-trees.ts
2404
- import { execFileSync as execFileSync2 } from "child_process";
2497
+ import { execFileSync as execFileSync3 } from "child_process";
2405
2498
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "fs";
2406
2499
  import { tmpdir as tmpdir3 } from "os";
2407
2500
  import { join as join8 } from "path";
@@ -2409,8 +2502,8 @@ function coreTag(version) {
2409
2502
  parseCoreVersion(version);
2410
2503
  return `core-v${version}`;
2411
2504
  }
2412
- var defaultGit = (args) => execFileSync2("git", args, { encoding: "utf8" });
2413
- function workingTreeMatchesTag(repo, version, git = defaultGit) {
2505
+ var defaultGit2 = (args) => execFileSync3("git", args, { encoding: "utf8" });
2506
+ function workingTreeMatchesTag(repo, version, git = defaultGit2) {
2414
2507
  const tag = coreTag(version);
2415
2508
  try {
2416
2509
  const head = git(["-C", repo, "rev-parse", "HEAD"]).trim();
@@ -2430,7 +2523,7 @@ function tagExists(repo, tag, git) {
2430
2523
  return false;
2431
2524
  }
2432
2525
  }
2433
- function materializeTemplateAtTag(repo, version, git = defaultGit) {
2526
+ function materializeTemplateAtTag(repo, version, git = defaultGit2) {
2434
2527
  const tag = coreTag(version);
2435
2528
  if (!tagExists(repo, tag, git)) {
2436
2529
  try {
@@ -2447,7 +2540,7 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
2447
2540
  const tarball = join8(dir, ".tree.tar");
2448
2541
  try {
2449
2542
  git(["-C", repo, "archive", "--format=tar", "-o", tarball, tag]);
2450
- execFileSync2("tar", ["-x", "-f", tarball, "-C", dir]);
2543
+ execFileSync3("tar", ["-x", "-f", tarball, "-C", dir]);
2451
2544
  rmSync3(tarball, { force: true });
2452
2545
  } catch (err) {
2453
2546
  rmSync3(dir, { recursive: true, force: true });
@@ -7150,25 +7243,29 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
7150
7243
  async (name, options) => {
7151
7244
  const cwd = options.cwd ? resolve11(options.cwd) : process.cwd();
7152
7245
  try {
7153
- await runPluginCreate(
7154
- name,
7155
- {
7156
- firstParty: options.firstParty ?? false,
7157
- standalone: options.standalone ?? false,
7158
- ...options.org ? { org: options.org } : {},
7159
- ...options.runnerLabel ? { runnerLabel: options.runnerLabel } : {},
7160
- register: options.register !== false,
7161
- ...options.skeleton ? { skeletonRoot: resolve11(options.skeleton) } : {},
7162
- dryRun: options.dryRun ?? false,
7163
- commit: options.commit !== false,
7164
- cwd
7165
- },
7166
- {
7167
- git: new GitAdapter(),
7168
- makeGitHub: (token) => new GitHubAdapter(token),
7169
- resolveToken: resolveGithubToken3
7170
- }
7171
- );
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
+ }
7172
7269
  } catch (err) {
7173
7270
  log.error(err.message);
7174
7271
  process.exit(1);
@@ -7184,6 +7281,7 @@ async function runPluginCreate(name, options, deps) {
7184
7281
  );
7185
7282
  }
7186
7283
  await runStandaloneCreate(names, options, deps);
7284
+ reportBranchProtectionSummary();
7187
7285
  return;
7188
7286
  }
7189
7287
  const isInstance = existsSync23(join23(options.cwd, INSTANCE_CORE_FILE));
@@ -7309,6 +7407,14 @@ async function createAndPushStandaloneRepo(org, names, destDir, options, deps) {
7309
7407
  log.warn(
7310
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.`
7311
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
+ });
7312
7418
  } else {
7313
7419
  await github.protectSingleBranch(org, names.dist, "dev", contexts);
7314
7420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.199.1",
3
+ "version": "0.199.3",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",