diff --git a/lib/config/decrypt.ts b/lib/config/decrypt.ts index 09443ad5f4c835..ac560c4d456a45 100644 --- a/lib/config/decrypt.ts +++ b/lib/config/decrypt.ts @@ -1,6 +1,5 @@ import is from '@sindresorhus/is'; import { logger } from '../logger'; -import { maskToken } from '../util/mask'; import { regEx } from '../util/regex'; import { addSecretForSanitizing } from '../util/sanitize'; import { ensureTrailingSlash } from '../util/url'; @@ -166,31 +165,8 @@ export async function decryptConfig( logger.debug(`Decrypted ${eKey}`); if (eKey === 'npmToken') { const token = decryptedStr.replace(regEx(/\n$/), ''); + decryptedConfig[eKey] = token; addSecretForSanitizing(token); - logger.debug( - { decryptedToken: maskToken(token) }, - 'Migrating npmToken to npmrc', - ); - if (is.string(decryptedConfig.npmrc)) { - /* eslint-disable no-template-curly-in-string */ - if (decryptedConfig.npmrc.includes('${NPM_TOKEN}')) { - logger.debug('Replacing ${NPM_TOKEN} with decrypted token'); - decryptedConfig.npmrc = decryptedConfig.npmrc.replace( - regEx(/\${NPM_TOKEN}/g), - token, - ); - } else { - logger.debug('Appending _authToken= to end of existing npmrc'); - decryptedConfig.npmrc = decryptedConfig.npmrc.replace( - regEx(/\n?$/), - `\n_authToken=${token}\n`, - ); - } - /* eslint-enable no-template-curly-in-string */ - } else { - logger.debug('Adding npmrc to config'); - decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`; - } } else { decryptedConfig[eKey] = decryptedStr; addSecretForSanitizing(decryptedStr); diff --git a/lib/config/decrypt/legacy.spec.ts b/lib/config/decrypt/legacy.spec.ts index c36943526e319c..936cd1c369ff56 100644 --- a/lib/config/decrypt/legacy.spec.ts +++ b/lib/config/decrypt/legacy.spec.ts @@ -44,10 +44,7 @@ describe('config/decrypt/legacy', () => { }; const res = await decryptConfig(config, repository); expect(res.encrypted).toBeUndefined(); - expect(res.npmToken).toBeUndefined(); - expect(res.npmrc).toBe( - '//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n', - ); + expect(res.npmToken).toBe('abcdef-ghijklm-nopqf-stuvwxyz'); }); it('appends npm token in npmrc', async () => { @@ -59,10 +56,7 @@ describe('config/decrypt/legacy', () => { }; const res = await decryptConfig(config, repository); expect(res.encrypted).toBeUndefined(); - expect(res.npmToken).toBeUndefined(); - expect(res.npmrc).toBe( - `foo=bar\n_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n`, - ); + expect(res.npmToken).toBe('abcdef-ghijklm-nopqf-stuvwxyz'); }); it('decrypts nested', async () => { @@ -88,9 +82,8 @@ describe('config/decrypt/legacy', () => { expect(res.packageFiles[0].devDependencies.branchPrefix).toBe( 'abcdef-ghijklm-nopqf-stuvwxyz', ); - expect(res.packageFiles[0].devDependencies.npmToken).toBeUndefined(); - expect(res.packageFiles[0].devDependencies.npmrc).toBe( - '//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n', + expect(res.packageFiles[0].devDependencies.npmToken).toBe( + 'abcdef-ghijklm-nopqf-stuvwxyz', ); }); }); diff --git a/lib/workers/repository/init/merge.spec.ts b/lib/workers/repository/init/merge.spec.ts index 23e1a27d14534d..00ee13353d132f 100644 --- a/lib/workers/repository/init/merge.spec.ts +++ b/lib/workers/repository/init/merge.spec.ts @@ -7,6 +7,7 @@ import { platform, scm, } from '../../../../test/util'; +import * as decrypt from '../../../config/decrypt'; import { getConfig } from '../../../config/defaults'; import * as _migrateAndValidate from '../../../config/migrate-validate'; import * as _migrate from '../../../config/migration'; @@ -20,6 +21,7 @@ import { checkForRepoConfigError, detectRepoFileConfig, mergeRenovateConfig, + setNpmTokenInNpmrc, } from './merge'; jest.mock('../../../util/fs'); @@ -385,5 +387,86 @@ describe('workers/repository/init/merge', () => { }), ).toBeDefined(); }); + + it('sets npmToken to npmrc when it is not inside encrypted', async () => { + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); + fs.readLocalFile.mockResolvedValue( + '{"npmToken": "{{ secrets.NPM_TOKEN }}", "npmrc": "something_authToken=${NPM_TOKEN}"}', + ); + migrateAndValidate.migrateAndValidate.mockResolvedValue({ + ...config, + npmToken: '{{ secrets.NPM_TOKEN }}', + npmrc: 'something_authToken=${NPM_TOKEN}', + warnings: [], + errors: [], + }); + migrate.migrateConfig.mockImplementation((c) => ({ + isMigrated: true, + migratedConfig: c, + })); + config.secrets = { + NPM_TOKEN: 'confidential', + }; + const res = await mergeRenovateConfig(config); + expect(res.npmrc).toBe('something_authToken=confidential'); + }); + + it('sets npmToken to npmrc when it is inside encrypted', async () => { + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); + fs.readLocalFile.mockResolvedValue( + '{"encrypted": { "npmToken": "encrypted-token" }, "npmrc": "something_authToken=${NPM_TOKEN}"}', + ); + migrateAndValidate.migrateAndValidate.mockResolvedValue({ + ...config, + npmrc: 'something_authToken=${NPM_TOKEN}', + encrypted: { + npmToken: 'encrypted-token', + }, + warnings: [], + errors: [], + }); + migrate.migrateConfig.mockImplementation((c) => ({ + isMigrated: true, + migratedConfig: c, + })); + jest.spyOn(decrypt, 'decryptConfig').mockResolvedValueOnce({ + ...config, + npmrc: 'something_authToken=${NPM_TOKEN}', + npmToken: 'token', + }); + const res = await mergeRenovateConfig(config); + expect(res.npmrc).toBe('something_authToken=token'); + }); + }); + + describe('setNpmTokenInNpmrc', () => { + it('skips in no npmToken found', () => { + const config = {}; + setNpmTokenInNpmrc(config); + expect(config).toMatchObject({}); + }); + + it('adds default npmrc registry if it does not exist', () => { + const config = { npmToken: 'token' }; + setNpmTokenInNpmrc(config); + expect(config).toMatchObject({ + npmrc: '//registry.npmjs.org/:_authToken=token\n', + }); + }); + + it('adds npmToken at end of npmrc string if ${NPM_TOKEN} string not found', () => { + const config = { npmToken: 'token', npmrc: 'something\n' }; + setNpmTokenInNpmrc(config); + expect(config).toMatchObject({ npmrc: 'something\n_authToken=token\n' }); + }); + + it('replaces ${NPM_TOKEN} with npmToken value', () => { + const config = { + npmToken: 'token', + npmrc: 'something_auth=${NPM_TOKEN}\n', + }; + setNpmTokenInNpmrc(config); + expect(config).toMatchObject({ npmrc: 'something_auth=token\n' }); + }); }); }); diff --git a/lib/workers/repository/init/merge.ts b/lib/workers/repository/init/merge.ts index dd3683331f9047..4cf53b1395c3e5 100644 --- a/lib/workers/repository/init/merge.ts +++ b/lib/workers/repository/init/merge.ts @@ -23,6 +23,8 @@ import { readLocalFile } from '../../../util/fs'; import * as hostRules from '../../../util/host-rules'; import * as queue from '../../../util/http/queue'; import * as throttle from '../../../util/http/throttle'; +import { maskToken } from '../../../util/mask'; +import { regEx } from '../../../util/regex'; import { getOnboardingConfig } from '../onboarding/branch/config'; import { getDefaultConfigFileName } from '../onboarding/branch/create'; import { @@ -216,6 +218,7 @@ export async function mergeRenovateConfig( const repository = config.repository!; // Decrypt before resolving in case we need npm authentication for any presets const decryptedConfig = await decryptConfig(migratedConfig, repository); + setNpmTokenInNpmrc(decryptedConfig); // istanbul ignore if if (is.string(decryptedConfig.npmrc)) { logger.debug('Found npmrc in decrypted config - setting'); @@ -237,6 +240,7 @@ export async function mergeRenovateConfig( logger.trace({ config: resolvedConfig }, 'resolved config after migrating'); resolvedConfig = migrationResult.migratedConfig; } + setNpmTokenInNpmrc(resolvedConfig); // istanbul ignore if if (is.string(resolvedConfig.npmrc)) { logger.debug( @@ -278,3 +282,33 @@ export async function mergeRenovateConfig( } return returnConfig; } + +/** needed when using portal secrets for npmToken */ +export function setNpmTokenInNpmrc(config: RenovateConfig): void { + if (!is.string(config.npmToken)) { + return; + } + + const token = config.npmToken; + logger.debug({ npmToken: maskToken(token) }, 'Migrating npmToken to npmrc'); + + if (!is.string(config.npmrc)) { + logger.debug('Adding npmrc to config'); + config.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`; + delete config.npmToken; + return; + } + + if (config.npmrc.includes(`\${NPM_TOKEN}`)) { + logger.debug(`Replacing \${NPM_TOKEN} with npmToken`); + config.npmrc = config.npmrc.replace(regEx(/\${NPM_TOKEN}/g), token); + } else { + logger.debug('Appending _authToken= to end of existing npmrc'); + config.npmrc = config.npmrc.replace( + regEx(/\n?$/), + `\n_authToken=${token}\n`, + ); + } + + delete config.npmToken; +}