@bensandee/tooling 0.79.0 → 0.80.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/bin.mjs CHANGED
@@ -397,7 +397,7 @@ function loadEnvFileForBin(rawArgs, io = processIo) {
397
397
  if (resolveJsonMode(rawArgs)) {
398
398
  const document = buildErrorDocument({
399
399
  command: resolveCommandName(rawArgs),
400
- version: "0.79.0",
400
+ version: "0.80.0",
401
401
  kind: "usage",
402
402
  message
403
403
  });
@@ -764,6 +764,11 @@ const AsciiConfigSchema = z.strictObject({
764
764
  treatAsProse: z.array(z.string()).optional().meta({ description: "Path globs (matched the same way as exclude) whose files --fix classifies as prose wholesale regardless of extension, flattening rather than flagging. The opt-back-in for an extension with no dedicated lexical-context classifier where blind flattening is known safe. No built-in defaults." })
765
765
  });
766
766
  const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional().meta({ description: "Make `bst lint:types` (the type-aware oxlint gate, backed by oxlint-tsgolint) exit non-zero on any finding while the tooling is still in its report phase (default false; the enforce phase, a later tooling release, exits non-zero unconditionally). A missing oxlint-tsgolint binary is also a hard failure rather than a notice while this is set: arming the gate is what turns a platform gap into a build break, deliberately, because the repo that armed it is the one that asked for it." }) });
767
+ const ChecksConfigSchema = z.strictObject({
768
+ add: z.array(z.string()).optional().meta({ description: "Extra package.json scripts to run as first-class check steps, appended to the built-in list (e.g. ['lint:php']). An added step with no matching script is a hard failure." }),
769
+ skip: z.array(z.string()).optional().meta({ description: "Check steps to skip, unioned with the built-in CI defaults (never replacing them). Supports glob patterns." }),
770
+ runOnVersionPr: z.boolean().optional().meta({ description: "Run the CI check job on the changesets version PR (changeset-release/main). Default false: the generated check job is skipped there, because that branch is regenerated on every push to main and main's own run gates the release. Has no effect unless releaseStrategy is changesets." })
771
+ });
767
772
  const CoverageConfigSchema = z.strictObject({
768
773
  history: z.enum(["orphan-branch", "none"]).optional().meta({ description: "How coverage history is persisted. 'orphan-branch' (default) appends to a dedicated branch; 'none' disables CI history but keeps the local ratchet." }),
769
774
  historyBranch: z.string().optional().meta({ description: "Orphan branch name for coverage history (default: 'bst/coverage-metrics')" }),
@@ -1892,10 +1897,6 @@ const BranchPruneConfigSchema = z.strictObject({
1892
1897
  exclude: z.array(z.string()).optional().meta({ description: "Branch-name globs added to bst branch:prune's built-in never-touch list (main, changeset-release/*, renovate/*, bst/coverage-metrics, bst/health-metrics, bst/egress-metrics, anything with an open PR or protected). Additive only -- there is no key that removes a built-in entry." })
1893
1898
  });
1894
1899
  const IssuesConfigSchema = z.strictObject({ assignees: z.array(z.string().min(1)).optional().meta({ description: "The repo's default reviewer(s) for issues opened by `bst issue:create` -- assigned when the invocation names no --assignee and no --repo. An explicit --assignee replaces this default rather than adding to it; --no-assignee opens an issue deliberately unassigned." }) });
1895
- const ChecksConfigSchema = z.strictObject({
1896
- add: z.array(z.string()).optional().meta({ description: "Extra package.json scripts to run as first-class check steps, appended to the built-in list (e.g. ['lint:php']). An added step with no matching script is a hard failure." }),
1897
- skip: z.array(z.string()).optional().meta({ description: "Check steps to skip, unioned with the built-in CI defaults (never replacing them). Supports glob patterns." })
1898
- });
1899
1900
  /** One label definition inside a `labelSets` entry -- the same shape `bst forgejo:labels sync`
1900
1901
  * writes to the forge (`AgentLabelSpec` in `@bensandee/forgejo/label-provisioning`), expressed as a
1901
1902
  * zod schema so a malformed entry in `.tooling.json` fails at load time rather than at the first API
@@ -5225,6 +5226,7 @@ function buildWorkflowYaml(options) {
5225
5226
  if (options.permissions) doc["permissions"] = options.permissions;
5226
5227
  if (options.concurrency) doc["concurrency"] = options.concurrency;
5227
5228
  const jobs = { [options.jobName]: {
5229
+ ...options.jobIf && { if: options.jobIf },
5228
5230
  "runs-on": options.runsOn ?? "ubuntu-latest",
5229
5231
  ...options.jobEnv && { env: options.jobEnv },
5230
5232
  ...options.jobOutputs && { outputs: options.jobOutputs },
@@ -6712,7 +6714,7 @@ function buildManagedDevDeps(ctx) {
6712
6714
  const devDeps = { ...rootDevDeps(ctx.config) };
6713
6715
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
6714
6716
  if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.5";
6715
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.79.0";
6717
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.80.0";
6716
6718
  if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.15.2";
6717
6719
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
6718
6720
  "@changesets/cli": "3.0.3",
@@ -9182,6 +9184,16 @@ async function generateReleaseCi(ctx) {
9182
9184
  };
9183
9185
  }
9184
9186
  //#endregion
9187
+ //#region src/release/version-branch.ts
9188
+ /**
9189
+ * The branch the changesets release flow writes its version commit to, and so the head branch of
9190
+ * the version PR. `bst release:changesets` force-pushes it on Forgejo; on GitHub it is
9191
+ * `changesets/action@v1`'s default, `changeset-release/<base>`. Not configurable. The CI generator
9192
+ * reads it too, to skip the check job on that PR, which is why it lives at `release/`'s root
9193
+ * rather than in `publish/`.
9194
+ */
9195
+ const VERSION_PR_BRANCH = "changeset-release/main";
9196
+ //#endregion
9185
9197
  //#region src/generators/ci.ts
9186
9198
  function coverageHistorySteps() {
9187
9199
  return [
@@ -9381,6 +9393,8 @@ async function generateCi(ctx) {
9381
9393
  ...emitCoverageSteps || emitHealthHistory ? IS_MAIN_JOB_ENV : {},
9382
9394
  ...npmAuth.jobEnv
9383
9395
  };
9396
+ const checksSlice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ checks: ChecksConfigSchema.optional() }), { block: "checks" });
9397
+ const skipOnVersionPr = isChangesets && checksSlice?.checks?.runOnVersionPr !== true;
9384
9398
  const content = buildWorkflowYaml({
9385
9399
  ci: ctx.config.ci,
9386
9400
  name: "CI",
@@ -9395,6 +9409,7 @@ async function generateCi(ctx) {
9395
9409
  enableEmailNotifications: isForgejo,
9396
9410
  ...isChangesets && { concurrency: ciConcurrency(ctx.config.ci) },
9397
9411
  jobName: "check",
9412
+ ...skipOnVersionPr && { jobIf: `github.head_ref != 'changeset-release/main'` },
9398
9413
  ...Object.keys(checkJobEnv).length > 0 && { jobEnv: checkJobEnv },
9399
9414
  ...jobOutputs && { jobOutputs },
9400
9415
  steps,
@@ -10660,7 +10675,11 @@ const GIT_GUIDE_BODY = [
10660
10675
  "",
10661
10676
  "## Commit subjects",
10662
10677
  "",
10663
- "Follow Conventional Commits: `<type>(<scope>): <summary>`. The type is one of `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `build`, `ci`; the scope is optional and names the package or area touched; the summary is an imperative, plain-language sentence. This binds a commit message you suggest to the user too. Nothing checks it, so do not copy the style of recent `git log` if it has drifted."
10678
+ "Follow Conventional Commits: `<type>(<scope>): <summary>`. The type is one of `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `build`, `ci`; the scope is optional and names the package or area touched; the summary is an imperative, plain-language sentence. This binds a commit message you suggest to the user too. Nothing checks it, so do not copy the style of recent `git log` if it has drifted.",
10679
+ "",
10680
+ "## Pull request titles",
10681
+ "",
10682
+ "Start a pull request's title with the stage it carries: `Spec:` for a spec, `Plan:` for a plan, and no stem once it carries the implementation. A draft keeps its `WIP:` marker in front (`WIP: Plan: Add retry to uploads`). When a pull request moves on to the next stage, move the stem with it. This is informational -- nothing checks it and nothing fails without it -- but it tells a reviewer at a glance what they are reading."
10664
10683
  ].join("\n");
10665
10684
  /**
10666
10685
  * The plan lifecycle. `bst docs:check` owns the mechanics; this states the one rule it cannot
@@ -11324,7 +11343,7 @@ function reportFailures(failed, command) {
11324
11343
  function generateMigratePrompt(results, config, detected) {
11325
11344
  const sections = [];
11326
11345
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11327
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.79.0 repo:sync\` on ${timestamp}_\n`);
11346
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.80.0 repo:sync\` on ${timestamp}_\n`);
11328
11347
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
11329
11348
  if (changed.length > 0) {
11330
11349
  const items = changed.map((r) => {
@@ -12350,6 +12369,32 @@ function execStep(cmd, cwd, sink = process.stdout) {
12350
12369
  });
12351
12370
  }
12352
12371
  //#endregion
12372
+ //#region src/checks/skip-report.ts
12373
+ function matches(name, patterns) {
12374
+ return patterns.size > 0 && picomatch.isMatch(name, [...patterns]);
12375
+ }
12376
+ /**
12377
+ * Which steps each skip source removed, for reporting. Only a step with a defined script is
12378
+ * named: one without a script was never going to run, so a repo with no Docker is never told
12379
+ * `docker:build` was skipped. A step matched by both sources is credited to `--skip`, the one
12380
+ * the caller typed.
12381
+ */
12382
+ function groupSkippedSteps(steps, defined, flagSkip, configSkip) {
12383
+ const bySource = {
12384
+ "--skip": [],
12385
+ "checks.skip": []
12386
+ };
12387
+ for (const name of steps) {
12388
+ if (!defined.has(name)) continue;
12389
+ if (matches(name, flagSkip)) bySource["--skip"].push(name);
12390
+ else if (matches(name, configSkip)) bySource["checks.skip"].push(name);
12391
+ }
12392
+ return ["--skip", "checks.skip"].filter((source) => bySource[source].length > 0).map((source) => ({
12393
+ source,
12394
+ steps: bySource[source]
12395
+ }));
12396
+ }
12397
+ //#endregion
12353
12398
  //#region src/utils/branch.ts
12354
12399
  const BRANCH_REF_PREFIX = "refs/heads/";
12355
12400
  function defaultGitRunner(args, cwd) {
@@ -12601,8 +12646,20 @@ function defaultGetScripts(targetDir) {
12601
12646
  }
12602
12647
  }
12603
12648
  const rawLog$2 = (msg) => console.log(msg);
12604
- function advisorySummary(warned) {
12605
- return `All checks passed (${String(warned.length)} advisory: ${warned.map((w) => w.name).join(", ")})`;
12649
+ /**
12650
+ * The skipped names on a failing run, as a line of their own. Never folded into the
12651
+ * `Failed checks: <names>` line: the agent's verify backstop (`parseFailingStep`) reads
12652
+ * everything after it as step names and discards the lot on anything else.
12653
+ */
12654
+ function failedRunSkippedLine(skipped) {
12655
+ return skipped.length === 0 ? null : `Skipped: ${skipped.join(", ")}`;
12656
+ }
12657
+ function passedSummary(warned, skipped) {
12658
+ const parts = [...warned.length > 0 ? [`${String(warned.length)} advisory: ${warned.map((w) => w.name).join(", ")}`] : [], ...skipped.length > 0 ? [`skipped: ${skipped.join(", ")}`] : []];
12659
+ return parts.length === 0 ? "All checks passed" : `All checks passed (${parts.join("; ")})`;
12660
+ }
12661
+ function skippedLine(group) {
12662
+ return `Skipped (${group.source}): ${group.steps.join(", ")}`;
12606
12663
  }
12607
12664
  const ciReporter = {
12608
12665
  groupStart: (name) => rawLog$2(`::group::${name}`),
@@ -12617,9 +12674,16 @@ const ciReporter = {
12617
12674
  rawLog$2(`::error::${name} failed`);
12618
12675
  },
12619
12676
  undefinedCheck: (name) => rawLog$2(`::error::${name} not defined in package.json`),
12677
+ skipped: (groups) => {
12678
+ for (const group of groups) rawLog$2(skippedLine(group));
12679
+ },
12620
12680
  skippedNotDefined: (names) => rawLog$2(`Skipped (not defined): ${names.join(", ")}`),
12621
- allPassed: (warned) => rawLog$2(warned.length === 0 ? "[ok] All checks passed" : `[ok] ${advisorySummary(warned)}`),
12622
- anyFailed: (names) => rawLog$2(`::error::Failed checks: ${names.join(", ")}`)
12681
+ allPassed: (warned, skipped) => rawLog$2(`[ok] ${passedSummary(warned, skipped)}`),
12682
+ anyFailed: (names, skipped) => {
12683
+ const skippedNames = failedRunSkippedLine(skipped);
12684
+ if (skippedNames !== null) rawLog$2(skippedNames);
12685
+ rawLog$2(`::error::Failed checks: ${names.join(", ")}`);
12686
+ }
12623
12687
  };
12624
12688
  const localReporter = {
12625
12689
  groupStart: (_name) => {},
@@ -12628,16 +12692,35 @@ const localReporter = {
12628
12692
  warned: (name, _elapsedS, count) => log$1.warn(`${name}: ${String(count)} advisory finding(s)`),
12629
12693
  failed: (name) => log$1.error(`${name} failed`),
12630
12694
  undefinedCheck: (name) => log$1.error(`${name} not defined in package.json`),
12695
+ skipped: (groups) => {
12696
+ for (const group of groups) log$1.info(skippedLine(group));
12697
+ },
12631
12698
  skippedNotDefined: (names) => log$1.info(`Skipped (not defined): ${names.join(", ")}`),
12632
- allPassed: (warned) => warned.length === 0 ? log$1.success("All checks passed") : log$1.success(advisorySummary(warned)),
12633
- anyFailed: (names) => log$1.error(`Failed checks: ${names.join(", ")}`)
12699
+ allPassed: (warned, skipped) => log$1.success(passedSummary(warned, skipped)),
12700
+ anyFailed: (names, skipped) => {
12701
+ const skippedNames = failedRunSkippedLine(skipped);
12702
+ if (skippedNames !== null) log$1.info(skippedNames);
12703
+ log$1.error(`Failed checks: ${names.join(", ")}`);
12704
+ }
12634
12705
  };
12706
+ /**
12707
+ * Name every skipped step before the first one runs, and return the names for the summary: a
12708
+ * step that silently vanishes from the run reads as a bug in the pipeline rather than a skip
12709
+ * someone asked for.
12710
+ */
12711
+ function reportSkipped(reporter, names, defined, flagSkip, configSkip) {
12712
+ const groups = groupSkippedSteps(names, defined, flagSkip, configSkip);
12713
+ reporter.skipped(groups);
12714
+ return groups.flatMap((group) => group.steps);
12715
+ }
12635
12716
  async function runRunChecks(targetDir, options = {}) {
12636
12717
  const exec = options.execCommand ?? execStep;
12637
12718
  const getScripts = options.getScripts ?? defaultGetScripts;
12638
12719
  const toolingConfig = options.toolingConfig ?? loadToolingConfig(targetDir);
12639
12720
  const configChecks = toolingConfig?.checks;
12640
- const skip = /* @__PURE__ */ new Set([...options.skip ?? [], ...configChecks?.skip ?? []]);
12721
+ const flagSkip = new Set(options.skip ?? []);
12722
+ const configSkip = new Set(configChecks?.skip ?? []);
12723
+ const skip = /* @__PURE__ */ new Set([...flagSkip, ...configSkip]);
12641
12724
  const add = [.../* @__PURE__ */ new Set([...options.add ?? [], ...configChecks?.add ?? []])];
12642
12725
  const isCI = Boolean(process.env["CI"]);
12643
12726
  const failFast = options.failFast ?? !isCI;
@@ -12656,6 +12739,7 @@ async function runRunChecks(targetDir, options = {}) {
12656
12739
  if (coverage.enabled && !useCoverageScript && coverage.source === "flag") log$1.warn("--with-coverage requested but test:coverage script is not defined; falling back to test");
12657
12740
  if (coverage.enabled && !useCoverageScript && coverage.source === "config") log$1.warn("coverage.gateOnPullRequests is set but test:coverage script is not defined; falling back to test");
12658
12741
  debug(vc, `Coverage: ${options.coverage ?? "auto"} (source: ${coverage.source}) -> ${useCoverageScript ? "test:coverage" : "test"}`);
12742
+ const skippedSteps = reportSkipped(reporter, allChecks.map((check) => check.name), definedScripts, flagSkip, configSkip);
12659
12743
  for (const check of allChecks) {
12660
12744
  if (shouldSkip(check.name, skip)) continue;
12661
12745
  if (!definedScripts.has(check.name)) {
@@ -12694,10 +12778,10 @@ async function runRunChecks(targetDir, options = {}) {
12694
12778
  }
12695
12779
  if (notDefined.length > 0) reporter.skippedNotDefined(notDefined);
12696
12780
  if (failures.length > 0) {
12697
- reporter.anyFailed(failures);
12781
+ reporter.anyFailed(failures, skippedSteps);
12698
12782
  return 1;
12699
12783
  }
12700
- reporter.allPassed(warnedSteps);
12784
+ reporter.allPassed(warnedSteps, skippedSteps);
12701
12785
  return 0;
12702
12786
  }
12703
12787
  /**
@@ -13733,9 +13817,8 @@ function childOutput(result) {
13733
13817
  }
13734
13818
  //#endregion
13735
13819
  //#region src/release/publish/version.ts
13736
- const BRANCH = "changeset-release/main";
13737
13820
  const MAIN_REF = "refs/heads/main";
13738
- const BRANCH_REF = `refs/heads/${BRANCH}`;
13821
+ const BRANCH_REF = `refs/heads/${VERSION_PR_BRANCH}`;
13739
13822
  /**
13740
13823
  * Read one remote ref's SHA, without mutating the local repository or its remote-tracking refs.
13741
13824
  *
@@ -13788,7 +13871,7 @@ function isSuperseded(executor, config, auth, baseSha) {
13788
13871
  return false;
13789
13872
  }
13790
13873
  if (remoteMain === baseSha) return false;
13791
- log$1.warn(`Superseded release run: this job versioned ${baseSha}, but main has advanced to ${remoteMain}. A newer run is versioning a superset of these changesets -- skipping to avoid overwriting its version commit on ${BRANCH}.`);
13874
+ log$1.warn(`Superseded release run: this job versioned ${baseSha}, but main has advanced to ${remoteMain}. A newer run is versioning a superset of these changesets -- skipping to avoid overwriting its version commit on ${VERSION_PR_BRANCH}.`);
13792
13875
  return true;
13793
13876
  }
13794
13877
  /** Pattern matching a dateless version heading like `## 1.2.3` or `## 1.2.3-beta.0`. */
@@ -13895,9 +13978,9 @@ async function runVersionMode(executor, config) {
13895
13978
  const baseShaResult = executor.exec("git", ["rev-parse", "HEAD"], { cwd: config.cwd });
13896
13979
  const baseSha = baseShaResult.exitCode === 0 ? baseShaResult.stdout.trim() : "";
13897
13980
  const leaseTip = config.dryRun ? null : readRemoteRef(executor, config, auth, BRANCH_REF);
13898
- if (leaseTip === null && !config.dryRun) throw new FatalError(`Could not read the current tip of ${BRANCH} from the remote, so this run cannot take a push lease. Refusing to force-push without one -- an unleased push is what allows a concurrent run's version commit to be silently discarded.`);
13981
+ if (leaseTip === null && !config.dryRun) throw new FatalError(`Could not read the current tip of ${VERSION_PR_BRANCH} from the remote, so this run cannot take a push lease. Refusing to force-push without one -- an unleased push is what allows a concurrent run's version commit to be silently discarded.`);
13899
13982
  debug(config, `Base commit: ${baseSha || "(unknown)"}`);
13900
- debug(config, `Lease tip for ${BRANCH}: ${leaseTip === null ? "(dry-run)" : leaseTip || "(absent)"}`);
13983
+ debug(config, `Lease tip for ${VERSION_PR_BRANCH}: ${leaseTip === null ? "(dry-run)" : leaseTip || "(absent)"}`);
13901
13984
  const packagesBefore = executor.listWorkspacePackages(config.cwd);
13902
13985
  debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
13903
13986
  const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
@@ -13959,7 +14042,7 @@ async function runVersionMode(executor, config) {
13959
14042
  pr: "none"
13960
14043
  };
13961
14044
  if (leaseTip === null) throw new UnexpectedError("Push lease was never observed on a non-dry-run release");
13962
- debug(config, `Force-pushing version commit to ${BRANCH} (lease: ${leaseTip || "(absent)"})`);
14045
+ debug(config, `Force-pushing version commit to ${VERSION_PR_BRANCH} (lease: ${leaseTip || "(absent)"})`);
13963
14046
  const pushResult = executor.exec("git", [
13964
14047
  ...auth.args,
13965
14048
  "push",
@@ -13968,18 +14051,18 @@ async function runVersionMode(executor, config) {
13968
14051
  `--force-with-lease=${BRANCH_REF}:${leaseTip}`
13969
14052
  ], { cwd: config.cwd });
13970
14053
  debugExec(config, "git push", pushResult);
13971
- if (pushResult.exitCode !== 0) throw new FatalError(`git push to ${BRANCH} failed (exit code ${String(pushResult.exitCode)}). If the lease was rejected, the branch moved after this run started (expected ${leaseTip || "the branch to be absent"}) -- another run pushed a version commit, and overwriting it would discard that release. This job fails rather than clobber it.\n${pushResult.stderr || pushResult.stdout}`);
14054
+ if (pushResult.exitCode !== 0) throw new FatalError(`git push to ${VERSION_PR_BRANCH} failed (exit code ${String(pushResult.exitCode)}). If the lease was rejected, the branch moved after this run started (expected ${leaseTip || "the branch to be absent"}) -- another run pushed a version commit, and overwriting it would discard that release. This job fails rather than clobber it.\n${pushResult.stderr || pushResult.stdout}`);
13972
14055
  const conn = {
13973
14056
  serverUrl: config.serverUrl,
13974
14057
  repository: config.repository,
13975
14058
  token: config.token
13976
14059
  };
13977
- const existingPr = await findOpenPr(executor, conn, BRANCH);
13978
- debug(config, `Existing open PR for ${BRANCH}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
14060
+ const existingPr = await findOpenPr(executor, conn, VERSION_PR_BRANCH);
14061
+ debug(config, `Existing open PR for ${VERSION_PR_BRANCH}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
13979
14062
  if (existingPr === null) try {
13980
14063
  await createPr(executor, conn, {
13981
14064
  title,
13982
- head: BRANCH,
14065
+ head: VERSION_PR_BRANCH,
13983
14066
  base: "main",
13984
14067
  body
13985
14068
  });
@@ -13990,7 +14073,7 @@ async function runVersionMode(executor, config) {
13990
14073
  };
13991
14074
  } catch (error) {
13992
14075
  debug(config, `createPr failed, retrying as update: ${describeError(error)}`);
13993
- const racePr = await findOpenPr(executor, conn, BRANCH);
14076
+ const racePr = await findOpenPr(executor, conn, VERSION_PR_BRANCH);
13994
14077
  if (racePr === null) throw error;
13995
14078
  await updatePr(executor, conn, racePr, {
13996
14079
  title,
@@ -14667,8 +14750,6 @@ z.object({
14667
14750
  });
14668
14751
  //#endregion
14669
14752
  //#region src/release/publish/version-merge.ts
14670
- /** The changesets version-PR branch this command merges. Not configurable. */
14671
- const HEAD_BRANCH = "changeset-release/main";
14672
14753
  /**
14673
14754
  * Emptiness outranks mergeability: an empty version PR must be reported as content-free, not as a
14674
14755
  * blocked merge -- that is the clobbered state this gate exists to catch. `unconfirmed` exists
@@ -14786,8 +14867,8 @@ function evaluateConfirmingRead(second, first, delayMs) {
14786
14867
  };
14787
14868
  }
14788
14869
  async function mergeVersionPrForgejo(executor, conn, options) {
14789
- const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
14790
- if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
14870
+ const prNumber = await findOpenPr(executor, conn, VERSION_PR_BRANCH);
14871
+ if (prNumber === null) throw new FatalError(`No open PR found for branch ${VERSION_PR_BRANCH}`);
14791
14872
  const sleep$1 = options.sleep ?? sleep;
14792
14873
  const recheckDelayMs = envMsOverride(process.env["BST_TEST_VERSION_MERGE_RECHECK_MS"], RECHECK_DELAY_MS);
14793
14874
  const firstGate = await readMergeGate(executor, conn, prNumber);
@@ -14803,7 +14884,7 @@ async function mergeVersionPrForgejo(executor, conn, options) {
14803
14884
  }
14804
14885
  const headSha = headShaOrThrow(verdict);
14805
14886
  if (options.dryRun) {
14806
- log$1.info(`[dry-run] Would merge PR #${String(prNumber)} at ${headSha} and delete branch ${HEAD_BRANCH}`);
14887
+ log$1.info(`[dry-run] Would merge PR #${String(prNumber)} at ${headSha} and delete branch ${VERSION_PR_BRANCH}`);
14807
14888
  return;
14808
14889
  }
14809
14890
  await mergePr(executor, conn, prNumber, {
@@ -14811,7 +14892,7 @@ async function mergeVersionPrForgejo(executor, conn, options) {
14811
14892
  deleteBranch: true,
14812
14893
  expectedHeadSha: headSha
14813
14894
  });
14814
- log$1.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
14895
+ log$1.info(`Merged PR #${String(prNumber)} and deleted branch ${VERSION_PR_BRANCH}`);
14815
14896
  }
14816
14897
  const GitHubVersionPrSchema = z.object({
14817
14898
  number: z.number(),
@@ -14825,13 +14906,13 @@ function mergeVersionPrGitHub(executor, options) {
14825
14906
  const view = executor.exec("gh", [
14826
14907
  "pr",
14827
14908
  "view",
14828
- HEAD_BRANCH,
14909
+ VERSION_PR_BRANCH,
14829
14910
  "--json",
14830
14911
  "number,state,isDraft,mergeable,headRefOid,changedFiles"
14831
14912
  ], { cwd: process.cwd() });
14832
- if (view.exitCode !== 0) throw new FatalError(`Failed to read PR for branch ${HEAD_BRANCH}: ${view.stderr || view.stdout || "unknown error"}`);
14913
+ if (view.exitCode !== 0) throw new FatalError(`Failed to read PR for branch ${VERSION_PR_BRANCH}: ${view.stderr || view.stdout || "unknown error"}`);
14833
14914
  const parsed = GitHubVersionPrSchema.safeParse(JSON.parse(view.stdout));
14834
- if (!parsed.success) throw new FatalError(`Unexpected 'gh pr view' response for branch ${HEAD_BRANCH}: ${parsed.error.message}`);
14915
+ if (!parsed.success) throw new FatalError(`Unexpected 'gh pr view' response for branch ${VERSION_PR_BRANCH}: ${parsed.error.message}`);
14835
14916
  const data = parsed.data;
14836
14917
  const emptiness = data.changedFiles === 0 ? "empty" : "non-empty";
14837
14918
  const mergeable = data.mergeable === "CONFLICTING" ? "conflicted" : "mergeable";
@@ -14845,20 +14926,20 @@ function mergeVersionPrGitHub(executor, options) {
14845
14926
  emptiness
14846
14927
  }));
14847
14928
  if (options.dryRun) {
14848
- log$1.info(`[dry-run] Would merge PR #${String(data.number)} at ${headSha} and delete branch ${HEAD_BRANCH}`);
14929
+ log$1.info(`[dry-run] Would merge PR #${String(data.number)} at ${headSha} and delete branch ${VERSION_PR_BRANCH}`);
14849
14930
  return;
14850
14931
  }
14851
14932
  const result = executor.exec("gh", [
14852
14933
  "pr",
14853
14934
  "merge",
14854
- HEAD_BRANCH,
14935
+ VERSION_PR_BRANCH,
14855
14936
  "--merge",
14856
14937
  "--delete-branch",
14857
14938
  "--match-head-commit",
14858
14939
  headSha
14859
14940
  ], { cwd: process.cwd() });
14860
14941
  if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
14861
- log$1.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
14942
+ log$1.info(`Merged changesets PR and deleted branch ${VERSION_PR_BRANCH}`);
14862
14943
  }
14863
14944
  //#endregion
14864
14945
  //#region src/commands/changesets-merge.ts
@@ -30445,7 +30526,7 @@ setForgejoLogger(log$1);
30445
30526
  * `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
30446
30527
  * can't pass itself off as the published release whose version it carries.
30447
30528
  */
30448
- const displayVersion = "0.79.0";
30529
+ const displayVersion = "0.80.0";
30449
30530
  /**
30450
30531
  * Wait until a stream's internally-buffered data has been flushed to the OS.
30451
30532
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -577,6 +577,10 @@
577
577
  "items": {
578
578
  "type": "string"
579
579
  }
580
+ },
581
+ "runOnVersionPr": {
582
+ "description": "Run the CI check job on the changesets version PR (changeset-release/main). Default false: the generated check job is skipped there, because that branch is regenerated on every push to main and main's own run gates the release. Has no effect unless releaseStrategy is changesets.",
583
+ "type": "boolean"
580
584
  }
581
585
  },
582
586
  "additionalProperties": false