Skip to content

Commit

Permalink
feat(npm): fuzzy merge registries in .yarnrc.yml (renovatebot#26922)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <[email protected]>
  • Loading branch information
2 people authored and Ronald van Butselaar committed Feb 2, 2024
1 parent acb4c44 commit 16c5b3b
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 6 deletions.
14 changes: 10 additions & 4 deletions docs/usage/getting-started/private-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions lib/modules/manager/npm/post-update/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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',
);
});
});
});
});
8 changes: 6 additions & 2 deletions lib/modules/manager/npm/post-update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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) {
Expand Down
51 changes: 51 additions & 0 deletions lib/modules/manager/npm/post-update/yarn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
);
});
});
21 changes: 21 additions & 0 deletions lib/modules/manager/npm/post-update/yarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }), {}),
};
}

0 comments on commit 16c5b3b

Please sign in to comment.