feat!: set default minimumReleaseAge to 1 day (1440 minutes) (#11158)

set default minimumReleaseAge to 1 day

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
sotanengel
2026-04-04 20:26:22 +09:00
committed by GitHub
parent 3d67773188
commit c7203b99ad
18 changed files with 37 additions and 12 deletions

View File

@@ -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`.

View File

@@ -28,6 +28,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -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',

View File

@@ -34,6 +34,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -28,6 +28,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -28,6 +28,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -30,6 +30,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -33,6 +33,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -35,6 +35,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -27,6 +27,7 @@ const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
preferWorkspacePackages: true,

View File

@@ -25,6 +25,7 @@ const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
preferWorkspacePackages: true,

View File

@@ -32,6 +32,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -30,6 +30,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -194,6 +194,7 @@ function createEnv (opts?: { storeDir?: string }): NodeJS.ProcessEnv {
const env: Record<string, string> = {
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'),

View File

@@ -32,6 +32,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -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,

View File

@@ -29,6 +29,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -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')