From 2843e25bf36cb80d5edd26f39e8cb61fabdd68f7 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 5 Aug 2026 19:27:07 -0400 Subject: [PATCH] fix(#2988): local changeset/docs lint falls back to next, not main (#3095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#2988): local changeset/docs lint falls back to next, not main Both scripts/changeset/lint.cjs and scripts/lint-docs-required.cjs resolved their diff base as GITHUB_BASE_REF || 'main'. GITHUB_BASE_REF is set only in GitHub Actions; locally it falls back to 'main' (the release branch), which lags far behind 'next' (the integration branch every PR targets). The oversized diff range swept in every changeset fragment merged since the last release, so the lint passed on the first fragment it saw regardless of whether the current PR authored it — structurally vacuous. Changed the fallback to 'next' (DEFAULT_BASE constant, exported from both scripts for parity). CI behavior unchanged (GITHUB_BASE_REF is set there). Added a parity test asserting both lints resolve the same base. * chore(#2988): backfill changeset PR number 3095 --------- Co-authored-by: sim --- .changeset/witty-yaks-munch.md | 5 +++++ scripts/changeset/lint.cjs | 11 +++++++++-- scripts/lint-docs-required.cjs | 10 +++++++++- tests/changeset-lint.test.cjs | 19 ++++++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .changeset/witty-yaks-munch.md diff --git a/.changeset/witty-yaks-munch.md b/.changeset/witty-yaks-munch.md new file mode 100644 index 000000000..458bf18eb --- /dev/null +++ b/.changeset/witty-yaks-munch.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3095 +--- +**Local `lint:changeset` and `lint:docs-required` now diff against `next` instead of `main`** — the local fallback was the release branch (`main`), which lags far behind the integration branch (`next`), so the lint always passed by finding fragments from other already-merged PRs in the oversized diff range. The local invocation now matches the base CI uses. (#2988) diff --git a/scripts/changeset/lint.cjs b/scripts/changeset/lint.cjs index 9fe975093..933d5fe55 100755 --- a/scripts/changeset/lint.cjs +++ b/scripts/changeset/lint.cjs @@ -12,6 +12,10 @@ * Tests assert on the typed verdict, never on free text. */ +// #2988: the repo's integration/default branch — the base every PR targets. +// Used as the local fallback when GITHUB_BASE_REF is unset (CI sets it). +const DEFAULT_BASE = 'next'; + const LINT_REASON = Object.freeze({ OK_FRAGMENT_PRESENT: 'ok_fragment_present', OK_OPT_OUT_LABEL: 'ok_opt_out_label', @@ -80,7 +84,10 @@ function main() { labels = (event.pull_request?.labels || []).map((l) => l.name); } catch { /* fall through */ } } - const base = process.env.GITHUB_BASE_REF || 'main'; + // #2988: local fallback must match the repo's integration branch (`next`), + // not the release branch (`main`). CI sets GITHUB_BASE_REF explicitly; the + // fallback only fires locally, where `next` is the base every PR targets. + const base = process.env.GITHUB_BASE_REF || DEFAULT_BASE; let changedFiles = []; try { // Use execFileSync with an argv array — the base ref is interpolated @@ -145,4 +152,4 @@ function main() { if (require.main === module) runMain(main); -module.exports = { evaluateLint, LINT_REASON, OPT_OUT_LABEL, isUserFacing, isFragment }; +module.exports = { evaluateLint, LINT_REASON, OPT_OUT_LABEL, isUserFacing, isFragment, DEFAULT_BASE }; diff --git a/scripts/lint-docs-required.cjs b/scripts/lint-docs-required.cjs index a6580cb4a..3bcce28ca 100755 --- a/scripts/lint-docs-required.cjs +++ b/scripts/lint-docs-required.cjs @@ -16,6 +16,10 @@ const { parseFragment, FRAGMENT_ERROR } = require('./changeset/parse.cjs'); const { ExitError, runMain } = require('./lib/cli-exit.cjs'); +// #2988: the repo's integration/default branch — the base every PR targets. +// Used as the local fallback when GITHUB_BASE_REF is unset (CI sets it). +const DEFAULT_BASE = 'next'; + const LINT_REASON = Object.freeze({ OK_NO_TRIGGERING_FRAGMENTS: 'ok_no_triggering_fragments', OK_DOCS_UPDATED: 'ok_docs_updated', @@ -149,7 +153,10 @@ function main() { } catch { /* fall through */ } } - const base = process.env.GITHUB_BASE_REF || 'main'; + // #2988: local fallback must match the repo's integration branch (`next`), + // not the release branch (`main`). CI sets GITHUB_BASE_REF explicitly; the + // fallback only fires locally, where `next` is the base every PR targets. + const base = process.env.GITHUB_BASE_REF || DEFAULT_BASE; let changedFiles = []; try { // execFileSync with argv — no shell, so a malicious GITHUB_BASE_REF @@ -216,6 +223,7 @@ module.exports = { OPT_OUT_LABEL, TRIGGERING_TYPES, FRAGMENT_ERROR, + DEFAULT_BASE, isFragmentPath, isDocsFile, isExemptFragment, diff --git a/tests/changeset-lint.test.cjs b/tests/changeset-lint.test.cjs index dff7ab16a..6775d142d 100644 --- a/tests/changeset-lint.test.cjs +++ b/tests/changeset-lint.test.cjs @@ -8,7 +8,8 @@ const fs = require('node:fs'); const os = require('node:os'); const cp = require('node:child_process'); -const { evaluateLint, LINT_REASON } = require(path.join(__dirname, '..', 'scripts', 'changeset', 'lint.cjs')); +const { evaluateLint, LINT_REASON, DEFAULT_BASE: CHANGESET_DEFAULT_BASE } = require(path.join(__dirname, '..', 'scripts', 'changeset', 'lint.cjs')); +const { DEFAULT_BASE: DOCS_DEFAULT_BASE } = require(path.join(__dirname, '..', 'scripts', 'lint-docs-required.cjs')); const ROOT = path.join(__dirname, '..'); const LINT_SCRIPT = path.join(ROOT, 'scripts', 'changeset', 'lint.cjs'); @@ -297,3 +298,19 @@ describe('changeset lint: main() end-to-end wiring (#1006)', () => { assert.ok(!deletedEntry, `deleted fragment must not appear in failures, got: ${JSON.stringify(failures)}`); }); }); + +// ─── #2988: local base fallback parity ────────────────────────────────────── + +describe('#2988: changeset + docs lints resolve the same local base fallback', () => { + test('both lints default to `next` (the integration branch), not `main`', () => { + assert.strictEqual(CHANGESET_DEFAULT_BASE, 'next', + `changeset lint DEFAULT_BASE must be 'next', got '${CHANGESET_DEFAULT_BASE}'`); + assert.strictEqual(DOCS_DEFAULT_BASE, 'next', + `docs lint DEFAULT_BASE must be 'next', got '${DOCS_DEFAULT_BASE}'`); + }); + + test('both lints resolve the same base given the same environment (parity)', () => { + assert.strictEqual(CHANGESET_DEFAULT_BASE, DOCS_DEFAULT_BASE, + `the two lints must not diverge on base resolution: changeset='${CHANGESET_DEFAULT_BASE}' docs='${DOCS_DEFAULT_BASE}'`); + }); +});