Files
pnpm/pnpm11/__utils__/scripts/test/bump.ts
Zoltan Kochan d6a6b48073 fix(release): park the Rust products' changelogs after the bump (#16315)
pnpm/pnpm#14934 (in pnpm 12.7.0) gives every private project
`repository` changelog storage: `pnpm version -r` commits its section to
CHANGELOG.md. That is right for packages that are never published, but
the npm wrappers of the Rust products (pacquet, `@pnpm/napi`,
`@pnpm/pnpr`) are private in-repo only to keep a workspace-wide publish
from shipping them. Their generate-packages scripts drop the flag when
they are published for real.

The first release bumped with pnpm 12.7.0 (pnpm/pnpm#16312) therefore
committed their sections to CHANGELOG.md and parked nothing. release.yml
reads the parked section for the GitHub release body, and the
publish-time changelog composer reads it for the npm tarball's
CHANGELOG.md, so both would have come out empty. That release was fixed
by hand.

bump.ts now moves each wrapper's freshly written CHANGELOG.md to
`.changeset/changelogs/<name>@<version>.md` after `pnpm version -r`.
Intent consumption is unchanged: a private project's intents are
consumed immediately, and the parked section is garbage-collected by the
next bump once the registry confirms the published changelog carries it.
The move refuses a CHANGELOG.md holding anything but the released
section, so a stale file cannot be parked under the wrong version.

The script runs before the workspace is installed, so it mirrors the
filename encoding of `pendingChangelogPath` in
`@pnpm/releasing.versioning` instead of importing it.

Related to pnpm/tasks#38.
2026-09-28 09:23:06 +02:00

140 lines
6.0 KiB
TypeScript

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'
import { findRepoRoot, parkPublishedPrivateChangelogs, parseSelectedProducts, releaseFilterArgs } from '../src/bump.js'
describe('findRepoRoot', () => {
let dir: string
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bump-root-'))
})
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))
test('walks up to the directory containing .changeset', () => {
fs.mkdirSync(path.join(dir, '.changeset'))
const nested = path.join(dir, 'pnpm11', '__utils__', 'scripts', 'src')
fs.mkdirSync(nested, { recursive: true })
expect(findRepoRoot(nested)).toBe(dir)
})
test('throws when no .changeset directory exists above', () => {
expect(() => findRepoRoot(dir)).toThrow(/No \.changeset directory/)
})
})
describe('parseSelectedProducts', () => {
test('collects the products named by --release', () => {
expect(parseSelectedProducts(['--release', 'pnpm11', '--release', 'pnpr']))
.toEqual(new Set(['pnpm11', 'pnpr']))
})
test('is empty when no argument is passed (a bare `pnpm bump` releases everything)', () => {
expect(parseSelectedProducts([])).toEqual(new Set())
})
test('skips the leading `--` separator forwarded by `pnpm run bump -- …`', () => {
expect(parseSelectedProducts(['--', '--release', 'pnpm', '--release', 'pnpr']))
.toEqual(new Set(['pnpm', 'pnpr']))
expect(parseSelectedProducts(['--'])).toEqual(new Set())
})
test('rejects a `--` that is not in the leading position', () => {
expect(() => parseSelectedProducts(['--release', 'pnpm', '--'])).toThrow(/Unexpected bump argument/)
})
test('throws on an unknown product', () => {
expect(() => parseSelectedProducts(['--release', 'bogus'])).toThrow(/Unknown --release product/)
})
test('fails closed on a misspelled flag instead of releasing everything', () => {
expect(() => parseSelectedProducts(['--releases', 'pnpr'])).toThrow(/Unexpected bump argument/)
})
})
describe('releaseFilterArgs', () => {
test('releases everything (no filter) when nothing is selected', () => {
expect(releaseFilterArgs(new Set())).toEqual([])
})
test('releases everything (no filter) when all three products are selected', () => {
expect(releaseFilterArgs(new Set(['pnpm11', 'pnpm', 'pnpr']))).toEqual([])
})
test('excludes the unselected alpha products when pnpm11 is selected', () => {
expect(releaseFilterArgs(new Set(['pnpm11'])))
.toEqual(['--filter=!pacquet', '--filter=!@pnpm/napi', '--filter=!@pnpm/pnpr'])
expect(releaseFilterArgs(new Set(['pnpm11', 'pnpr'])))
.toEqual(['--filter=!pacquet', '--filter=!@pnpm/napi'])
})
test('includes only the selected alpha products when pnpm11 is not selected', () => {
expect(releaseFilterArgs(new Set(['pnpm'])))
.toEqual(['--filter=pacquet', '--filter=@pnpm/napi'])
expect(releaseFilterArgs(new Set(['pnpm', 'pnpr'])))
.toEqual(['--filter=pacquet', '--filter=@pnpm/napi', '--filter=@pnpm/pnpr'])
expect(releaseFilterArgs(new Set(['pnpr'])))
.toEqual(['--filter=@pnpm/pnpr'])
})
})
describe('parkPublishedPrivateChangelogs', () => {
let dir: string
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bump-park-'))
})
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))
function writeProject (projectDir: string, manifest: { name: string, version: string }, changelog?: string): void {
fs.mkdirSync(path.join(dir, projectDir), { recursive: true })
fs.writeFileSync(path.join(dir, projectDir, 'package.json'), JSON.stringify(manifest))
if (changelog != null) {
fs.writeFileSync(path.join(dir, projectDir, 'CHANGELOG.md'), changelog)
}
}
const section = '## 12.8.0\n\n### Minor Changes\n\n- A change.\n\n### Patch Changes\n\n- A fix.\n'
test('moves the committed changelog to the parked section of the released version', () => {
writeProject('pnpm/npm/pnpm', { name: 'pacquet', version: '12.8.0' }, `# pacquet\n\n${section}`)
writeProject('pnpm/npm/napi', { name: '@pnpm/napi', version: '12.8.0' }, '# @pnpm/napi\n\n## 12.8.0\n')
parkPublishedPrivateChangelogs(dir, ['pnpm/npm/pnpm', 'pnpm/npm/napi'])
expect(fs.readFileSync(path.join(dir, '.changeset/changelogs/pacquet@12.8.0.md'), 'utf8')).toBe(section)
expect(fs.readFileSync(path.join(dir, '.changeset/changelogs/@pnpm!napi@12.8.0.md'), 'utf8')).toBe('## 12.8.0\n')
expect(fs.existsSync(path.join(dir, 'pnpm/npm/pnpm/CHANGELOG.md'))).toBe(false)
expect(fs.existsSync(path.join(dir, 'pnpm/npm/napi/CHANGELOG.md'))).toBe(false)
})
test('skips a project the release did not bump', () => {
writeProject('pnpr/npm/pnpr', { name: '@pnpm/pnpr', version: '0.1.0-alpha.14' })
parkPublishedPrivateChangelogs(dir, ['pnpr/npm/pnpr'])
expect(fs.existsSync(path.join(dir, '.changeset/changelogs'))).toBe(false)
})
test('refuses a changelog that holds more than the released section', () => {
writeProject('pnpm/npm/pnpm', { name: 'pacquet', version: '12.9.0' }, `# pacquet\n\n## 12.9.0\n\n- New.\n\n${section}`)
expect(() => parkPublishedPrivateChangelogs(dir, ['pnpm/npm/pnpm'])).toThrow(/exactly one section, for pacquet@12\.9\.0/)
expect(fs.existsSync(path.join(dir, 'pnpm/npm/pnpm/CHANGELOG.md'))).toBe(true)
})
test('refuses a changelog that holds an older section before the released one', () => {
writeProject('pnpm/npm/pnpm', { name: 'pacquet', version: '12.8.0' }, `# pacquet\n\n## 12.7.0\n\n- Old.\n\n${section}`)
expect(() => parkPublishedPrivateChangelogs(dir, ['pnpm/npm/pnpm'])).toThrow(/exactly one section, for pacquet@12\.8\.0/)
expect(fs.existsSync(path.join(dir, 'pnpm/npm/pnpm/CHANGELOG.md'))).toBe(true)
})
test('refuses a changelog without a section for the released version', () => {
writeProject('pnpm/npm/pnpm', { name: 'pacquet', version: '12.9.0' }, `# pacquet\n\n${section}`)
expect(() => parkPublishedPrivateChangelogs(dir, ['pnpm/npm/pnpm'])).toThrow(/exactly one section/)
})
})