From 979f96cafc1b07169494307ed809c929957c9447 Mon Sep 17 00:00:00 2001 From: chh-ay Date: Sun, 26 Jul 2026 21:31:30 +0700 Subject: [PATCH] fix(ci): gate every pull request against its base --- .changeset/clear-hotels-enter.md | 4 ++++ .github/workflows/ci.yml | 4 ++-- package.json | 2 +- scripts/changeset-ci.test.ts | 22 ++++++++++++++++++++++ scripts/changeset-ci.ts | 26 ++++++++++++++++++++++---- scripts/workflow-contract.test.ts | 6 ++++++ scripts/workflow-contract.ts | 6 ++++++ 7 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 .changeset/clear-hotels-enter.md create mode 100644 scripts/changeset-ci.test.ts diff --git a/.changeset/clear-hotels-enter.md b/.changeset/clear-hotels-enter.md new file mode 100644 index 00000000..c4248012 --- /dev/null +++ b/.changeset/clear-hotels-enter.md @@ -0,0 +1,4 @@ +--- +--- + +Gate every pull request and compare changesets against its base branch. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9c60c7..79b889a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,8 @@ name: CI on: push: branches: [develop] - pull_request: - branches: [develop] + # Every pull request is gated, including release branches cut after this workflow lands. + pull_request: {} concurrency: group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/package.json b/package.json index ce75ad7d..30cdb9f5 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "test:browser": "playwright test --config test/browser/playwright.config.ts", "browser:install:chromium": "playwright install --with-deps chromium", "test:browser:portability": "playwright test --config test/browser/playwright.config.ts --grep @portability", - "test:tooling": "bun test scripts/toolchain-contract.test.ts scripts/workspace-tooling.test.ts scripts/dependency-audit.test.ts scripts/verify-clean-build.test.ts scripts/size-report.test.ts scripts/release-artifacts.test.ts scripts/consumer-lock.test.ts scripts/release-verify.test.ts scripts/release-workflow.test.ts scripts/workflow-contract.test.ts scripts/public-api.test.ts scripts/coverage-check.test.ts scripts/sheetwrite-code-hovers.test.ts scripts/normalize-sitemap.test.ts", + "test:tooling": "bun test scripts/toolchain-contract.test.ts scripts/workspace-tooling.test.ts scripts/dependency-audit.test.ts scripts/verify-clean-build.test.ts scripts/size-report.test.ts scripts/release-artifacts.test.ts scripts/consumer-lock.test.ts scripts/release-verify.test.ts scripts/release-workflow.test.ts scripts/changeset-ci.test.ts scripts/workflow-contract.test.ts scripts/public-api.test.ts scripts/coverage-check.test.ts scripts/sheetwrite-code-hovers.test.ts scripts/normalize-sitemap.test.ts", "test:coverage:ts": "bun scripts/coverage.ts ts", "test:coverage:rust": "bun scripts/coverage.ts rust", "test:coverage": "bun run test:coverage:ts && bun run test:coverage:rust", diff --git a/scripts/changeset-ci.test.ts b/scripts/changeset-ci.test.ts new file mode 100644 index 00000000..f81bef76 --- /dev/null +++ b/scripts/changeset-ci.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "bun:test"; +import { changesetComparisonFromBaseRef } from "./changeset-ci.js"; + +const CASES = [ + { githubBaseRef: "0.4.0", expectedBaseRef: "origin/0.4.0" }, + { githubBaseRef: "develop", expectedBaseRef: "origin/develop" }, + { githubBaseRef: "release/next", expectedBaseRef: "origin/release/next" }, + { githubBaseRef: "", expectedBaseRef: "origin/develop" }, + { githubBaseRef: undefined, expectedBaseRef: "origin/develop" }, +] as const; + +describe("changeset comparison base", () => { + for (const { githubBaseRef, expectedBaseRef } of CASES) { + it(`uses ${expectedBaseRef} for ${githubBaseRef ?? "an absent GITHUB_BASE_REF"}`, () => { + expect(changesetComparisonFromBaseRef(githubBaseRef)).toEqual({ + baseRef: expectedBaseRef, + diffRange: `${expectedBaseRef}...HEAD`, + sinceArgument: `--since=${expectedBaseRef}`, + }); + }); + } +}); diff --git a/scripts/changeset-ci.ts b/scripts/changeset-ci.ts index 886f16a5..35d4d22c 100644 --- a/scripts/changeset-ci.ts +++ b/scripts/changeset-ci.ts @@ -14,6 +14,23 @@ const ALLOWED_PACKAGE_SIZE_HISTORY_FILES = new Set([ "docs/src/generated/docs-contract.json", ]); +export interface ChangesetComparison { + readonly baseRef: string; + readonly diffRange: string; + readonly sinceArgument: string; +} + +export function changesetComparisonFromBaseRef( + githubBaseRef: string | undefined, +): ChangesetComparison { + const baseRef = `origin/${githubBaseRef?.trim() || "develop"}`; + return { + baseRef, + diffRange: `${baseRef}...HEAD`, + sinceArgument: `--since=${baseRef}`, + }; +} + export function releaseVersionFromHeadRef(headRef: string | undefined): string | undefined { return headRef !== undefined && STABLE_VERSION.test(headRef) ? headRef : undefined; } @@ -33,8 +50,8 @@ export function isGeneratedPackageSizeHistoryChange(paths: readonly string[]): b ); } -async function changedFilesSinceDevelop(): Promise { - const child = Bun.spawn(["git", "diff", "--name-only", "origin/develop...HEAD"], { +async function changedFilesSince(comparison: ChangesetComparison): Promise { + const child = Bun.spawn(["git", "diff", "--name-only", comparison.diffRange], { cwd: REPOSITORY_ROOT, stdout: "pipe", stderr: "pipe", @@ -72,12 +89,13 @@ async function main(): Promise { console.log(`Release package versions match branch ${releaseVersion}`); return; } + const comparison = changesetComparisonFromBaseRef(process.env.GITHUB_BASE_REF); const packageSizeHistoryVersion = packageSizeHistoryVersionFromHeadRef( process.env.GITHUB_HEAD_REF, ); if (packageSizeHistoryVersion !== undefined) { validateReleasePackageVersions(packageSizeHistoryVersion); - if (isGeneratedPackageSizeHistoryChange(await changedFilesSinceDevelop())) { + if (isGeneratedPackageSizeHistoryChange(await changedFilesSince(comparison))) { console.log(`Package size history versions match branch ${packageSizeHistoryVersion}`); return; } @@ -85,7 +103,7 @@ async function main(): Promise { "Package size history branch contains non-generated changes; requiring a changeset", ); } - const child = Bun.spawn(["bun", "run", "changeset:status", "--", "--since=origin/develop"], { + const child = Bun.spawn(["bun", "run", "changeset:status", "--", comparison.sinceArgument], { cwd: REPOSITORY_ROOT, stdin: "inherit", stdout: "inherit", diff --git a/scripts/workflow-contract.test.ts b/scripts/workflow-contract.test.ts index d79f6d6b..221277e4 100644 --- a/scripts/workflow-contract.test.ts +++ b/scripts/workflow-contract.test.ts @@ -113,6 +113,12 @@ describe("CI and release workflow contracts", () => { ).toThrow("action is not reviewed"); }); + it("runs CI for every pull request without duplicating release-branch push work", () => { + const triggers = workflows().ci.on; + expect(triggers?.push).toEqual({ branches: ["develop"] }); + expect(triggers?.pull_request).toEqual({}); + }); + it("routes ordinary changes through Changesets and validates semver release branches", () => { const changesetStatus = workflows().ci.jobs.preflight?.steps?.find( (step) => step.name === "Changeset status", diff --git a/scripts/workflow-contract.ts b/scripts/workflow-contract.ts index 06b4af10..fccaac55 100644 --- a/scripts/workflow-contract.ts +++ b/scripts/workflow-contract.ts @@ -34,6 +34,12 @@ export interface WorkflowJob { } export interface WorkflowTriggers { + readonly push?: { + readonly branches?: readonly string[]; + }; + readonly pull_request?: { + readonly branches?: readonly string[]; + }; readonly workflow_dispatch?: { readonly inputs?: Readonly>; };