Files
pnpm/.github/scripts/ts-test-scope.test.mjs
T
Zoltan KochanandClaude Opus 5.5 82e235bacf chore: split workspace settings into policy, catalog, and task files (#17192)
* chore: move supply-chain settings to a protected config file

Use the `include` setting from pnpm 12.15 to move the settings that
decide what gets installed and what may run into
.config/pnpm/security.yaml: allowBuilds, auditConfig, the
minimumReleaseAge and trustPolicy settings, overrides,
packageExtensions, patchedDependencies, patchesDir, and registries.
pnpm-workspace.yaml delegates exactly that list, so no other file can
define those settings. Patch paths are relative to the included file.

pnpm 12.14 ignores `include` and would drop every moved setting, so
the packageManager pin moves to 12.15.0.

CODEOWNERS now names both workspace files, the pnpmfile, and the
patches explicitly, and the CI path filters watch the new file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: move routine settings to a file without a code owner

The catalog, the update settings, and the task-runner settings
(pipelines, tasks, concurrencyGroups) move to
.config/pnpm/routine.yaml, which CODEOWNERS leaves unowned. Because
pnpm-workspace.yaml delegates only that list to the file, any other
setting defined there fails with ERR_PNPM_SETTING_NOT_DELEGATED, so
the file cannot be used to change the protected settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: move the catalogs to their own file

.config/pnpm/catalog.yaml owns `catalogs`, which covers the default
catalog and any named catalog added later. Like routine.yaml, it has
no code owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: name the protected file policies.yaml and give tasks their own file

security.yaml becomes policies.yaml. The task-runner settings
(pipelines, tasks, concurrencyGroups) move to tasks.yaml, and
`update` joins the catalogs in catalog.yaml, since its ignoreDeps list
holds back catalog entries. routine.yaml is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: move catalogMode and catalogPrune to catalog.yaml

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: keep the patch list next to the patches

patchedDependencies and patchesDir move to __patches__/patches.yaml,
so the entries name the patch files without a directory prefix and
patchesDir is `.`. CODEOWNERS already covers __patches__/. The CI path
filters that did not watch __patches__/ now watch patches.yaml, since
they watched the patch list when it lived in pnpm-workspace.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: move the patches to .config/pnpm/patches

Every pnpm configuration file now lives under .config/pnpm, so each CI
path filter watches `.config/pnpm/**` instead of listing the files.
CODEOWNERS keeps the patches protected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:33:59 +02:00

96 lines
5.9 KiB
JavaScript

import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { test } from 'node:test'
import { determineTestScope } from './ts-test-scope.mjs'
test('PR scope stays pinned when main advances; global changes and missing bases run everything', () => {
const dir = mkdtempSync(path.join(tmpdir(), 'ci-scope-'))
const origin = path.join(dir, 'origin')
const checkout = path.join(dir, 'checkout')
const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
const commit = (cwd, file, content) => {
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true })
writeFileSync(path.join(cwd, file), content)
git(cwd, 'add', '.')
git(cwd, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-m', 'test')
return git(cwd, 'rev-parse', 'HEAD')
}
try {
mkdirSync(origin)
git(origin, 'init', '-b', 'main')
commit(origin, 'pnpm11/example/src/index.ts', 'base')
const base = commit(origin, 'global-test-config.json', 'original config')
git(dir, 'clone', origin, checkout)
git(checkout, 'switch', '-c', 'pr')
commit(checkout, 'pnpr/docs/introduction.md', 'registry docs')
assert.equal(determineTestScope({ event: 'pull_request', base, cwd: checkout }).full_tests, 'false', 'registry docs alone do not require all TS tests')
git(checkout, 'reset', '--hard', 'HEAD^')
commit(checkout, 'pnpm11/example/src/index.ts', 'pr')
commit(origin, 'pnpm11/unrelated/src/index.ts', 'new main')
const scope = () => determineTestScope({ event: 'pull_request', base, cwd: checkout })
assert.equal(scope().full_tests, 'false')
assert.equal(git(checkout, 'rev-parse', 'origin/main'), base)
assert.equal(git(checkout, 'diff', '--name-only', 'origin/main', 'HEAD'), 'pnpm11/example/src/index.ts')
commit(checkout, '.changeset/fix-example.md', 'Release note')
assert.equal(scope().full_tests, 'false', 'TS code plus release notes remain affected')
commit(checkout, 'pnpm/crates/example/src/lib.rs', 'rust')
assert.equal(scope().full_tests, 'false', 'TS code plus pacquet sources remain affected')
for (const file of ['package.json', 'pnpm-lock.yaml', '.pnpmfile.cjs', 'pnpm-workspace.yaml', '.config/pnpm/patches/dependency.patch', '.github/scripts/new-helper.mjs', '.changeset/config.json', 'pnpr/crates/example/src/lib.rs', 'pnpr/.fixtures/packages/example/package.json', 'Cargo.lock']) {
commit(checkout, file, 'global input')
assert.equal(scope().full_tests, 'true', file)
git(checkout, 'reset', '--hard', 'HEAD^')
}
git(checkout, 'mv', 'global-test-config.json', 'pnpm11/example/moved-config.json')
git(checkout, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-m', 'rename global config')
assert.equal(scope().full_tests, 'true', 'moving a global input into a package still affects all tests')
git(checkout, 'reset', '--hard', 'HEAD^')
git(checkout, 'rm', 'global-test-config.json')
git(checkout, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-m', 'delete global config')
assert.equal(scope().full_tests, 'true', 'deleted global inputs still affect all tests')
assert.equal(determineTestScope({ event: 'pull_request', base: 'f'.repeat(40), cwd: checkout }).full_tests, 'true')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
test('merge queues, main, manual and unknown events cannot narrow tests', () => {
for (const event of ['merge_group', 'push', 'workflow_dispatch', 'unknown', undefined]) {
assert.equal(determineTestScope({ event, base: 'a'.repeat(40) }).full_tests, 'true')
}
for (const base of [undefined, '', 'main', '--upload-pack=evil', 'a'.repeat(39)]) {
assert.equal(determineTestScope({ event: 'pull_request', base }).full_tests, 'true')
}
})
test('Rust gate configuration keeps exclusions limited to approved documentation', () => {
const workflow = readFileSync(new URL('../workflows/pacquet-ci.yml', import.meta.url), 'utf8')
const filters = workflow.slice(workflow.indexOf(' predicate-quantifier:'), workflow.indexOf('\n test:'))
assert.match(filters, /predicate-quantifier: some-with-excludes/)
const patterns = filters.split(' rust:\n')[1].split('\n').map(line => line.match(/- '([^']+)'/)?.[1]).filter(Boolean)
const docs = filters.split(' docs:\n')[1].split(' rust:\n')[0].split('\n').map(line => line.match(/- '([^']+)'/)?.[1]).filter(Boolean)
assert.deepEqual(patterns.filter(pattern => pattern.startsWith('!')), [
'!pnpm/docs/**',
'!pnpr/docs/**',
'!pnpm/*.md',
'!pnpm/plans/*.md',
'!pnpm/scripts/*.md',
'!pnpm/crates/*/*.md',
'!pnpm/tasks/*/*.md',
'!pnpr/*.md',
'!pnpr/crates/*/*.md',
'!pnpr/docker/*.md',
'!pnpr/client/*.md',
])
// Markdown under pnpr/.fixtures ships inside registry fixtures.
const recursiveDocs = new Set(['!pnpm/docs/**', '!pnpr/docs/**'])
assert.ok(!patterns.some(pattern => pattern.startsWith('!') && pattern.includes('**') && !recursiveDocs.has(pattern)), 'recursive exclusions only cover product documentation')
for (const pattern of ['pnpm/**', 'pnpr/**', '.config/nextest.toml', 'fixtures/**', 'pnpm11/installing/deps-installer/test/fixtures/patch-pkg/**', 'pnpm11/deps/compliance/commands/test/sbom/fixtures/**']) {
assert.ok(patterns.includes(pattern), `required test input: ${pattern}`)
}
assert.deepEqual(docs, patterns.filter(pattern => pattern.startsWith('!')).map(pattern => pattern.slice(1)), 'docs covers every exclusion, so the spell check still runs')
assert.match(workflow.slice(workflow.indexOf('\n typos:'), workflow.indexOf('\n deny:')), /needs.changes.outputs.docs == 'true'/)
})