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
235 changes: 235 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,10 +7,12 @@ 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 {
determineLockFileDirs,
fuzzyMatchAdditionalYarnrcYml,
getAdditionalFiles,
updateYarnBinary,
writeExistingFiles,
Expand Down Expand Up @@ -393,11 +395,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 +684,233 @@ 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 set 1 registry when the registries are the same except for the protocol', async () => {
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 set 1 registry when the registries are the same except for a trailing slash in the existingYarnrRcYml', async () => {
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 set 1 registry when the registries are the same except for a trailing slash in the additionalYarnRcYml', async () => {
spyProcessHostRules.mockReturnValue({
additionalNpmrcContent: [],
additionalYarnRcYml: {
npmRegistries: {
'//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 set 2 registry when the registries don't match", async () => {
fs.readLocalFile.mockImplementation((f): Promise<any> => {
if (f === '.yarnrc.yml') {
return Promise.resolve(
'npmRegistries:\n' +
' https://some-other-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://some-other-private-registry:\n' +
' npmAlwaysAuth: true\n' +
' //my-private-registry:\n' +
' npmAuthToken: xxxxxx\n',
);
});

it('should set 1 registry when the existingYarnrRcYml has no registries set', 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);
});

spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' });
await getAdditionalFiles(
{
...updateConfig,
updateLockFiles: true,
reuseExistingBranch: true,
},
additionalFiles,
);
expect(fs.writeLocalFile).toHaveBeenCalledWith(
'.yarnrc.yml',
'yarnPath: .yarn/releases/yarn-3.0.1.cjs\n' +
'a: b\n' +
'npmRegistries:\n' +
' //my-private-registry:\n' +
' npmAuthToken: xxxxxx\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(
fuzzyMatchAdditionalYarnrcYml(
{
npmRegistries: additionalRegistry?.reduce(
(acc, cur) => ({
...acc,
[cur]: { npmAuthToken: 'xxxxxx' },
}),
{},
),
},
{
npmRegistries: existingRegistry?.reduce(
(acc, cur) => ({
...acc,
[cur]: { npmAuthToken: 'xxxxxx' },
}),
{},
),
},
).npmRegistries,
).toContainAllKeys(expectedRegistry);
},
);
});
});
28 changes: 25 additions & 3 deletions lib/modules/manager/npm/post-update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ async function resetNpmrcContent(
}
}

export function fuzzyMatchAdditionalYarnrcYml<
RGunning marked this conversation as resolved.
Show resolved Hide resolved
T extends { npmRegistries?: Record<string, unknown> },
>(additionalYarnRcYml: T, existingYarnrRcYml: T): T {
return {
RGunning marked this conversation as resolved.
Show resolved Hide resolved
...additionalYarnRcYml,
npmRegistries: Object.entries(additionalYarnRcYml.npmRegistries ?? {})
.map(([k, v]) => {
const key =
Object.keys(existingYarnrRcYml.npmRegistries ?? {}).find(
// match without trailing slashes
(x) => x.replace(/\/$/, '').endsWith(k.replace(/\/$/, '')),
) ?? k;
RGunning marked this conversation as resolved.
Show resolved Hide resolved
return { [key]: v };
})
.reduce((acc, cur) => ({ ...acc, ...cur }), {}),
};
}

// istanbul ignore next
async function updateYarnOffline(
lockFileDir: string,
Expand Down Expand Up @@ -563,7 +581,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,13 +590,18 @@ export async function getAdditionalFiles(
const existingYarnrRcYml = parseSingleYaml<Record<string, unknown>>(
existingYarnrcYmlContent,
);

const updatedYarnYrcYml = deepmerge(
existingYarnrRcYml,
additionalYarnRcYml,
fuzzyMatchAdditionalYarnrcYml(
additionalYarnRcYml,
existingYarnrRcYml,
),
);

await writeLocalFile(yarnRcYmlFilename, dump(updatedYarnYrcYml));
logger.debug('Added authentication to .yarnrc.yml');
} catch (err) {
} catch (err) /* istanbul ignore next */ {
RGunning marked this conversation as resolved.
Show resolved Hide resolved
logger.warn({ err }, 'Error appending .yarnrc.yml content');
}
}
Expand Down