diff --git a/.changeset/README.md b/.changeset/README.md index 72ae5cd..b12d619 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -4,7 +4,7 @@ Releases are managed by Changesets and `.github/workflows/release.yml`. On each successful push to `main`, CI derives a temporary changeset from the Conventional Commits since the latest package tags. The Changesets action keeps one rolling `chore: version packages` pull request up to date. Merging that pull -request publishes all three public packages, creates package tags, and creates +request publishes all four public packages, creates package tags, and creates GitHub Releases. Contributor lists use a paginated GitHub comparison to map release commit SHAs @@ -14,6 +14,7 @@ rendered; unlinked identities and bots are omitted. The public packages form a fixed group and always share one version: - `gitlab-ui-react` +- `@gitlab-ui-react/json-render` - `@gitlab-ui-react/styles` - `@gitlab-ui-react/tokens` @@ -28,10 +29,16 @@ The public packages form a fixed group and always share one version: | `type!` or a `BREAKING CHANGE` footer | minor, shown as a breaking change | | all other types | none | -Major versions are deliberately never inferred. Before merging a version pull -request that should be a major release, run `pnpm changeset`, select all three -packages with a `major` bump, commit the generated Markdown file, and merge it -to `main`. The rolling version pull request will update automatically. +Major versions are deliberately never inferred. Use `pnpm release:version` to +apply changesets: the wrapper temporarily hides workspace peer dependencies +within the fixed package group from Changesets 2.x's automatic major escalation, +then restores them even if versioning fails. The fixed group still advances +together, and external peer dependencies remain unchanged. + +Before merging a version pull request that should be a major release, run +`pnpm changeset`, select all four packages with a `major` bump, commit the +generated Markdown file, and merge it to `main`. The rolling version pull +request will update automatically. ## Pending release snapshots @@ -48,10 +55,10 @@ state blocks publishing rather than silently dropping commits. Before entering prerelease mode, publish all pending `0.x` changes. 1. Enter alpha with `pnpm changeset pre enter alpha`, add a manual major - changeset for all three packages, and commit both changes. The next version + changeset for all four packages, and commit both changes. The next version is `1.0.0-alpha.0`. 2. To enter beta, change the `tag` in `.changeset/pre.json` from `alpha` to - `beta` and add a patch changeset for all three packages. The next version is + `beta` and add a patch changeset for all four packages. The next version is `1.0.0-beta.0`. The version script resets Changesets' numeric prerelease counter when it detects the tag change. 3. To publish `1.0.0`, run `pnpm changeset pre exit` and commit the result. diff --git a/scripts/release-snapshot.test.ts b/scripts/release-snapshot.test.ts index 674dc68..f470761 100644 --- a/scripts/release-snapshot.test.ts +++ b/scripts/release-snapshot.test.ts @@ -54,7 +54,7 @@ function commit(root: string, message: string): string { return git(root, "rev-parse", "HEAD"); } -function createRepository(): string { +function createRepository(version = "0.1.0"): string { const root = mkdtempSync(join(tmpdir(), "gitlab-ui-snapshot-")); temporaryDirectories.push(root); mkdirSync(join(root, ".changeset")); @@ -91,8 +91,15 @@ function createRepository(): string { join(root, directory, "package.json"), `${JSON.stringify({ name, - version: "0.1.0", + version, ...(dependencies ? { dependencies } : {}), + ...(name === "@gitlab-ui-react/json-render" ? { + peerDependencies: { + "@gitlab-ui-react/styles": "workspace:^", + react: "^19.2.8", + }, + devDependencies: { "@gitlab-ui-react/styles": "workspace:^" }, + } : {}), }, null, 2)}\n`, ); } @@ -102,7 +109,7 @@ function createRepository(): string { git(root, "config", "user.name", "Release Test"); commit(root, "chore: create release fixture"); for(const [, name] of PACKAGES) { - git(root, "-c", "tag.gpgSign=false", "tag", `${name}@0.1.0`); + git(root, "-c", "tag.gpgSign=false", "tag", `${name}@${version}`); } return root; } @@ -152,6 +159,50 @@ function writeManualChangeset( } describe("pending release snapshots", () => { + it("versions internal workspace peers from 0.2.0 to 0.3.0 for a feature", () => { + const root = createRepository("0.2.0"); + writeFileSync(join(root, "packages/styles/feature.css"), ".feature {}\n"); + const featureSha = commit(root, "feat(styles): add a feature"); + expect(createAutomaticChangeset(root).bumps).toEqual({ + "@gitlab-ui-react/styles": "minor", + }); + + versionPackages(root, { runChangesetVersion: () => runChangesetVersion(root) }); + + expect(fixedVersion(root)).toBe("0.3.0"); + expect(readReleaseSnapshot(root)).toMatchObject({ + coveredThrough: featureSha, + version: "0.3.0", + }); + const manifest = JSON.parse( + readFileSync(join(root, "packages/json-render/package.json"), "utf8"), + ); + expect(manifest.peerDependencies).toEqual({ + "@gitlab-ui-react/styles": "workspace:^", + react: "^19.2.8", + }); + expect(manifest.devDependencies).toEqual({ + "@gitlab-ui-react/styles": "workspace:^", + }); + }, 30_000); + + it("restores workspace peers when Changesets fails and keeps external peers visible", () => { + const root = createRepository(); + const manifestPath = join(root, "packages/json-render/package.json"); + const original = readFileSync(manifestPath, "utf8"); + + expect(() => versionPackages(root, { + runChangesetVersion: () => { + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + expect(manifest.peerDependencies).toEqual({ react: "^19.2.8" }); + throw new Error("Changesets failed"); + }, + })).toThrow("Changesets failed"); + + expect(readFileSync(manifestPath, "utf8")).toBe(original); + expect(fixedVersion(root)).toBe("0.1.0"); + }); + it("rolls release-worthy commits after a stale snapshot into a new version", () => { const root = createRepository(); diff --git a/scripts/version.mts b/scripts/version.mts index 71d3b8e..9d8cb83 100644 --- a/scripts/version.mts +++ b/scripts/version.mts @@ -334,6 +334,40 @@ function resetPrereleaseCounters(root: string, preState: PreState | null): void } } +function withHiddenFixedWorkspacePeers(root: string, operation: () => void): void { + const fixedNames = new Set(PUBLISHABLE_PACKAGES.map(({ name }) => name)); + const originals = new Map>(); + + // Changesets 2.x promotes minor peer dependency updates to major releases. + // Our fixed group already versions together; its workspace peers must not + // override the bump selected by automatic or maintainer-authored changesets. + try { + for(const { directory } of PUBLISHABLE_PACKAGES) { + const manifestPath = join(root, directory, "package.json"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { + peerDependencies?: Record; + }; + if(!manifest.peerDependencies) continue; + const peers = Object.entries(manifest.peerDependencies); + const remaining = peers.filter( + ([name, range]) => !fixedNames.has(name) || !range.startsWith("workspace:"), + ); + if(remaining.length === peers.length) continue; + + originals.set(manifestPath, manifest.peerDependencies); + manifest.peerDependencies = Object.fromEntries(remaining); + writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + } + operation(); + } finally { + for(const [manifestPath, peerDependencies] of originals) { + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + manifest.peerDependencies = peerDependencies; + writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + } + } +} + export function run(root = REPOSITORY_ROOT, options: VersionRunOptions = {}): void { const preState = readPreState(root); const previousSnapshot = readReleaseSnapshot(root); @@ -353,15 +387,17 @@ export function run(root = REPOSITORY_ROOT, options: VersionRunOptions = {}): vo resetPrereleaseCounters(root, preState); console.log("[release] Running `changeset version`."); - if(options.runChangesetVersion) { - options.runChangesetVersion(); - } else { - const version = pnpmInvocation(["exec", "changeset", "version"]); - execFileSync(version.command, version.args, { - cwd: root, - stdio: "inherit", - }); - } + withHiddenFixedWorkspacePeers(root, () => { + if(options.runChangesetVersion) { + options.runChangesetVersion(); + } else { + const version = pnpmInvocation(["exec", "changeset", "version"]); + execFileSync(version.command, version.args, { + cwd: root, + stdio: "inherit", + }); + } + }); const after = readVersions(root); const nextFixedVersion = assertFixedVersions(after);