mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 10:09:50 +00:00
6f0b6a56d3
* fix(manager/npm): pass --no-frozen-lockfile to pnpm lock file updates pnpm applies an explicitly-set frozenLockfile: true from pnpm-workspace.yaml even to 'pnpm install --lockfile-only', so lock file regeneration always fails with ERR_PNPM_OUTDATED_LOCKFILE in repos that set it: pnpm's CI frozen default has a --lockfile-only carve-out, but explicitly-set values skip that carve-out, and only CLI flags take precedence over pnpm-workspace.yaml settings. 'pnpm update' is not affected by frozen mode (and rejects the flag), so only the install command gets it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: less verbose comment --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
284 lines
9.7 KiB
TypeScript
284 lines
9.7 KiB
TypeScript
import { isNumber, isNumericString } from '@sindresorhus/is';
|
|
import { quote } from 'shlex';
|
|
import upath from 'upath';
|
|
import { GlobalConfig } from '../../../../config/global.ts';
|
|
import { TEMPORARY_ERROR } from '../../../../constants/error-messages.ts';
|
|
import { logger } from '../../../../logger/index.ts';
|
|
import { exec, getToolSettingsOptions } from '../../../../util/exec/index.ts';
|
|
import type {
|
|
ExecOptions,
|
|
ExtraEnv,
|
|
ToolConstraint,
|
|
} from '../../../../util/exec/types.ts';
|
|
import {
|
|
deleteLocalFile,
|
|
ensureCacheDir,
|
|
getSiblingFileName,
|
|
localPathExists,
|
|
readLocalFile,
|
|
} from '../../../../util/fs/index.ts';
|
|
import { uniqueStrings } from '../../../../util/string.ts';
|
|
import { parseSingleYaml } from '../../../../util/yaml.ts';
|
|
import type { PostUpdateConfig, Upgrade } from '../../types.ts';
|
|
import { PNPM_CACHE_DIR, PNPM_STORE_DIR } from '../constants.ts';
|
|
import type { PnpmWorkspaceFile } from '../extract/types.ts';
|
|
import { getNodeToolConstraint } from './node-version.ts';
|
|
import type { GenerateLockFileResult, PnpmLockFile } from './types.ts';
|
|
import {
|
|
getNodeOptions,
|
|
getPackageManagerVersion,
|
|
lazyLoadPackageJson,
|
|
} from './utils.ts';
|
|
|
|
function getPnpmConstraintFromUpgrades(upgrades: Upgrade[]): string | null {
|
|
for (const upgrade of upgrades) {
|
|
if (upgrade.depName === 'pnpm' && upgrade.newVersion) {
|
|
return upgrade.newVersion;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function generateLockFile(
|
|
lockFileDir: string,
|
|
env: NodeJS.ProcessEnv,
|
|
config: PostUpdateConfig,
|
|
upgrades: Upgrade[] = [],
|
|
): Promise<GenerateLockFileResult> {
|
|
const lockFileName = upath.join(lockFileDir, 'pnpm-lock.yaml');
|
|
logger.debug(`Spawning pnpm install to create ${lockFileName}`);
|
|
let lockFile: string | null = null;
|
|
const commands: string[] = [];
|
|
try {
|
|
const lazyPgkJson = lazyLoadPackageJson(lockFileDir);
|
|
const pnpmToolConstraint: ToolConstraint = {
|
|
toolName: 'pnpm',
|
|
constraint:
|
|
getPnpmConstraintFromUpgrades(upgrades) ?? // if pnpm is being upgraded, it comes first
|
|
config.constraints?.pnpm ?? // from user config or extraction
|
|
getPackageManagerVersion('pnpm', await lazyPgkJson.getValue()) ?? // look in package.json > packageManager or engines
|
|
(await getConstraintFromLockFile(lockFileName)), // use lockfileVersion to find pnpm version range
|
|
};
|
|
|
|
const pnpmConfigCacheDir = await ensureCacheDir(PNPM_CACHE_DIR);
|
|
const pnpmConfigStoreDir = await ensureCacheDir(PNPM_STORE_DIR);
|
|
const extraEnv: ExtraEnv = {
|
|
// those arwe no longer working and it's unclear if they ever worked
|
|
NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE,
|
|
npm_config_store: env.npm_config_store,
|
|
// these are used by pnpm v5 to v10. Maybe earlier versions too
|
|
npm_config_cache_dir: pnpmConfigCacheDir,
|
|
npm_config_store_dir: pnpmConfigStoreDir,
|
|
// pnpm stops reading npm_config_* env vars since v11
|
|
pnpm_config_cache_dir: pnpmConfigCacheDir,
|
|
pnpm_config_store_dir: pnpmConfigStoreDir,
|
|
};
|
|
|
|
const { nodeMaxMemory } = getToolSettingsOptions(config.toolSettings);
|
|
if (nodeMaxMemory) {
|
|
extraEnv.NODE_OPTIONS = getNodeOptions(nodeMaxMemory);
|
|
}
|
|
|
|
const execOptions: ExecOptions = {
|
|
cwdFile: lockFileName,
|
|
extraEnv,
|
|
docker: {},
|
|
toolConstraints: [
|
|
await getNodeToolConstraint(config, upgrades, lockFileDir, lazyPgkJson),
|
|
pnpmToolConstraint,
|
|
],
|
|
};
|
|
/* v8 ignore next -- needs test */
|
|
if (GlobalConfig.get('exposeAllEnv')) {
|
|
extraEnv.NPM_AUTH = env.NPM_AUTH;
|
|
extraEnv.NPM_EMAIL = env.NPM_EMAIL;
|
|
}
|
|
|
|
const pnpmWorkspaceFilePath = getSiblingFileName(
|
|
lockFileName,
|
|
'pnpm-workspace.yaml',
|
|
);
|
|
|
|
let args = '--lockfile-only';
|
|
|
|
// If it's not a workspaces project/monorepo, but single project with unrelated other npm project in source tree (for example, a git submodule),
|
|
// `--recursive` will install un-wanted project.
|
|
// we should avoid this.
|
|
if (await localPathExists(pnpmWorkspaceFilePath)) {
|
|
const pnpmWorkspace = parseSingleYaml<PnpmWorkspaceFile>(
|
|
(await readLocalFile(pnpmWorkspaceFilePath, 'utf8'))!,
|
|
);
|
|
if (pnpmWorkspace?.packages?.length) {
|
|
logger.debug(
|
|
`Found pnpm workspace with ${pnpmWorkspace.packages.length} package definitions`,
|
|
);
|
|
// we are in a monorepo, 'pnpm update' needs the '--recursive' flag contrary to 'pnpm install'
|
|
args += ' --recursive';
|
|
}
|
|
}
|
|
if (!GlobalConfig.get('allowScripts')) {
|
|
// If the admin disallows scripts, then neither scripts nor the pnpmfile should be run
|
|
args += ' --ignore-scripts';
|
|
args += ' --ignore-pnpmfile';
|
|
} else if (config.ignoreScripts) {
|
|
// If the admin allows scripts then always allow the pnpmfile
|
|
args += ' --ignore-scripts';
|
|
}
|
|
|
|
logger.trace({ args }, 'pnpm command options');
|
|
|
|
const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);
|
|
|
|
if (lockUpdates.length !== upgrades.length) {
|
|
// This command updates the lock file based on package.json.
|
|
// Pass `--no-frozen-lockfile` to ensure the lockfile is updated
|
|
commands.push(`pnpm install ${args} --no-frozen-lockfile`);
|
|
}
|
|
|
|
// rangeStrategy = update-lockfile
|
|
if (lockUpdates.length) {
|
|
logger.debug('Performing lockfileUpdate (pnpm)');
|
|
commands.push(
|
|
`pnpm update --no-save ${lockUpdates
|
|
// TODO: types (#22198)
|
|
.map((update) => `${update.packageName!}@${update.newVersion!}`)
|
|
.filter(uniqueStrings)
|
|
.map(quote)
|
|
.join(' ')} ${args}`,
|
|
);
|
|
}
|
|
|
|
// postUpdateOptions
|
|
if (config.postUpdateOptions?.includes('pnpmDedupe')) {
|
|
commands.push(`pnpm dedupe ${args.replace(' --recursive', '')}`);
|
|
}
|
|
|
|
if (upgrades.find((upgrade) => upgrade.isLockFileMaintenance)) {
|
|
logger.debug(
|
|
`Removing ${lockFileName} first due to lock file maintenance upgrade`,
|
|
);
|
|
try {
|
|
await deleteLocalFile(lockFileName);
|
|
} catch (err) /* v8 ignore next -- TODO: add test #40625 */ {
|
|
logger.debug(
|
|
{ err, lockFileName },
|
|
'Error removing `pnpm-lock.yaml` for lock file maintenance',
|
|
);
|
|
}
|
|
}
|
|
|
|
await exec(commands, execOptions);
|
|
lockFile = await readLocalFile(lockFileName, 'utf8');
|
|
} catch (err) {
|
|
// v8 ignore if -- TODO: add test #40625
|
|
if (err.message === TEMPORARY_ERROR) {
|
|
throw err;
|
|
}
|
|
logger.debug(
|
|
{
|
|
commands,
|
|
err,
|
|
stdout: err.stdout,
|
|
stderr: err.stderr,
|
|
type: 'pnpm',
|
|
},
|
|
'lock file error',
|
|
);
|
|
return { error: true, stderr: err.stderr, stdout: err.stdout };
|
|
}
|
|
return { lockFile };
|
|
}
|
|
|
|
export async function getConstraintFromLockFile(
|
|
lockFileName: string,
|
|
): Promise<string | null> {
|
|
let constraint: string | null = null;
|
|
try {
|
|
const lockfileContent = await readLocalFile(lockFileName, 'utf8');
|
|
if (!lockfileContent) {
|
|
logger.trace(`Empty pnpm lock file: ${lockFileName}`);
|
|
return null;
|
|
}
|
|
// TODO: use schema (#9610)
|
|
const pnpmLock = parseSingleYaml<PnpmLockFile>(lockfileContent);
|
|
if (
|
|
!isNumber(pnpmLock?.lockfileVersion) &&
|
|
!isNumericString(pnpmLock?.lockfileVersion)
|
|
) {
|
|
logger.trace(`Invalid pnpm lockfile version: ${lockFileName}`);
|
|
return null;
|
|
}
|
|
// find matching lockfileVersion and use its constraints
|
|
// if no match found use lockfileVersion 5
|
|
// lockfileVersion 5 is the minimum version required to generate the pnpm-lock.yaml file
|
|
const { lowerConstraint, upperConstraint } = lockToPnpmVersionMapping.find(
|
|
(m) => m.lockfileVersion === pnpmLock.lockfileVersion,
|
|
) ?? {
|
|
lockfileVersion: 5.0,
|
|
lowerConstraint: '>=3',
|
|
upperConstraint: '<3.5.0',
|
|
};
|
|
constraint = lowerConstraint;
|
|
if (upperConstraint) {
|
|
constraint += ` ${upperConstraint}`;
|
|
}
|
|
} catch (err) {
|
|
logger.warn({ err }, 'Error getting pnpm constraints from lock file');
|
|
}
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* pnpm lockfiles have corresponding version numbers called "lockfileVersion"
|
|
* each lockfileVersion can only be generated by a certain pnpm version range
|
|
* eg. lockfileVersion: 5.4 can only be generated by pnpm version >=7 && <8
|
|
* official list can be found here : https://github.com/pnpm/spec/tree/master/lockfile
|
|
* we use the mapping present below to find the compatible pnpm version range for a given lockfileVersion
|
|
*
|
|
* the various terms used in the mapping are explained below:
|
|
* lowerConstraint : lowest pnpm version that can generate the lockfileVersion
|
|
* upperConstraint : highest pnpm version that can generate the lockfileVersion
|
|
* lowerBound : highest pnpm version that is less than the lowerConstraint
|
|
* upperBound : lowest pnpm version that is greater than the upperConstraint
|
|
*
|
|
* To handle future lockfileVersions, we need to:
|
|
* 1. add a upperBound and upperConstraint to the current latest lockfileVersion
|
|
* 2. add an object for the new lockfileVersion with lowerBound and lowerConstraint
|
|
*
|
|
* lockfileVersion from v6 on are strings
|
|
*/
|
|
const lockToPnpmVersionMapping: LockToPnpmVersionMapping[] = [
|
|
{ lockfileVersion: '9.0', lowerConstraint: '>=9' },
|
|
{
|
|
lockfileVersion: '6.0',
|
|
lowerConstraint: '>=7.24.2',
|
|
upperConstraint: '<9',
|
|
},
|
|
{
|
|
lockfileVersion: 5.4,
|
|
lowerConstraint: '>=7',
|
|
upperConstraint: '<8',
|
|
},
|
|
{
|
|
lockfileVersion: 5.3,
|
|
lowerConstraint: '>=6',
|
|
upperConstraint: '<7',
|
|
},
|
|
{
|
|
lockfileVersion: 5.2,
|
|
lowerConstraint: '>=5.10.0',
|
|
upperConstraint: '<6',
|
|
},
|
|
{
|
|
lockfileVersion: 5.1,
|
|
lowerConstraint: '>=3.5.0',
|
|
upperConstraint: '<5.9.3',
|
|
},
|
|
];
|
|
|
|
interface LockToPnpmVersionMapping {
|
|
lockfileVersion: string | number;
|
|
lowerConstraint: string;
|
|
upperConstraint?: string;
|
|
}
|