feat(npm): fuzzy merge registries in .yarnrc.yml (#26922)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
Richard Sahrakorpi 2024-01-30 10:21:14 +02:00 committed by GitHub
parent 88000a4f9b
commit 88daaf5a89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 179 additions and 6 deletions

View file

@ -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

View file

@ -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',
);
});
});
});
});

View file

@ -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) {

View file

@ -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);
},
);
});
});

View file

@ -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 }), {}),
};
}