Files
Zoltan Kochan 1860c1c75e chore: move get-pnpm into the workspace and pin npm's signing keys once (#17341)
get-pnpm moves from pnpm/get.pnpm.io into pnpm/npm/get-pnpm, at 0.0.9
(get.pnpm.io be4e983, where its history stays).

- The pnpm v12 npm package depends on it with `workspace:*`, so a pnpm
  release bundles get-pnpm's source as of that release. A wrapper fix no
  longer needs a get-pnpm release, a catalog bump, and a
  minimumReleaseAge exclusion first. bundle-node-gyp builds it before
  the deploy injects its build output.
- It is released on its own: a `get-pnpm` product in bump.ts and
  create-release-pr.yml (off by default), `get-pnpm@<version>` tags,
  and build-get-pnpm/publish-get-pnpm jobs in release.yml. It is private
  in the workspace so the TypeScript release's recursive publish skips
  it; the packed manifest drops the flag.
- Its runtime dependencies stay exact, through a `get-pnpm` named
  catalog. Its lint is the root ESLint config. Its TLS tests use the
  fixture the Rust network tests use.
- The npm-wrapper CI job runs its tests on Node.js 24 and 22, beside
  the wrapper's. The forwarding proxy in its download tests forwards
  only to the servers the tests start.

npm's registry signing keys were pinned in five places: v11, v12,
get-pnpm, and get.pnpm.io's install.sh and install.ps1. Only v11's copy
was refreshed, by the release PR, and the two repositories handled a
withdrawn key in opposite ways. They now come from
.config/npm-signing-keys.json:

- .github/scripts/update-npm-signing-keys.mjs refreshes it from npm on
  every release PR, not only v11 ones. A key npm stops listing stays,
  marked withdrawn.
- The meta-updater renders it into the v11, v12, and get-pnpm copies,
  so `pnpm lint:meta` fails on a copy edited by hand. pnpm v11 and v12
  keep trusting withdrawn keys, so packages signed before a rotation
  verify. get-pnpm drops them, as get.pnpm.io's installers do.
- get.pnpm.io's update script is to read this file.
2026-10-25 17:38:04 +01:00

144 lines
6.2 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 every product is selected', () => {
expect(releaseFilterArgs(new Set(['pnpm11', 'pnpm', 'pnpr', 'get-pnpm']))).toEqual([])
})
test('excludes the unselected separate products when pnpm11 is selected', () => {
expect(releaseFilterArgs(new Set(['pnpm11'])))
.toEqual(['--filter=!pacquet', '--filter=!@pnpm/napi', '--filter=!@pnpm/pnpr', '--filter=!get-pnpm'])
expect(releaseFilterArgs(new Set(['pnpm11', 'pnpr', 'get-pnpm'])))
.toEqual(['--filter=!pacquet', '--filter=!@pnpm/napi'])
})
test('releases get-pnpm alone', () => {
expect(releaseFilterArgs(new Set(['get-pnpm']))).toEqual(['--filter=get-pnpm'])
})
test('includes only the selected separate 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/)
})
})