diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index 4f0761bd1d36bfcbdc07bab4a4bce8046b17e64f..e3090fb0f6d2f3e31a03d5d0b04d7080c847324c 100644 --- a/docs/usage/getting-started/private-packages.md +++ b/docs/usage/getting-started/private-packages.md @@ -382,14 +382,20 @@ For example, the Renovate configuration: will update `.yarnrc.yml` as following: +If no registry currently set + ```yaml npmRegistries: //npm.pkg.github.com/: npmAuthToken: <Decrypted PAT Token> - //npm.pkg.github.com: - # this will not be overwritten and may conflict - https://npm.pkg.github.com/: - # this will not be overwritten and may conflict +``` + +If current registry key has protocol set: + +```yaml +npmRegistries: + https://npm.pkg.github.com: + npmAuthToken: <Decrypted PAT Token> ``` ### maven diff --git a/lib/modules/manager/npm/post-update/index.spec.ts b/lib/modules/manager/npm/post-update/index.spec.ts index af67dc833f217c60a6879f8b56e5cd283f93a3b6..1acaa10f78989916a1aaab9454f1770704161853 100644 --- a/lib/modules/manager/npm/post-update/index.spec.ts +++ b/lib/modules/manager/npm/post-update/index.spec.ts @@ -7,6 +7,7 @@ import type { FileChange } from '../../../../util/git/types'; import type { PostUpdateConfig } from '../../types'; import * as npm from './npm'; import * as pnpm from './pnpm'; +import * as rules from './rules'; import type { AdditionalPackageFiles } from './types'; import * as yarn from './yarn'; import { @@ -393,11 +394,16 @@ describe('modules/manager/npm/post-update/index', () => { const spyNpm = jest.spyOn(npm, 'generateLockFile'); const spyYarn = jest.spyOn(yarn, 'generateLockFile'); const spyPnpm = jest.spyOn(pnpm, 'generateLockFile'); + const spyProcessHostRules = jest.spyOn(rules, 'processHostRules'); beforeEach(() => { spyNpm.mockResolvedValue({}); spyPnpm.mockResolvedValue({}); spyYarn.mockResolvedValue({}); + spyProcessHostRules.mockReturnValue({ + additionalNpmrcContent: [], + additionalYarnRcYml: undefined, + }); }); it('works', async () => { @@ -677,5 +683,90 @@ describe('modules/manager/npm/post-update/index', () => { updatedArtifacts: [], }); }); + + describe('should fuzzy merge yarn npmRegistries', () => { + beforeEach(() => { + spyProcessHostRules.mockReturnValue({ + additionalNpmrcContent: [], + additionalYarnRcYml: { + npmRegistries: { + '//my-private-registry': { + npmAuthToken: 'xxxxxx', + }, + }, + }, + }); + fs.getSiblingFileName.mockReturnValue('.yarnrc.yml'); + }); + + it('should fuzzy merge the yarnrc Files', async () => { + (yarn.fuzzyMatchAdditionalYarnrcYml as jest.Mock).mockReturnValue({ + npmRegistries: { + 'https://my-private-registry': { npmAuthToken: 'xxxxxx' }, + }, + }); + fs.readLocalFile.mockImplementation((f): Promise<any> => { + if (f === '.yarnrc.yml') { + return Promise.resolve( + 'npmRegistries:\n' + + ' https://my-private-registry:\n' + + ' npmAlwaysAuth: true\n', + ); + } + return Promise.resolve(null); + }); + + spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' }); + await getAdditionalFiles( + { + ...updateConfig, + updateLockFiles: true, + reuseExistingBranch: true, + }, + additionalFiles, + ); + expect(fs.writeLocalFile).toHaveBeenCalledWith( + '.yarnrc.yml', + 'npmRegistries:\n' + + ' https://my-private-registry:\n' + + ' npmAlwaysAuth: true\n' + + ' npmAuthToken: xxxxxx\n', + ); + }); + + it('should warn if there is an error writing the yarnrc.yml', async () => { + fs.readLocalFile.mockImplementation((f): Promise<any> => { + if (f === '.yarnrc.yml') { + return Promise.resolve( + `yarnPath: .yarn/releases/yarn-3.0.1.cjs\na: b\n`, + ); + } + return Promise.resolve(null); + }); + + fs.writeLocalFile.mockImplementation((f): Promise<any> => { + if (f === '.yarnrc.yml') { + throw new Error(); + } + return Promise.resolve(null); + }); + + spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' }); + + await getAdditionalFiles( + { + ...updateConfig, + updateLockFiles: true, + reuseExistingBranch: true, + }, + additionalFiles, + ).catch(() => {}); + + expect(logger.logger.warn).toHaveBeenCalledWith( + expect.anything(), + 'Error appending .yarnrc.yml content', + ); + }); + }); }); }); diff --git a/lib/modules/manager/npm/post-update/index.ts b/lib/modules/manager/npm/post-update/index.ts index 20adb0c2658e5ffc9b8281240752f2701a7b9981..005400372fb5a54061ce6755142e1e89ae816058 100644 --- a/lib/modules/manager/npm/post-update/index.ts +++ b/lib/modules/manager/npm/post-update/index.ts @@ -563,7 +563,6 @@ export async function getAdditionalFiles( await updateNpmrcContent(lockFileDir, npmrcContent, additionalNpmrcContent); let yarnRcYmlFilename: string | undefined; let existingYarnrcYmlContent: string | undefined | null; - // istanbul ignore if: needs test if (additionalYarnRcYml) { yarnRcYmlFilename = getSiblingFileName(yarnLock, '.yarnrc.yml'); existingYarnrcYmlContent = await readLocalFile(yarnRcYmlFilename, 'utf8'); @@ -573,10 +572,15 @@ export async function getAdditionalFiles( const existingYarnrRcYml = parseSingleYaml<Record<string, unknown>>( existingYarnrcYmlContent, ); + const updatedYarnYrcYml = deepmerge( existingYarnrRcYml, - additionalYarnRcYml, + yarn.fuzzyMatchAdditionalYarnrcYml( + additionalYarnRcYml, + existingYarnrRcYml, + ), ); + await writeLocalFile(yarnRcYmlFilename, dump(updatedYarnYrcYml)); logger.debug('Added authentication to .yarnrc.yml'); } catch (err) { diff --git a/lib/modules/manager/npm/post-update/yarn.spec.ts b/lib/modules/manager/npm/post-update/yarn.spec.ts index f832ca6570ae831d2d047daa78401be9510cb5e3..791b648c3c0fe8f76a4c6825db42c11c64ad6f26 100644 --- a/lib/modules/manager/npm/post-update/yarn.spec.ts +++ b/lib/modules/manager/npm/post-update/yarn.spec.ts @@ -726,4 +726,55 @@ describe('modules/manager/npm/post-update/yarn', () => { expect(Fixtures.toJSON()['/tmp/renovate/.yarnrc']).toBe('\n\n'); }); }); + + describe('fuzzyMatchAdditionalYarnrcYml()', () => { + it.each` + additionalRegistry | existingRegistry | expectedRegistry + ${['//my-private-registry']} | ${['//my-private-registry']} | ${['//my-private-registry']} + ${[]} | ${['//my-private-registry']} | ${[]} + ${[]} | ${[]} | ${[]} + ${null} | ${null} | ${[]} + ${['//my-private-registry']} | ${[]} | ${['//my-private-registry']} + ${['//my-private-registry']} | ${['https://my-private-registry']} | ${['https://my-private-registry']} + ${['//my-private-registry']} | ${['http://my-private-registry']} | ${['http://my-private-registry']} + ${['//my-private-registry']} | ${['http://my-private-registry/']} | ${['http://my-private-registry/']} + ${['//my-private-registry']} | ${['https://my-private-registry/']} | ${['https://my-private-registry/']} + ${['//my-private-registry']} | ${['//my-private-registry/']} | ${['//my-private-registry/']} + ${['//my-private-registry/']} | ${['//my-private-registry/']} | ${['//my-private-registry/']} + ${['//my-private-registry/']} | ${['//my-private-registry']} | ${['//my-private-registry']} + `( + 'should return $expectedRegistry when parsing $additionalRegistry against local $existingRegistry', + ({ + additionalRegistry, + existingRegistry, + expectedRegistry, + }: Record< + 'additionalRegistry' | 'existingRegistry' | 'expectedRegistry', + string[] + >) => { + expect( + yarnHelper.fuzzyMatchAdditionalYarnrcYml( + { + npmRegistries: additionalRegistry?.reduce( + (acc, cur) => ({ + ...acc, + [cur]: { npmAuthToken: 'xxxxxx' }, + }), + {}, + ), + }, + { + npmRegistries: existingRegistry?.reduce( + (acc, cur) => ({ + ...acc, + [cur]: { npmAuthToken: 'xxxxxx' }, + }), + {}, + ), + }, + ).npmRegistries, + ).toContainAllKeys(expectedRegistry); + }, + ); + }); }); diff --git a/lib/modules/manager/npm/post-update/yarn.ts b/lib/modules/manager/npm/post-update/yarn.ts index 92488dd4563fc7d1871757dc523d25e6d5b27908..eae353681990d584a793ea9fe19740b703098d6c 100644 --- a/lib/modules/manager/npm/post-update/yarn.ts +++ b/lib/modules/manager/npm/post-update/yarn.ts @@ -315,3 +315,24 @@ export async function generateLockFile( } return { lockFile }; } + +export function fuzzyMatchAdditionalYarnrcYml< + T extends { npmRegistries?: Record<string, unknown> }, +>(additionalYarnRcYml: T, existingYarnrRcYml: T): T { + const keys = new Map( + Object.keys(existingYarnrRcYml.npmRegistries ?? {}).map((x) => [ + x.replace(/\/$/, '').replace(/^https?:/, ''), + x, + ]), + ); + + return { + ...additionalYarnRcYml, + npmRegistries: Object.entries(additionalYarnRcYml.npmRegistries ?? {}) + .map(([k, v]) => { + const key = keys.get(k.replace(/\/$/, '')) ?? k; + return { [key]: v }; + }) + .reduce((acc, cur) => ({ ...acc, ...cur }), {}), + }; +}