diff --git a/lib/manager/npm/post-update/__snapshots__/rules.spec.ts.snap b/lib/manager/npm/post-update/__snapshots__/rules.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..cbba45a52a451a2621259049cbe6bcc18906d0c7 --- /dev/null +++ b/lib/manager/npm/post-update/__snapshots__/rules.spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`manager/npm/post-update/rules processHostRules() returns mixed rules content 1`] = ` +Object { + "additionalNpmrcContent": Array [ + "//registry.npmjs.org:_authToken=token123", + "//registry.other.org:_auth=basictoken123", + "//registry.company.com/:username=user123", + "//registry.company.com/:_password=cGFzczEyMw==", + ], + "additionalYarnRcYml": Object { + "npmRegistries": Object { + "//registry.company.com/": Object { + "npmAuthIdent": "dXNlcjEyMzpwYXNzMTIz", + }, + "//registry.npmjs.org": Object { + "npmAuthToken": "token123", + }, + "//registry.other.org": Object { + "npmAuthIdent": "basictoken123", + }, + }, + }, +} +`; + +exports[`manager/npm/post-update/rules processHostRules() returns rules content 1`] = ` +Object { + "additionalNpmrcContent": Array [ + "//registry.company.com/:username=user123", + "//registry.company.com/:_password=cGFzczEyMw==", + ], + "additionalYarnRcYml": Object { + "npmRegistries": Object { + "//registry.company.com/": Object { + "npmAuthIdent": "dXNlcjEyMzpwYXNzMTIz", + }, + }, + }, +} +`; diff --git a/lib/manager/npm/post-update/index.ts b/lib/manager/npm/post-update/index.ts index d467847f66898a48f403f65948dec1b20d7d8c07..1e45616c596e8bfc5244621f2cc13c83cf93ff26 100644 --- a/lib/manager/npm/post-update/index.ts +++ b/lib/manager/npm/post-update/index.ts @@ -1,5 +1,7 @@ import is from '@sindresorhus/is'; import { parseSyml } from '@yarnpkg/parsers'; +import deepmerge from 'deepmerge'; +import { safeDump, safeLoad } from 'js-yaml'; import upath from 'upath'; import { getAdminConfig } from '../../../config/admin'; import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../../constants/error-messages'; @@ -10,20 +12,23 @@ import { getChildProcessEnv } from '../../../util/exec/env'; import { deleteLocalFile, ensureDir, + getSiblingFileName, getSubDirectory, outputFile, readFile, + readLocalFile, remove, unlink, writeFile, + writeLocalFile, } from '../../../util/fs'; import { branchExists, getFile, getRepoStatus } from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; -import { validateUrl } from '../../../util/url'; import type { PackageFile, PostUpdateConfig, Upgrade } from '../../types'; import * as lerna from './lerna'; import * as npm from './npm'; import * as pnpm from './pnpm'; +import { processHostRules } from './rules'; import type { AdditionalPackageFiles, ArtifactError, @@ -427,25 +432,7 @@ export async function getAdditionalFiles( await writeExistingFiles(config, packageFiles); await writeUpdatedPackageFiles(config); - // Determine the additional npmrc content to add based on host rules - const additionalNpmrcContent = []; - const npmHostRules = hostRules.findAll({ - hostType: 'npm', - }); - for (const hostRule of npmHostRules) { - if (hostRule.resolvedHost) { - let uri = hostRule.matchHost; - uri = validateUrl(uri) ? uri.replace(/^https?:/, '') : `//${uri}/`; - if (hostRule.token) { - const key = hostRule.authType === 'Basic' ? '_auth' : '_authToken'; - additionalNpmrcContent.push(`${uri}:${key}=${hostRule.token}`); - } else if (is.string(hostRule.username) && is.string(hostRule.password)) { - const password = Buffer.from(hostRule.password).toString('base64'); - additionalNpmrcContent.push(`${uri}:username=${hostRule.username}`); - additionalNpmrcContent.push(`${uri}:_password=${password}`); - } - } - } + const { additionalNpmrcContent, additionalYarnRcYml } = processHostRules(); const env = getChildProcessEnv([ 'NPM_CONFIG_CACHE', @@ -549,6 +536,25 @@ export async function getAdditionalFiles( npmrcContent, additionalNpmrcContent ); + let yarnRcYmlFilename: string; + let existingYarnrcYmlContent: string; + if (additionalYarnRcYml) { + yarnRcYmlFilename = getSiblingFileName(yarnLock, '.yarnrc.yml'); + existingYarnrcYmlContent = await readLocalFile(yarnRcYmlFilename, 'utf8'); + if (existingYarnrcYmlContent) { + try { + const existingYarnrRcYml = safeLoad(existingYarnrcYmlContent); + const updatedYarnYrcYml = deepmerge( + existingYarnrRcYml, + additionalYarnRcYml + ); + await writeLocalFile(yarnRcYmlFilename, safeDump(updatedYarnYrcYml)); + logger.debug('Added authentication to .yarnrc.yml'); + } catch (err) { + logger.warn({ err }, 'Error appending .yarnrc.yml content'); + } + } + } logger.debug(`Generating yarn.lock for ${lockFileDir}`); const lockFileName = upath.join(lockFileDir, 'yarn.lock'); const upgrades = config.upgrades.filter( @@ -605,6 +611,9 @@ export async function getAdditionalFiles( } } await resetNpmrcContent(fullLockFileDir, npmrcContent); + if (existingYarnrcYmlContent) { + await writeLocalFile(yarnRcYmlFilename, existingYarnrcYmlContent); + } } for (const pnpmShrinkwrap of dirs.pnpmShrinkwrapDirs) { diff --git a/lib/manager/npm/post-update/rules.spec.ts b/lib/manager/npm/post-update/rules.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..456f839969216d83cf5a054f0646e16faf1feaea --- /dev/null +++ b/lib/manager/npm/post-update/rules.spec.ts @@ -0,0 +1,53 @@ +import { getName } from '../../../../test/util'; +import * as hostRules from '../../../util/host-rules'; +import { processHostRules } from './rules'; + +describe(getName(), () => { + describe('processHostRules()', () => { + beforeEach(() => { + hostRules.clear(); + }); + it('returns empty if no rules', () => { + const res = processHostRules(); + expect(res.additionalNpmrcContent).toHaveLength(0); + expect(res.additionalYarnRcYml).toBeUndefined(); + }); + it('returns empty if no resolvedHost', () => { + hostRules.add({ hostType: 'npm', token: 'abc123' }); + const res = processHostRules(); + expect(res.additionalNpmrcContent).toHaveLength(0); + expect(res.additionalYarnRcYml).toBeUndefined(); + }); + it('returns rules content', () => { + hostRules.add({ + hostType: 'npm', + matchHost: 'registry.company.com', + username: 'user123', + password: 'pass123', + }); + const res = processHostRules(); + expect(res).toMatchSnapshot(); + }); + it('returns mixed rules content', () => { + hostRules.add({ + hostType: 'npm', + matchHost: 'https://registry.npmjs.org', + token: 'token123', + }); + hostRules.add({ + hostType: 'npm', + matchHost: 'https://registry.other.org', + authType: 'Basic', + token: 'basictoken123', + }); + hostRules.add({ + hostType: 'npm', + matchHost: 'registry.company.com', + username: 'user123', + password: 'pass123', + }); + const res = processHostRules(); + expect(res).toMatchSnapshot(); + }); + }); +}); diff --git a/lib/manager/npm/post-update/rules.ts b/lib/manager/npm/post-update/rules.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf83c6c9b49228facf343d8ecf950594db885a84 --- /dev/null +++ b/lib/manager/npm/post-update/rules.ts @@ -0,0 +1,49 @@ +import is from '@sindresorhus/is'; +import * as hostRules from '../../../util/host-rules'; +import { validateUrl } from '../../../util/url'; + +export interface HostRulesResult { + additionalNpmrcContent: string[]; + additionalYarnRcYml?: any; +} + +export function processHostRules(): HostRulesResult { + let additionalYarnRcYml: any; + + // Determine the additional npmrc content to add based on host rules + const additionalNpmrcContent = []; + const npmHostRules = hostRules.findAll({ + hostType: 'npm', + }); + for (const hostRule of npmHostRules) { + if (hostRule.resolvedHost) { + let uri = hostRule.matchHost; + uri = validateUrl(uri) ? uri.replace(/^https?:/, '') : `//${uri}/`; + if (hostRule.token) { + const key = hostRule.authType === 'Basic' ? '_auth' : '_authToken'; + additionalNpmrcContent.push(`${uri}:${key}=${hostRule.token}`); + additionalYarnRcYml ||= { npmRegistries: {} }; + if (hostRule.authType === 'Basic') { + additionalYarnRcYml.npmRegistries[uri] = { + npmAuthIdent: hostRule.token, + }; + } else { + additionalYarnRcYml.npmRegistries[uri] = { + npmAuthToken: hostRule.token, + }; + } + } else if (is.string(hostRule.username) && is.string(hostRule.password)) { + const password = Buffer.from(hostRule.password).toString('base64'); + additionalNpmrcContent.push(`${uri}:username=${hostRule.username}`); + additionalNpmrcContent.push(`${uri}:_password=${password}`); + additionalYarnRcYml ||= { npmRegistries: {} }; + additionalYarnRcYml.npmRegistries[uri] = { + npmAuthIdent: Buffer.from( + `${hostRule.username}:${hostRule.password}` + ).toString('base64'), + }; + } + } + } + return { additionalNpmrcContent, additionalYarnRcYml }; +}