diff --git a/lib/modules/manager/npm/post-update/lerna.spec.ts b/lib/modules/manager/npm/post-update/lerna.spec.ts index 8b2277a322658010379cb737976ec99eec5f00ae..7be793ba6046f1c72f492b1efe4b5eecf4c6c66c 100644 --- a/lib/modules/manager/npm/post-update/lerna.spec.ts +++ b/lib/modules/manager/npm/post-update/lerna.spec.ts @@ -1,12 +1,16 @@ import { envMock, mockExecAll } from '../../../../../test/exec-util'; -import { env, partial } from '../../../../../test/util'; +import { env, mockedFunction, partial } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { RepoGlobalConfig } from '../../../../config/types'; import type { PackageFile, PostUpdateConfig } from '../../types'; import * as lernaHelper from './lerna'; +import { getNodeToolConstraint } from './node-version'; jest.mock('../../../../util/exec/env'); -jest.mock('../../npm/post-update/node-version'); +jest.mock('./node-version'); +jest.mock('../../../datasource'); + +process.env.BUILDPACK = 'true'; function lernaPkgFile(lernaClient: string): Partial<PackageFile> { return { @@ -26,7 +30,10 @@ function lernaPkgFileWithoutLernaDep( const config = partial<PostUpdateConfig>({}); describe('modules/manager/npm/post-update/lerna', () => { - const globalConfig: RepoGlobalConfig = { localDir: '' }; + const globalConfig: RepoGlobalConfig = { + localDir: '', + cacheDir: '/tmp/cache', + }; describe('generateLockFiles()', () => { beforeEach(() => { @@ -34,6 +41,10 @@ describe('modules/manager/npm/post-update/lerna', () => { jest.resetModules(); env.getChildProcessEnv.mockReturnValue(envMock.basic); GlobalConfig.set(globalConfig); + mockedFunction(getNodeToolConstraint).mockResolvedValueOnce({ + toolName: 'node', + constraint: '16.16.0', + }); }); it('returns if no lernaClient', async () => { @@ -120,6 +131,89 @@ describe('modules/manager/npm/post-update/lerna', () => { expect(res.error).toBeFalse(); expect(execSnapshots).toMatchSnapshot(); }); + + it('suppports docker', async () => { + const execSnapshots = mockExecAll(); + GlobalConfig.set({ ...globalConfig, binarySource: 'docker' }); + + const res = await lernaHelper.generateLockFiles( + lernaPkgFile('npm'), + 'some-dir', + { ...config, constraints: { npm: '6.0.0' } }, + {} + ); + expect(execSnapshots).toMatchObject([ + { + cmd: 'docker pull renovate/sidecar', + }, + { + cmd: 'docker ps --filter name=renovate_sidecar -aq', + }, + { + cmd: + 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + + '-v "/tmp/cache":"/tmp/cache" ' + + '-e BUILDPACK_CACHE_DIR ' + + '-w "some-dir" renovate/sidecar ' + + 'bash -l -c "' + + 'install-tool node 16.16.0 ' + + '&& ' + + 'install-tool npm 6.0.0 ' + + '&& ' + + 'hash -d npm 2>/dev/null || true ' + + '&& ' + + 'install-tool lerna 2.0.0 ' + + '&& ' + + 'lerna info || echo \\"Ignoring lerna info failure\\" ' + + '&& ' + + 'npm install --ignore-scripts --no-audit --package-lock-only ' + + '&& ' + + 'lerna bootstrap --no-ci --ignore-scripts -- --ignore-scripts --no-audit --package-lock-only' + + '"', + options: { + cwd: 'some-dir', + }, + }, + ]); + expect(res.error).toBeFalse(); + }); + + it('suppports binarySource=install', async () => { + const execSnapshots = mockExecAll(); + GlobalConfig.set({ ...globalConfig, binarySource: 'install' }); + + const res = await lernaHelper.generateLockFiles( + lernaPkgFile('npm'), + 'some-dir', + { ...config, constraints: { npm: '6.0.0' } }, + {} + ); + expect(res.error).toBeFalse(); + expect(execSnapshots).toMatchObject([ + { cmd: 'install-tool node 16.16.0' }, + { cmd: 'install-tool npm 6.0.0' }, + { cmd: 'hash -d npm 2>/dev/null || true' }, + { cmd: 'install-tool lerna 2.0.0' }, + { + cmd: 'lerna info || echo "Ignoring lerna info failure"', + options: { + cwd: 'some-dir', + }, + }, + { + cmd: 'npm install --ignore-scripts --no-audit --package-lock-only', + options: { + cwd: 'some-dir', + }, + }, + { + cmd: 'lerna bootstrap --no-ci --ignore-scripts -- --ignore-scripts --no-audit --package-lock-only', + options: { + cwd: 'some-dir', + }, + }, + ]); + }); }); describe('getLernaVersion()', () => { diff --git a/lib/modules/manager/npm/post-update/lerna.ts b/lib/modules/manager/npm/post-update/lerna.ts index 22d9a8717fb7651c33f86cceba88f5063c775ef2..40c2d6a2b962642ad409ddc11a2e8319f5e30623 100644 --- a/lib/modules/manager/npm/post-update/lerna.ts +++ b/lib/modules/manager/npm/post-update/lerna.ts @@ -1,22 +1,24 @@ import semver from 'semver'; -import { quote } from 'shlex'; import upath from 'upath'; import { GlobalConfig } from '../../../../config/global'; import { TEMPORARY_ERROR } from '../../../../constants/error-messages'; import { logger } from '../../../../logger'; import { exec } from '../../../../util/exec'; -import type { ExecOptions, ExtraEnv } from '../../../../util/exec/types'; +import type { + ExecOptions, + ExtraEnv, + ToolConstraint, +} from '../../../../util/exec/types'; import type { PackageFile, PostUpdateConfig } from '../../types'; -import { getNodeConstraint } from './node-version'; +import { getNodeToolConstraint } from './node-version'; import type { GenerateLockFileResult } from './types'; -import { getOptimizeCommand } from './yarn'; // Exported for testability export function getLernaVersion( lernaPackageFile: Partial<PackageFile> ): string { const lernaDep = lernaPackageFile.deps?.find((d) => d.depName === 'lerna'); - if (!lernaDep || !semver.validRange(lernaDep.currentValue)) { + if (!lernaDep?.currentValue || !semver.validRange(lernaDep.currentValue)) { logger.warn( // TODO: types (#7154) // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -24,8 +26,7 @@ export function getLernaVersion( ); return 'latest'; } - // TODO #7154 - return lernaDep.currentValue!; + return lernaDep.currentValue; } export async function generateLockFiles( @@ -41,28 +42,34 @@ export async function generateLockFiles( return { error: false }; } logger.debug(`Spawning lerna with ${lernaClient} to create lock files`); - const preCommands: string[] = []; + const toolConstraints: ToolConstraint[] = [ + await getNodeToolConstraint(config, []), + ]; const cmd: string[] = []; let cmdOptions = ''; try { if (lernaClient === 'yarn') { - let installYarn = 'npm i -g yarn'; + const yarnTool: ToolConstraint = { + toolName: 'yarn', + constraint: '^1.22.18', // needs to be a v1 yarn, otherwise v2 will be installed + }; const yarnCompatibility = config.constraints?.yarn; if (semver.validRange(yarnCompatibility)) { - installYarn += `@${quote(yarnCompatibility)}`; + yarnTool.constraint = yarnCompatibility; } - preCommands.push(installYarn); + toolConstraints.push(yarnTool); if (skipInstalls !== false) { - preCommands.push(getOptimizeCommand()); + // The following change causes Yarn 1.x to exit gracefully after updating the lock file but without installing node_modules + yarnTool.toolName = 'yarn-slim'; } cmdOptions = '--ignore-scripts --ignore-engines --ignore-platform'; } else if (lernaClient === 'npm') { - let installNpm = 'npm i -g npm'; + const npmTool: ToolConstraint = { toolName: 'npm' }; const npmCompatibility = config.constraints?.npm; if (semver.validRange(npmCompatibility)) { - installNpm += `@${quote(npmCompatibility)} || true`; + npmTool.constraint = npmCompatibility; } - preCommands.push(installNpm, 'hash -d npm 2>/dev/null || true'); + toolConstraints.push(npmTool); cmdOptions = '--ignore-scripts --no-audit'; if (skipInstalls !== false) { cmdOptions += ' --package-lock-only'; @@ -77,7 +84,6 @@ export async function generateLockFiles( lernaCommand = lernaCommand.replace('--ignore-scripts ', ''); } lernaCommand += cmdOptions; - const tagConstraint = await getNodeConstraint(config); const extraEnv: ExtraEnv = { NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE, npm_config_store: env.npm_config_store, @@ -86,11 +92,9 @@ export async function generateLockFiles( cwdFile: upath.join(lockFileDir, 'package.json'), extraEnv, docker: { - image: 'node', - tagScheme: 'node', - tagConstraint, + image: 'sidecar', }, - preCommands, + toolConstraints, }; // istanbul ignore if if (GlobalConfig.get('exposeAllEnv')) { @@ -99,7 +103,7 @@ export async function generateLockFiles( } const lernaVersion = getLernaVersion(lernaPackageFile); logger.debug('Using lerna version ' + lernaVersion); - preCommands.push(`npm i -g lerna@${quote(lernaVersion)}`); + toolConstraints.push({ toolName: 'lerna', constraint: lernaVersion }); cmd.push('lerna info || echo "Ignoring lerna info failure"'); cmd.push(`${lernaClient} install ${cmdOptions}`); cmd.push(lernaCommand); diff --git a/lib/modules/manager/npm/post-update/yarn.ts b/lib/modules/manager/npm/post-update/yarn.ts index e5a819d44f1054cf5b58bfe5eaa24ec7ce09fa6a..f46075e1257b500a21da1755c06b9f4f9faac226 100644 --- a/lib/modules/manager/npm/post-update/yarn.ts +++ b/lib/modules/manager/npm/post-update/yarn.ts @@ -74,9 +74,7 @@ export async function checkYarnrc( return { offlineMirror, yarnPath }; } -export function getOptimizeCommand( - fileName = '/home/ubuntu/.npm-global/lib/node_modules/yarn/lib/cli.js' -): string { +export function getOptimizeCommand(fileName: string): string { return `sed -i 's/ steps,/ steps.slice(0,1),/' ${quote(fileName)}`; } diff --git a/lib/util/exec/buildpack.ts b/lib/util/exec/buildpack.ts index 3224d65800f21d63fa006f36a7fb79dea46f7411..247bd49c10eaeb38658b92a3a8814b1cc27d9cc1 100644 --- a/lib/util/exec/buildpack.ts +++ b/lib/util/exec/buildpack.ts @@ -63,6 +63,11 @@ const allToolConfig: Record<string, ToolConfig> = { depName: 'jsonnet-bundler/jsonnet-bundler', versioning: semverVersioningId, }, + lerna: { + datasource: 'npm', + depName: 'lerna', + versioning: npmVersioningId, + }, node: { datasource: 'node', depName: 'node',