Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Jan 30, 2024
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
RGunning marked this conversation as resolved.
Show resolved Hide resolved
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 }), {}),
};
}
Loading