diff --git a/.changeset/default-minimum-release-age.md b/.changeset/default-minimum-release-age.md new file mode 100644 index 0000000000..22eeab4431 --- /dev/null +++ b/.changeset/default-minimum-release-age.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config.reader": major +"pnpm": major +--- + +The default value of the `minimumReleaseAge` setting is now 1440 minutes (1 day). Newly published packages will not be resolved until they are at least 1 day old. This protects against supply chain attacks by giving the community time to detect and remove compromised versions. To opt out, set `minimumReleaseAge: 0` in `pnpm-workspace.yaml`. diff --git a/building/commands/test/build/utils/index.ts b/building/commands/test/build/utils/index.ts index cb58673eca..e1ec04d168 100644 --- a/building/commands/test/build/utils/index.ts +++ b/building/commands/test/build/utils/index.ts @@ -28,6 +28,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/config/reader/src/index.ts b/config/reader/src/index.ts index 011faf62b7..7ec8e0e29f 100644 --- a/config/reader/src/index.ts +++ b/config/reader/src/index.ts @@ -173,6 +173,7 @@ export async function getConfig (opts: { 'link-workspace-packages': false, 'lockfile-include-tarball-url': false, 'manage-package-manager-versions': true, + 'minimum-release-age': 24 * 60, // 1 day 'modules-cache-max-age': 7 * 24 * 60, // 7 days 'dlx-cache-max-age': 24 * 60, // 1 day 'node-linker': 'isolated', diff --git a/deps/compliance/commands/test/audit/utils/options.ts b/deps/compliance/commands/test/audit/utils/options.ts index a27b533100..49437a3e28 100644 --- a/deps/compliance/commands/test/audit/utils/options.ts +++ b/deps/compliance/commands/test/audit/utils/options.ts @@ -34,6 +34,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/deps/compliance/commands/test/licenses/utils/index.ts b/deps/compliance/commands/test/licenses/utils/index.ts index 36ea26bb36..83b9e7cc35 100644 --- a/deps/compliance/commands/test/licenses/utils/index.ts +++ b/deps/compliance/commands/test/licenses/utils/index.ts @@ -28,6 +28,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/deps/compliance/commands/test/sbom/utils/index.ts b/deps/compliance/commands/test/sbom/utils/index.ts index 2acabed942..26df1a0b1f 100644 --- a/deps/compliance/commands/test/sbom/utils/index.ts +++ b/deps/compliance/commands/test/sbom/utils/index.ts @@ -28,6 +28,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/deps/inspection/commands/test/listing/utils/index.ts b/deps/inspection/commands/test/listing/utils/index.ts index abe686f8af..a9fe033b9e 100644 --- a/deps/inspection/commands/test/listing/utils/index.ts +++ b/deps/inspection/commands/test/listing/utils/index.ts @@ -30,6 +30,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/deps/inspection/commands/test/outdated/utils/index.ts b/deps/inspection/commands/test/outdated/utils/index.ts index b5995e544f..439cfa1331 100644 --- a/deps/inspection/commands/test/outdated/utils/index.ts +++ b/deps/inspection/commands/test/outdated/utils/index.ts @@ -33,6 +33,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/exec/commands/test/utils/index.ts b/exec/commands/test/utils/index.ts index c2932045cf..9314ba6b46 100644 --- a/exec/commands/test/utils/index.ts +++ b/exec/commands/test/utils/index.ts @@ -35,6 +35,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/installing/commands/test/import.ts b/installing/commands/test/import.ts index cca9cf49cd..ca7af8f993 100644 --- a/installing/commands/test/import.ts +++ b/installing/commands/test/import.ts @@ -27,6 +27,7 @@ const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, preferWorkspacePackages: true, diff --git a/installing/commands/test/importRecursive.ts b/installing/commands/test/importRecursive.ts index 154304bbd5..4d01913d80 100644 --- a/installing/commands/test/importRecursive.ts +++ b/installing/commands/test/importRecursive.ts @@ -25,6 +25,7 @@ const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, preferWorkspacePackages: true, diff --git a/installing/commands/test/utils/index.ts b/installing/commands/test/utils/index.ts index 8329c1d3c4..c8587e3452 100644 --- a/installing/commands/test/utils/index.ts +++ b/installing/commands/test/utils/index.ts @@ -32,6 +32,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/patching/commands/test/utils/index.ts b/patching/commands/test/utils/index.ts index 0b8d1ede2c..41c0b8fdb6 100644 --- a/patching/commands/test/utils/index.ts +++ b/patching/commands/test/utils/index.ts @@ -30,6 +30,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/pnpm/test/utils/execPnpm.ts b/pnpm/test/utils/execPnpm.ts index 7351619dcb..36d9cf3dc7 100644 --- a/pnpm/test/utils/execPnpm.ts +++ b/pnpm/test/utils/execPnpm.ts @@ -194,6 +194,7 @@ function createEnv (opts?: { storeDir?: string }): NodeJS.ProcessEnv { const env: Record = { pnpm_config_fetch_retries: fallback('fetchRetries', '4'), pnpm_config_hoist: fallback('hoist', 'true'), + pnpm_config_minimum_release_age: '0', pnpm_config_registry: fallback('registry', `http://localhost:${REGISTRY_MOCK_PORT}/`), pnpm_config_silent: 'true', pnpm_config_store_dir: opts?.storeDir ?? fallback('storeDir', '../store'), diff --git a/releasing/commands/test/deploy/utils/index.ts b/releasing/commands/test/deploy/utils/index.ts index 49573ee9e2..146fa166ff 100644 --- a/releasing/commands/test/deploy/utils/index.ts +++ b/releasing/commands/test/deploy/utils/index.ts @@ -32,6 +32,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/releasing/commands/test/publish/publish.ts b/releasing/commands/test/publish/publish.ts index c5c4e78291..f1270e13d9 100644 --- a/releasing/commands/test/publish/publish.ts +++ b/releasing/commands/test/publish/publish.ts @@ -25,6 +25,8 @@ const CREDENTIALS = [ ] const pnpmBin = path.join(import.meta.dirname, '../../../../pnpm/bin/pnpm.mjs') +const SPAWN_ENV = { ...process.env, pnpm_config_minimum_release_age: '0' } as NodeJS.ProcessEnv + test('publish: package with package.json', async () => { prepare({ name: 'test-publish-package.json', @@ -192,7 +194,7 @@ test('publish packages with workspace LICENSE if no own LICENSE is present', asy fs.writeFileSync(path.join(externalTarget, 'package.json'), JSON.stringify({ name: 'target', version: '1.0.0' })) process.chdir(externalTarget) - crossSpawn.sync(pnpmBin, ['add', 'project-100', 'project-200', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['add', 'project-100', 'project-200', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) expect(fs.readFileSync('node_modules/project-100/LICENSE', 'utf8')).toBe('workspace license') expect(fs.readFileSync('node_modules/project-200/LICENSE', 'utf8')).toBe('project-200 license') @@ -269,7 +271,7 @@ test('publish: package with all possible fields in publishConfig', async () => { }) process.chdir('../test-publish-config-installation') - crossSpawn.sync(pnpmBin, ['add', 'test-publish-config', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['add', 'test-publish-config', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) const { default: publishedManifest } = await import(path.resolve('node_modules/test-publish-config/package.json')) expect(publishedManifest).toEqual({ @@ -337,7 +339,7 @@ test('publish: package with publishConfig.directory', async () => { [] ) - crossSpawn.sync(pnpmBin, ['add', 'publish_config_directory_dist_package', '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['add', 'publish_config_directory_dist_package', '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) expect(JSON.parse(fs.readFileSync('node_modules/publish_config_directory_dist_package/package.json', { encoding: 'utf-8' }))) .toEqual({ @@ -504,7 +506,7 @@ test.skip('convert specs with workspace protocols to regular version ranges', as process.chdir('..') - crossSpawn.sync(pnpmBin, ['multi', 'install', '--store-dir=store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['multi', 'install', '--store-dir=store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) process.chdir('workspace-protocol-package') @@ -516,7 +518,7 @@ test.skip('convert specs with workspace protocols to regular version ranges', as process.chdir('../target') - crossSpawn.sync(pnpmBin, ['add', '--store-dir=store', 'workspace-protocol-package', '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['add', '--store-dir=store', 'workspace-protocol-package', '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) const { default: publishedManifest } = await import(path.resolve('node_modules/workspace-protocol-package/package.json')) expect(publishedManifest.dependencies).toEqual({ @@ -609,7 +611,7 @@ test.skip('convert specs with relative workspace protocols to regular version ra 'relative-workspace-protocol-package', '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`, - ]) + ], { env: SPAWN_ENV }) const { default: publishedManifest } = await import(path.resolve('node_modules/relative-workspace-protocol-package/package.json')) expect(publishedManifest.dependencies).toEqual({ @@ -678,7 +680,7 @@ describe('catalog protocol converted when publishing', () => { testPackageName, '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`, - ]) + ], { env: SPAWN_ENV }) const { default: publishedManifest } = await import(path.resolve(`node_modules/${testPackageName}/package.json`)) expect(publishedManifest.dependencies).toEqual({ 'is-positive': '1.0.0' }) @@ -740,7 +742,7 @@ describe('catalog protocol converted when publishing', () => { testPackageName, '--no-link-workspace-packages', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`, - ]) + ], { env: SPAWN_ENV }) const { default: publishedManifest } = await import(path.resolve(`node_modules/${testPackageName}/package.json`)) expect(publishedManifest.dependencies).toEqual({ 'is-positive': '1.0.0' }) @@ -769,7 +771,7 @@ test('publish: runs all the lifecycle scripts', async () => { }, }) - crossSpawn.sync(pnpmBin, ['install', '--ignore-scripts', '--store-dir=../store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['install', '--ignore-scripts', '--store-dir=../store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) await publish.handler({ ...DEFAULT_OPTS, @@ -806,7 +808,7 @@ test('publish: ignores all the lifecycle scripts when --ignore-scripts is used', }, }) - crossSpawn.sync(pnpmBin, ['install', '--ignore-scripts', '--store-dir=../store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) + crossSpawn.sync(pnpmBin, ['install', '--ignore-scripts', '--store-dir=../store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV }) await publish.handler({ ...DEFAULT_OPTS, diff --git a/releasing/commands/test/publish/utils/index.ts b/releasing/commands/test/publish/utils/index.ts index 84ea393eef..cd786adb16 100644 --- a/releasing/commands/test/publish/utils/index.ts +++ b/releasing/commands/test/publish/utils/index.ts @@ -29,6 +29,7 @@ export const DEFAULT_OPTS = { localAddress: undefined, lock: false, lockStaleDuration: 90, + minimumReleaseAge: 0, networkConcurrency: 16, offline: false, pending: false, diff --git a/releasing/exportable-manifest/test/index.test.ts b/releasing/exportable-manifest/test/index.test.ts index 1419a63b26..89d0a3fc99 100644 --- a/releasing/exportable-manifest/test/index.test.ts +++ b/releasing/exportable-manifest/test/index.test.ts @@ -10,6 +10,8 @@ import { writeYamlFileSync } from 'write-yaml-file' const pnpmBin = path.join(import.meta.dirname, '../../../pnpm/bin/pnpm.mjs') +const SPAWN_ENV = { ...process.env, pnpm_config_minimum_release_age: '0' } as NodeJS.ProcessEnv + const defaultOpts: MakePublishManifestOptions = { catalogs: {}, } @@ -163,7 +165,7 @@ test('workspace deps are replaced', async () => { writeYamlFileSync('pnpm-workspace.yaml', { packages: ['**', '!store/**'] }) - crossSpawn.sync(pnpmBin, ['install', '--store-dir=store']) + crossSpawn.sync(pnpmBin, ['install', '--store-dir=store'], { env: SPAWN_ENV }) process.chdir('workspace-protocol-package') @@ -227,7 +229,7 @@ test('catalog deps are replaced', async () => { } writeYamlFileSync('pnpm-workspace.yaml', workspaceManifest) - crossSpawn.sync(pnpmBin, ['install', '--store-dir=store']) + crossSpawn.sync(pnpmBin, ['install', '--store-dir=store'], { env: SPAWN_ENV }) process.chdir('catalog-protocol-package')