From 8d4970b60abc0242e6c5c498e7b9a6dbc4aba84c Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Tue, 2 Jun 2020 10:46:02 +0200 Subject: [PATCH] feat(npm): dynamic node tag selection (#6405) Detects preferred node version for npm, yarn and pnpm docker calls. --- .../npm/post-update/node-version.spec.ts | 45 ++++++++++++++++++ lib/manager/npm/post-update/node-version.ts | 47 +++++++++++++++++++ lib/manager/npm/post-update/npm.spec.ts | 1 + lib/manager/npm/post-update/npm.ts | 4 ++ lib/manager/npm/post-update/pnpm.spec.ts | 1 + lib/manager/npm/post-update/pnpm.ts | 4 ++ lib/manager/npm/post-update/yarn.spec.ts | 1 + lib/manager/npm/post-update/yarn.ts | 4 ++ 8 files changed, 107 insertions(+) create mode 100644 lib/manager/npm/post-update/node-version.spec.ts create mode 100644 lib/manager/npm/post-update/node-version.ts diff --git a/lib/manager/npm/post-update/node-version.spec.ts b/lib/manager/npm/post-update/node-version.spec.ts new file mode 100644 index 0000000000..af76ab43ec --- /dev/null +++ b/lib/manager/npm/post-update/node-version.spec.ts @@ -0,0 +1,45 @@ +import { mocked } from '../../../../test/util'; +import * as fs_ from '../../../util/fs'; +import { getNodeConstraint } from './node-version'; + +const fs = mocked(fs_); + +describe('getNodeConstraint', () => { + it('returns package.json range', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce('{"engines":{"node":"^12.16.0"}}'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('^12.16.0'); + }); + it('returns .node-version value', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce('12.16.1\n'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('12.16.1'); + }); + it('returns .nvmrc value', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce('12.16.2\n'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('12.16.2'); + }); + it('ignores unusable ranges in dotfiles', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce('latest'); + fs.readLocalFile.mockResolvedValueOnce('lts'); + fs.readLocalFile.mockResolvedValueOnce('{"engines":{"node":"^12.16.0"}}'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('^12.16.0'); + }); + it('returns no constraint', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const res = await getNodeConstraint('package.json'); + expect(res).toBeNull(); + }); +}); diff --git a/lib/manager/npm/post-update/node-version.ts b/lib/manager/npm/post-update/node-version.ts new file mode 100644 index 0000000000..28936276c9 --- /dev/null +++ b/lib/manager/npm/post-update/node-version.ts @@ -0,0 +1,47 @@ +import { validRange } from 'semver'; +import { logger } from '../../../logger'; +import { getSiblingFileName, readLocalFile } from '../../../util/fs'; + +async function getNodeFile(filename: string): Promise<string> | null { + try { + const constraint = (await readLocalFile(filename, 'utf8')) + .split('\n')[0] + .replace(/^v/, ''); + if (validRange(constraint)) { + logger.debug(`Using node constraint "${constraint}" from ${filename}`); + return constraint; + } + } catch (err) { + // do nothing + } + return null; +} + +async function getPackageJsonConstraint( + filename: string +): Promise<string> | null { + try { + const pj = JSON.parse(await readLocalFile(filename, 'utf8')); + const constraint = pj?.engines?.node; + if (constraint && validRange(constraint)) { + logger.debug(`Using node constraint "${constraint}" from package.json`); + return constraint; + } + } catch (err) { + // do nothing + } + return null; +} + +export async function getNodeConstraint( + filename: string +): Promise<string> | null { + const constraint = + (await getNodeFile(getSiblingFileName(filename, '.nvmrc'))) || + (await getNodeFile(getSiblingFileName(filename, '.node-version'))) || + (await getPackageJsonConstraint(filename)); + if (!constraint) { + logger.debug('No node constraint found - using latest'); + } + return constraint; +} diff --git a/lib/manager/npm/post-update/npm.spec.ts b/lib/manager/npm/post-update/npm.spec.ts index f27e6db4e7..83081050bd 100644 --- a/lib/manager/npm/post-update/npm.spec.ts +++ b/lib/manager/npm/post-update/npm.spec.ts @@ -10,6 +10,7 @@ import * as npmHelper from './npm'; jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../../util/exec/env'); +jest.mock('./node-version'); const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); diff --git a/lib/manager/npm/post-update/npm.ts b/lib/manager/npm/post-update/npm.ts index abf0d19f44..18511f8180 100644 --- a/lib/manager/npm/post-update/npm.ts +++ b/lib/manager/npm/post-update/npm.ts @@ -4,6 +4,7 @@ import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../../constants/error-message import { logger } from '../../../logger'; import { ExecOptions, exec } from '../../../util/exec'; import { PostUpdateConfig, Upgrade } from '../../common'; +import { getNodeConstraint } from './node-version'; export interface GenerateLockFileResult { error?: boolean; @@ -36,6 +37,7 @@ export async function generateLockFile( logger.debug('Updating lock file only'); cmdOptions += '--package-lock-only --no-audit'; } + const tagConstraint = await getNodeConstraint(config.packageFile); const execOptions: ExecOptions = { cwd, extraEnv: { @@ -44,6 +46,8 @@ export async function generateLockFile( }, docker: { image: 'renovate/node', + tagScheme: 'npm', + tagConstraint, preCommands, }, }; diff --git a/lib/manager/npm/post-update/pnpm.spec.ts b/lib/manager/npm/post-update/pnpm.spec.ts index 6eedd09a0b..0c2991a401 100644 --- a/lib/manager/npm/post-update/pnpm.spec.ts +++ b/lib/manager/npm/post-update/pnpm.spec.ts @@ -9,6 +9,7 @@ import * as _pnpmHelper from './pnpm'; jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../../util/exec/env'); +jest.mock('./node-version'); const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); diff --git a/lib/manager/npm/post-update/pnpm.ts b/lib/manager/npm/post-update/pnpm.ts index 9b1d5383fd..aba81ac352 100644 --- a/lib/manager/npm/post-update/pnpm.ts +++ b/lib/manager/npm/post-update/pnpm.ts @@ -3,6 +3,7 @@ import { join } from 'upath'; import { logger } from '../../../logger'; import { ExecOptions, exec } from '../../../util/exec'; import { PostUpdateConfig } from '../../common'; +import { getNodeConstraint } from './node-version'; export interface GenerateLockFileResult { error?: boolean; @@ -23,6 +24,7 @@ export async function generateLockFile( let cmd = 'pnpm'; try { const preCommands = ['npm i -g pnpm']; + const tagConstraint = await getNodeConstraint(config.packageFile); const execOptions: ExecOptions = { cwd, extraEnv: { @@ -31,6 +33,8 @@ export async function generateLockFile( }, docker: { image: 'renovate/node', + tagScheme: 'npm', + tagConstraint, preCommands, }, }; diff --git a/lib/manager/npm/post-update/yarn.spec.ts b/lib/manager/npm/post-update/yarn.spec.ts index 09f50f5eaa..2ad39862c9 100644 --- a/lib/manager/npm/post-update/yarn.spec.ts +++ b/lib/manager/npm/post-update/yarn.spec.ts @@ -8,6 +8,7 @@ import * as _yarnHelper from './yarn'; jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../../util/exec/env'); +jest.mock('./node-version'); const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); diff --git a/lib/manager/npm/post-update/yarn.ts b/lib/manager/npm/post-update/yarn.ts index f2f70b482f..8a5c71a0ea 100644 --- a/lib/manager/npm/post-update/yarn.ts +++ b/lib/manager/npm/post-update/yarn.ts @@ -6,6 +6,7 @@ import { DatasourceError } from '../../../datasource'; import { logger } from '../../../logger'; import { ExecOptions, exec } from '../../../util/exec'; import { PostUpdateConfig, Upgrade } from '../../common'; +import { getNodeConstraint } from './node-version'; export interface GenerateLockFileResult { error?: boolean; @@ -55,6 +56,7 @@ export async function generateLockFile( if (global.trustLevel !== 'high' || config.ignoreScripts) { cmdOptions += ' --ignore-scripts --ignore-engines --ignore-platform'; } + const tagConstraint = await getNodeConstraint(config.packageFile); const execOptions: ExecOptions = { cwd, extraEnv: { @@ -63,6 +65,8 @@ export async function generateLockFile( }, docker: { image: 'renovate/node', + tagScheme: 'npm', + tagConstraint, preCommands, }, }; -- GitLab