diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 10f6c161de366f489bc8c04005ee500e4dabc110..3d4b58ed791786bb86e92a289b8a6d556c85f1be 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1232,6 +1232,15 @@ Typically you would encrypt it and put it inside the `encrypted` object. See [Private npm module support](https://docs.renovatebot.com/getting-started/private-packages) for details on how this is used. +## npmrcMerge + +This option exists to provide flexibility about whether `npmrc` strings in config should override `.npmrc` files in the repo, or be merged with them. +In some situations you need the ability to force override `.npmrc` contents in a repo (`npmMerge=false`) while in others you might want to simply supplement the settings already in the `.npmrc` (`npmMerge=true`). +A use case for the latter is if you are a Renovate bot admin and wish to provide a default token for `npmjs.org` without removing any other `.npmrc` settings which individual repositories have configured (such as scopes/registries). + +If `false` (default), it means that defining `config.npmrc` will result in any `.npmrc` file in the repo being overridden and therefore its values ignored. +If configured to `true`, it means that any `.npmrc` file in the repo will have `config.npmrc` prepended to it before running `npm`. + ## packageRules `packageRules` is a powerful feature that lets you apply rules to individual packages or to groups of packages using regex pattern matching. diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index 9007d04028df64198fe424364e31329f302640a9..0af1ac31133e9c976c9047c3ba0277da8457cec2 100644 --- a/docs/usage/getting-started/private-packages.md +++ b/docs/usage/getting-started/private-packages.md @@ -180,6 +180,8 @@ You can add an `.npmrc` authentication line to your Renovate config under the fi ``` If configured like this, Renovate will use this to authenticate with npm and will ignore any `.npmrc` files(s) it finds checked into the repository. +If you wish for the values in your `config.npmrc` to be _merged_ (prepended) with any values found in repos then also set `config.npmrcMerge=true`. +This merge approach is similar to how `npm` itself behaves if `.npmrc` is found in both the user home directory as well as a project. #### Add npmToken to Renovate config diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 7a757b04d8cec1f986b72716fdf8e05e0cabf8a3..90105c4e4de306a2eeca332db1cdd8e88eda983c 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -589,6 +589,14 @@ const options: RenovateOptions[] = [ stage: 'branch', type: 'string', }, + { + name: 'npmrcMerge', + description: + 'Whether to merge config.npmrc with repo .npmrc content if both are found.', + stage: 'branch', + type: 'boolean', + default: false, + }, { name: 'npmToken', description: 'npm token used for authenticating with the default registry.', diff --git a/lib/config/types.ts b/lib/config/types.ts index 6094917fa250c2fcbf374de2441804ae7d15958e..a773d15247b84110c7e709d16933716a8e5b296b 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -44,6 +44,7 @@ export interface RenovateSharedConfig { dependencyDashboardApproval?: boolean; hashedBranchLength?: number; npmrc?: string; + npmrcMerge?: boolean; platform?: string; postUpgradeTasks?: PostUpgradeTasks; prBodyColumns?: string[]; diff --git a/lib/manager/npm/extract/index.spec.ts b/lib/manager/npm/extract/index.spec.ts index e513d19e92813b2a63ca5fe14e251322611d1618..538c8fbea66e10c3d58e6124e8dab4e36ff47e4f 100644 --- a/lib/manager/npm/extract/index.spec.ts +++ b/lib/manager/npm/extract/index.spec.ts @@ -122,7 +122,7 @@ describe('manager/npm/extract/index', () => { ); expect(res.npmrc).toBeDefined(); }); - it('ignores .npmrc when config.npmrc is defined', async () => { + it('ignores .npmrc when config.npmrc is defined and npmrcMerge=false', async () => { fs.readLocalFile = jest.fn((fileName) => { if (fileName === '.npmrc') { return 'some-npmrc\n'; @@ -136,6 +136,20 @@ describe('manager/npm/extract/index', () => { ); expect(res.npmrc).toBeUndefined(); }); + it('reads .npmrc when config.npmrc is merged', async () => { + fs.readLocalFile = jest.fn((fileName) => { + if (fileName === '.npmrc') { + return 'repo-npmrc\n'; + } + return null; + }); + const res = await npmExtract.extractPackageFile( + input01Content, + 'package.json', + { npmrc: 'config-npmrc', npmrcMerge: true } + ); + expect(res.npmrc).toEqual(`config-npmrc\nrepo-npmrc\n`); + }); it('finds and filters .npmrc with variables', async () => { fs.readLocalFile = jest.fn((fileName) => { if (fileName === '.npmrc') { diff --git a/lib/manager/npm/extract/index.ts b/lib/manager/npm/extract/index.ts index 4cf396790500b0d26b1578db351d50f29f6dd538..e3a7bf30a47bf3e256d5f09bfcaf7374fe3bae9c 100644 --- a/lib/manager/npm/extract/index.ts +++ b/lib/manager/npm/extract/index.ts @@ -96,29 +96,33 @@ export async function extractPackageFile( let npmrc: string; const npmrcFileName = getSiblingFileName(fileName, '.npmrc'); - const npmrcContent = await readLocalFile(npmrcFileName, 'utf8'); - if (is.string(npmrcContent)) { - if (is.string(config.npmrc)) { + let repoNpmrc = await readLocalFile(npmrcFileName, 'utf8'); + if (is.string(repoNpmrc)) { + if (is.string(config.npmrc) && !config.npmrcMerge) { logger.debug( { npmrcFileName }, - 'Repo .npmrc file is ignored due to presence of config.npmrc' + 'Repo .npmrc file is ignored due to config.npmrc with config.npmrcMerge=force' ); } else { - npmrc = npmrcContent; - if (npmrc?.includes('package-lock')) { + npmrc = config.npmrc || ''; + if (npmrc.length) { + npmrc = npmrc.replace(/\n?$/, '\n'); + } + if (repoNpmrc?.includes('package-lock')) { logger.debug('Stripping package-lock setting from .npmrc'); - npmrc = npmrc.replace(/(^|\n)package-lock.*?(\n|$)/g, '\n'); + repoNpmrc = repoNpmrc.replace(/(^|\n)package-lock.*?(\n|$)/g, '\n'); } - if (npmrc.includes('=${') && !getGlobalConfig().exposeAllEnv) { + if (repoNpmrc.includes('=${') && !getGlobalConfig().exposeAllEnv) { logger.debug( { npmrcFileName }, 'Stripping .npmrc file of lines with variables' ); - npmrc = npmrc + repoNpmrc = repoNpmrc .split('\n') .filter((line) => !line.includes('=${')) .join('\n'); } + npmrc += repoNpmrc; } } diff --git a/lib/manager/types.ts b/lib/manager/types.ts index 69358a103eb96af0d3e02a26d7a77767edc14370..5e1842c53d4130703f573980eb4817b3f807eee9 100644 --- a/lib/manager/types.ts +++ b/lib/manager/types.ts @@ -20,6 +20,7 @@ export interface ExtractConfig { gradle?: { timeout?: number }; aliases?: Record<string, string>; npmrc?: string; + npmrcMerge?: boolean; skipInstalls?: boolean; updateInternalDeps?: boolean; deepExtract?: boolean;