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}'`); + }); +});