Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/clear-hotels-enter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Gate every pull request and compare changesets against its base branch.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions scripts/changeset-ci.test.ts
Original file line number Diff line number Diff line change
@@ -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}`,
});
});
}
});
26 changes: 22 additions & 4 deletions scripts/changeset-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -33,8 +50,8 @@ export function isGeneratedPackageSizeHistoryChange(paths: readonly string[]): b
);
}

async function changedFilesSinceDevelop(): Promise<string[]> {
const child = Bun.spawn(["git", "diff", "--name-only", "origin/develop...HEAD"], {
async function changedFilesSince(comparison: ChangesetComparison): Promise<string[]> {
const child = Bun.spawn(["git", "diff", "--name-only", comparison.diffRange], {
cwd: REPOSITORY_ROOT,
stdout: "pipe",
stderr: "pipe",
Expand Down Expand Up @@ -72,20 +89,21 @@ async function main(): Promise<void> {
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;
}
console.log(
"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",
Expand Down
6 changes: 6 additions & 0 deletions scripts/workflow-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions scripts/workflow-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, { readonly required?: boolean }>>;
};
Expand Down
Loading