@biffo/cli 0.165.0 → 0.166.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 +167 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1506,6 +1506,70 @@ var GitHubAdapter = class {
1506
1506
  }
1507
1507
  log.success("Branch protection configured on dev, staging, and main");
1508
1508
  }
1509
+ /**
1510
+ * Protect a single branch with caller-supplied required checks (#803).
1511
+ *
1512
+ * `configureBranchProtection` above exists for **deployable** repos and
1513
+ * protects `dev`, `staging` and `main` with `DEFAULT_STATUS_CHECKS` — the
1514
+ * core workflow's job names. Neither is right for a plugin repo:
1515
+ *
1516
+ * - A plugin repo is **non-deployable** and has `dev` only (AGENTS.md §2).
1517
+ * Reusing the deployable path would demand two branches it should not have,
1518
+ * and `waitForBranch` would hang on them for two minutes each before
1519
+ * failing.
1520
+ * - Its checks are its own CI's job names (`Lint`, `Type Check`, `Test`, …),
1521
+ * not the core workflow's. Requiring a context that is never reported
1522
+ * blocks every PR for ever, on a branch whose CI is entirely green.
1523
+ *
1524
+ * `statusChecks` must be non-empty. Protection that requires nothing is worse
1525
+ * than none: it reads as configured while gating on nothing, so callers that
1526
+ * could not determine the contexts must not silently land here.
1527
+ */
1528
+ async protectSingleBranch(org, repo, branch, statusChecks, protectionIntervalMs = 3e3) {
1529
+ if (statusChecks.length === 0) {
1530
+ throw new Error(
1531
+ `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.`
1532
+ );
1533
+ }
1534
+ log.info(`Waiting for ${branch} branch to be ready...`);
1535
+ await this.waitForBranch(org, repo, branch);
1536
+ log.info(`Configuring branch protection on ${branch}...`);
1537
+ const params = {
1538
+ owner: org,
1539
+ repo,
1540
+ branch,
1541
+ required_status_checks: { strict: true, contexts: statusChecks },
1542
+ enforce_admins: false,
1543
+ required_pull_request_reviews: {
1544
+ required_approving_review_count: 0,
1545
+ dismiss_stale_reviews: false
1546
+ },
1547
+ restrictions: null,
1548
+ required_linear_history: true,
1549
+ allow_force_pushes: false,
1550
+ allow_deletions: false
1551
+ };
1552
+ const deadline = Date.now() + 3e4;
1553
+ while (true) {
1554
+ try {
1555
+ await this.octokit.repos.updateBranchProtection(params);
1556
+ break;
1557
+ } catch (err) {
1558
+ const status = err.status;
1559
+ if (status === 403) {
1560
+ log.warn(`Branch protection unavailable for ${org}/${repo}: ${err.message}`);
1561
+ log.warn(" Add it later via GitHub once the plan allows it, or make the repo public.");
1562
+ return;
1563
+ }
1564
+ if (status !== 404 || Date.now() >= deadline) throw err;
1565
+ log.info("Branch protection endpoint not yet ready, retrying...");
1566
+ await new Promise((resolve18) => setTimeout(resolve18, protectionIntervalMs));
1567
+ }
1568
+ }
1569
+ log.success(
1570
+ `Branch protection configured on ${branch} (${statusChecks.length} required check(s))`
1571
+ );
1572
+ }
1509
1573
  async createEnvironments(config) {
1510
1574
  const { org, repo } = config.source_control.config;
1511
1575
  for (const env of config.environments) {
@@ -6370,6 +6434,50 @@ import { fileURLToPath as fileURLToPath5 } from "url";
6370
6434
  import chalk13 from "chalk";
6371
6435
  import { Command as Command13 } from "commander";
6372
6436
 
6437
+ // src/lib/workflow-check-contexts.ts
6438
+ function unquote(value) {
6439
+ const trimmed = value.trim();
6440
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length > 1 || trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length > 1) {
6441
+ return trimmed.slice(1, -1);
6442
+ }
6443
+ return trimmed;
6444
+ }
6445
+ function workflowCheckContexts(workflow) {
6446
+ const lines = workflow.split("\n");
6447
+ const jobsIndex = lines.findIndex((line) => /^jobs:\s*(#.*)?$/.test(line));
6448
+ if (jobsIndex === -1) return [];
6449
+ const contexts = [];
6450
+ let currentJobId = null;
6451
+ let currentJobIndent = 0;
6452
+ let currentName = null;
6453
+ const flush = () => {
6454
+ if (currentJobId !== null) contexts.push(currentName ?? currentJobId);
6455
+ currentJobId = null;
6456
+ currentName = null;
6457
+ };
6458
+ for (const line of lines.slice(jobsIndex + 1)) {
6459
+ if (line.trim() === "" || /^\s*#/.test(line)) continue;
6460
+ if (!/^\s/.test(line)) break;
6461
+ const jobMatch = /^(\s+)([A-Za-z0-9_-]+):\s*(#.*)?$/.exec(line);
6462
+ if (jobMatch?.[1] !== void 0 && jobMatch[2] !== void 0) {
6463
+ const indent = jobMatch[1].length;
6464
+ if (currentJobId === null || indent === currentJobIndent) {
6465
+ flush();
6466
+ currentJobId = jobMatch[2];
6467
+ currentJobIndent = indent;
6468
+ continue;
6469
+ }
6470
+ }
6471
+ if (currentJobId === null) continue;
6472
+ const nameMatch = /^(\s+)name:\s*(.+?)\s*$/.exec(line);
6473
+ if (nameMatch?.[1] !== void 0 && nameMatch[2] !== void 0 && nameMatch[1].length === currentJobIndent + 2 && currentName === null) {
6474
+ currentName = unquote(nameMatch[2]);
6475
+ }
6476
+ }
6477
+ flush();
6478
+ return contexts;
6479
+ }
6480
+
6373
6481
  // src/lib/plugin-locations.ts
6374
6482
  import { existsSync as existsSync21, readdirSync as readdirSync10 } from "fs";
6375
6483
  import { join as join21 } from "path";
@@ -6728,6 +6836,9 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
6728
6836
  ).option(
6729
6837
  "--standalone",
6730
6838
  "Scaffold a standalone plugin repo (ADR-0003 section 2) into ./biffo-plugin-<name>/ instead of into this checkout, keeping its own CI/CD workflows"
6839
+ ).option(
6840
+ "--org <org>",
6841
+ "With --standalone, also create the GitHub repo under this org (or user), push, and protect dev"
6731
6842
  ).option(
6732
6843
  "--skeleton <path>",
6733
6844
  "Path to the plugin skeleton (defaults to _skeletons/plugin-template)"
@@ -6740,12 +6851,17 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
6740
6851
  {
6741
6852
  firstParty: options.firstParty ?? false,
6742
6853
  standalone: options.standalone ?? false,
6854
+ ...options.org ? { org: options.org } : {},
6743
6855
  ...options.skeleton ? { skeletonRoot: resolve11(options.skeleton) } : {},
6744
6856
  dryRun: options.dryRun ?? false,
6745
6857
  commit: options.commit !== false,
6746
6858
  cwd
6747
6859
  },
6748
- { git: new GitAdapter() }
6860
+ {
6861
+ git: new GitAdapter(),
6862
+ makeGitHub: (token) => new GitHubAdapter(token),
6863
+ resolveToken: resolveGithubToken3
6864
+ }
6749
6865
  );
6750
6866
  } catch (err) {
6751
6867
  log.error(err.message);
@@ -6858,8 +6974,58 @@ async function runStandaloneCreate(names, options, deps) {
6858
6974
  await deps.git.commit(destDir, `feat: scaffold ${names.slug} plugin`);
6859
6975
  log.success(`Initialised a git repo on dev with an initial commit`);
6860
6976
  }
6977
+ if (options.org !== void 0 && options.org !== "") {
6978
+ if (!options.commit) {
6979
+ throw new Error(
6980
+ "--org needs a commit to push. Drop --no-commit, or create the repo yourself using the steps printed by a --no-commit run."
6981
+ );
6982
+ }
6983
+ await createAndPushStandaloneRepo(options.org, names, destDir, deps);
6984
+ return;
6985
+ }
6861
6986
  printStandaloneNextSteps(names, minorOf(manifest.version));
6862
6987
  }
6988
+ async function createAndPushStandaloneRepo(org, names, destDir, deps) {
6989
+ if (deps.makeGitHub === void 0 || deps.resolveToken === void 0) {
6990
+ throw new Error("No GitHub adapter available \u2014 cannot create a repository.");
6991
+ }
6992
+ const token = await deps.resolveToken();
6993
+ const github = deps.makeGitHub(token);
6994
+ const cloneUrl = await github.createEmptyRepo(org, names.dist, `${names.slug} \u2014 a Biffo plugin`);
6995
+ await deps.git.addRemote(destDir, "origin", cloneUrl);
6996
+ await deps.git.push(destDir, "dev", { token });
6997
+ log.success(`Pushed dev to ${org}/${names.dist}`);
6998
+ await github.setDefaultBranch(org, names.dist, "dev");
6999
+ const ciPath = join23(destDir, ".github", "workflows", "ci.yml");
7000
+ const contexts = existsSync23(ciPath) ? workflowCheckContexts(readFileSync17(ciPath, "utf8")) : [];
7001
+ if (contexts.length === 0) {
7002
+ log.warn(
7003
+ `Could not determine required status checks from ${ciPath} \u2014 skipping branch protection. Configure it manually on dev once you know the CI job names.`
7004
+ );
7005
+ } else {
7006
+ await github.protectSingleBranch(org, names.dist, "dev", contexts);
7007
+ }
7008
+ printStandaloneRemoteNextSteps(names, org);
7009
+ }
7010
+ function printStandaloneRemoteNextSteps(names, org) {
7011
+ console.log(chalk13.bold("\n Standalone plugin repo created!\n"));
7012
+ console.log(` https://github.com/${org}/${names.dist} (dev, protected)
7013
+ `);
7014
+ console.log(" Next:");
7015
+ console.log(chalk13.dim(` 1. cd ${names.dist} && edit biffo.plugin.json`));
7016
+ console.log(
7017
+ chalk13.dim(" 2. Add it to the registry so it appears in the plugin store \u2014 either set")
7018
+ );
7019
+ console.log(
7020
+ chalk13.dim(
7021
+ " REGISTRY_PUBLISH_TOKEN on the repo, or add it to the registry\u2019s sources.json"
7022
+ )
7023
+ );
7024
+ console.log(
7025
+ chalk13.dim(` 3. Consumers install it with: biffo plugin install ${names.slug}@<minor>
7026
+ `)
7027
+ );
7028
+ }
6863
7029
  function minorOf(version) {
6864
7030
  const match = /^(\d+)\.(\d+)/.exec(version);
6865
7031
  return match ? `${match[1]}.${match[2]}` : version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.165.0",
3
+ "version": "0.166.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",