From 16c5b3b86770e3fe34b58fe9028d5c9cd25f8df7 Mon Sep 17 00:00:00 2001 From: Richard Sahrakorpi Date: Tue, 30 Jan 2024 10:21:14 +0200 Subject: [PATCH] feat(npm): fuzzy merge registries in .yarnrc.yml (#26922) Co-authored-by: Rhys Arkins --- .../usage/getting-started/private-packages.md | 14 ++- .../manager/npm/post-update/index.spec.ts | 91 +++++++++++++++++++ lib/modules/manager/npm/post-update/index.ts | 8 +- .../manager/npm/post-update/yarn.spec.ts | 51 +++++++++++ lib/modules/manager/npm/post-update/yarn.ts | 21 +++++ 5 files changed, 179 insertions(+), 6 deletions(-) diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index 4f0761bd1d36bfc..e3090fb0f6d2f3e 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: - //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: ``` ### maven diff --git a/lib/modules/manager/npm/post-update/index.spec.ts b/lib/modules/manager/npm/post-update/index.spec.ts index af67dc833f217c6..1acaa10f7898991 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 => { + 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 => { + 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 => { + 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 20adb0c2658e5ff..005400372fb5a54 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>( 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 f832ca6570ae831..791b648c3c0fe8f 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 92488dd4563fc7d..eae353681990d58 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 }, +>(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 }), {}), + }; +}