mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-28 00:08:24 +00:00
4220c0fe36
* refactor(manager): share the .npmrc host-rule wrapper between bun, deno and npm Adds `withNpmrcHostRules()` to the npm manager utils: it injects the host-rule lines into a directory's `.npmrc`, runs the callback, and restores the original `.npmrc` from a `finally` block. bun, deno, the corepack `updateArtifacts()` and the three lock file loops in `post-update/index.ts` now share it instead of repeating getNpmrcContent / updateNpmrcContent / resetNpmrcContent. Adds `readUpdatedBinaryLockFile()` to the shared manager utils for the `Buffer.compare` plus file-addition block that bun and deno both carried. Behaviour changes: - bun and deno now restore `.npmrc` when `exec()` throws. The reset previously ran only after a successful install, so a failed lock file update left the injected registry credentials in the working tree. Covered by "restores .npmrc when the install fails" in `bun/artifacts.spec.ts` and `deno/artifacts.spec.ts`. - The npm, yarn and pnpm loops in `post-update/index.ts` now restore `.npmrc` when the loop body throws an `ExternalHostError`, for the same reason. - deno reads `config.isLockFileMaintenance` instead of deriving the flag from `config.updateType === 'lockFileMaintenance'`. Grouped branches overwrite `updateType` while `isLockFileMaintenance` is set explicitly, so the flag is the reliable source. The deno specs were updated to set it. * refactor(manager): let updateLockFile() compare binary lock files A Buffer as the existing content makes the skeleton read, compare and return the new lock file as bytes, so bun and deno use it instead of a separate readUpdatedBinaryLockFile() helper. * refactor(manager/devbox): update the lock file through updateLockFile() devbox kept the same Buffer compare as bun and deno; the trace log before returning the lock file goes away with it. * fix(manager/devbox): type the command list explicitly The list is now only read inside the run() closure, where TypeScript cannot resolve an evolving array type, so tsc reported an implicit any.
456 lines
13 KiB
TypeScript
456 lines
13 KiB
TypeScript
import { fs } from '~test/util.ts';
|
|
import * as hostRules from '../../util/host-rules.ts';
|
|
import { GitRefsDatasource } from '../datasource/git-refs/index.ts';
|
|
import { GitTagsDatasource } from '../datasource/git-tags/index.ts';
|
|
import { GithubTagsDatasource } from '../datasource/github-tags/index.ts';
|
|
import { GitlabTagsDatasource } from '../datasource/gitlab-tags/index.ts';
|
|
import { type PackageDependency } from './types.ts';
|
|
import {
|
|
applyGitSource,
|
|
artifactError,
|
|
artifactErrorMessageFromExecError,
|
|
artifactErrorResult,
|
|
fileAddition,
|
|
fileChangesToArtifactResults,
|
|
resolveToolConstraint,
|
|
updateLockFile,
|
|
} from './util.ts';
|
|
|
|
vi.mock('../../util/fs/index.ts');
|
|
|
|
describe('modules/manager/util', () => {
|
|
beforeEach(() => {
|
|
hostRules.clear();
|
|
});
|
|
|
|
it('applies GitHub source for tag', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://github.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GithubTagsDatasource.id,
|
|
registryUrls: ['https://github.com'],
|
|
packageName: 'foo/bar',
|
|
currentValue: tag,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies GitLab source for tag', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://gitlab.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GitlabTagsDatasource.id,
|
|
registryUrls: ['https://gitlab.com'],
|
|
packageName: 'foo/bar',
|
|
currentValue: tag,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies other git source for tag', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://a-git-source.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GitTagsDatasource.id,
|
|
packageName: git,
|
|
currentValue: tag,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies git source with subdomain', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://git.example.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
hostRules.add({
|
|
hostType: 'github',
|
|
matchHost: 'git.example.com',
|
|
});
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GithubTagsDatasource.id,
|
|
packageName: 'foo/bar',
|
|
currentValue: tag,
|
|
registryUrls: ['https://git.example.com'],
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies GitHub source for tag with SSH URL', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'ssh://git@github.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GithubTagsDatasource.id,
|
|
registryUrls: ['https://github.com'],
|
|
packageName: 'foo/bar',
|
|
currentValue: tag,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies GitLab source for tag with SSH URL', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'ssh://git@gitlab.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GitlabTagsDatasource.id,
|
|
registryUrls: ['https://gitlab.com'],
|
|
packageName: 'foo/bar',
|
|
currentValue: tag,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies GitHub source for tag with HTTPS URL', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://github.com/foo/bar';
|
|
const tag = 'v1.2.3';
|
|
|
|
applyGitSource(dependency, git, undefined, tag, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GithubTagsDatasource.id,
|
|
registryUrls: ['https://github.com'],
|
|
packageName: 'foo/bar',
|
|
currentValue: tag,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('applies git source for rev', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://github.com/foo/bar';
|
|
const rev = 'abc1234';
|
|
|
|
applyGitSource(dependency, git, rev, undefined, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GitRefsDatasource.id,
|
|
packageName: git,
|
|
currentDigest: rev,
|
|
replaceString: rev,
|
|
skipReason: undefined,
|
|
});
|
|
});
|
|
|
|
it('skips git source for branch', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://github.com/foo/bar';
|
|
const branch = 'main';
|
|
|
|
applyGitSource(dependency, git, undefined, undefined, branch);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GitRefsDatasource.id,
|
|
packageName: git,
|
|
currentValue: branch,
|
|
skipReason: 'git-dependency',
|
|
});
|
|
});
|
|
|
|
it('skips git source for git only', () => {
|
|
const dependency: PackageDependency = {};
|
|
const git = 'https://github.com/foo/bar';
|
|
|
|
applyGitSource(dependency, git, undefined, undefined, undefined);
|
|
|
|
expect(dependency).toStrictEqual({
|
|
datasource: GitRefsDatasource.id,
|
|
packageName: git,
|
|
currentValue: undefined,
|
|
skipReason: 'unspecified-version',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('modules/manager/util', () => {
|
|
it('returns stderr when present', () => {
|
|
const message = artifactErrorMessageFromExecError(
|
|
{ stderr: 'some error', stdout: 'some output' },
|
|
'fallback message',
|
|
);
|
|
|
|
expect(message).toBe('some error');
|
|
});
|
|
|
|
it('returns stdout when stderr is empty', () => {
|
|
const message = artifactErrorMessageFromExecError(
|
|
{ stderr: '', stdout: 'some output' },
|
|
'fallback message',
|
|
);
|
|
|
|
expect(message).toBe('some output');
|
|
});
|
|
|
|
it('returns stdout when stderr is only whitespace', () => {
|
|
const message = artifactErrorMessageFromExecError(
|
|
{ stderr: ' ', stdout: 'some output' },
|
|
'fallback message',
|
|
);
|
|
|
|
expect(message).toBe('some output');
|
|
});
|
|
|
|
it('returns stdout when stderr is undefined', () => {
|
|
const message = artifactErrorMessageFromExecError(
|
|
{ stdout: 'some output' },
|
|
'fallback message',
|
|
);
|
|
|
|
expect(message).toBe('some output');
|
|
});
|
|
|
|
it('returns fallback message when neither stderr nor stdout are present', () => {
|
|
const message = artifactErrorMessageFromExecError({}, 'fallback message');
|
|
|
|
expect(message).toBe('fallback message');
|
|
});
|
|
|
|
it('returns fallback message when stderr and stdout are only whitespace', () => {
|
|
const message = artifactErrorMessageFromExecError(
|
|
{ stderr: ' ', stdout: ' ' },
|
|
'fallback message',
|
|
);
|
|
|
|
expect(message).toBe('fallback message');
|
|
});
|
|
|
|
it('wraps file changes into artifact results', () => {
|
|
expect(
|
|
fileChangesToArtifactResults([
|
|
{ type: 'addition', path: 'foo', contents: 'bar' },
|
|
{ type: 'deletion', path: 'baz' },
|
|
]),
|
|
).toEqual([
|
|
{ file: { type: 'addition', path: 'foo', contents: 'bar' } },
|
|
{ file: { type: 'deletion', path: 'baz' } },
|
|
]);
|
|
});
|
|
|
|
describe('resolveToolConstraint()', () => {
|
|
it('prefers the user configured constraint', async () => {
|
|
const constraint = await resolveToolConstraint(
|
|
{
|
|
constraints: { python: '==3.12' },
|
|
extractedConstraints: { python: '==3.10' },
|
|
},
|
|
'python',
|
|
() => '==3.11',
|
|
);
|
|
|
|
expect(constraint).toBe('==3.12');
|
|
});
|
|
|
|
it('prefers the derived constraint over the extracted one', async () => {
|
|
const constraint = await resolveToolConstraint(
|
|
{ extractedConstraints: { python: '==3.10' } },
|
|
'python',
|
|
() => Promise.resolve('==3.11'),
|
|
);
|
|
|
|
expect(constraint).toBe('==3.11');
|
|
});
|
|
|
|
it('falls back to the extracted constraint', async () => {
|
|
const constraint = await resolveToolConstraint(
|
|
{ extractedConstraints: { python: '==3.10' } },
|
|
'python',
|
|
() => null,
|
|
);
|
|
|
|
expect(constraint).toBe('==3.10');
|
|
});
|
|
|
|
it('returns the extracted constraint when nothing can be derived', async () => {
|
|
const constraint = await resolveToolConstraint(
|
|
{ extractedConstraints: { python: '==3.10' } },
|
|
'python',
|
|
);
|
|
|
|
expect(constraint).toBe('==3.10');
|
|
});
|
|
|
|
it('returns undefined when no constraint is known', async () => {
|
|
const constraint = await resolveToolConstraint({}, 'python', () => null);
|
|
|
|
expect(constraint).toBeUndefined();
|
|
});
|
|
|
|
it('treats an empty string as not set', async () => {
|
|
const constraint = await resolveToolConstraint(
|
|
{
|
|
constraints: { python: '' },
|
|
extractedConstraints: { python: '==3.10' },
|
|
},
|
|
'python',
|
|
() => '',
|
|
);
|
|
|
|
expect(constraint).toBe('==3.10');
|
|
});
|
|
|
|
it('accepts the null constraints of a post-update config', async () => {
|
|
const constraint = await resolveToolConstraint(
|
|
{ constraints: null, extractedConstraints: null },
|
|
'node',
|
|
);
|
|
|
|
expect(constraint).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
it('builds an addition result', () => {
|
|
expect(fileAddition('foo.lock', 'new')).toEqual({
|
|
file: { type: 'addition', path: 'foo.lock', contents: 'new' },
|
|
});
|
|
});
|
|
|
|
it('builds an artifact error result', () => {
|
|
expect(artifactError('foo.lock', 'boom')).toEqual({
|
|
artifactError: { fileName: 'foo.lock', stderr: 'boom' },
|
|
});
|
|
});
|
|
|
|
it('builds an artifact error result from an error', () => {
|
|
expect(
|
|
artifactErrorResult('foo.lock', Object.assign(new Error('msg'), {})),
|
|
).toEqual([{ artifactError: { fileName: 'foo.lock', stderr: 'msg' } }]);
|
|
|
|
expect(
|
|
artifactErrorResult(
|
|
'foo.lock',
|
|
Object.assign(new Error('msg'), { stderr: 'from stderr' }),
|
|
),
|
|
).toEqual([
|
|
{ artifactError: { fileName: 'foo.lock', stderr: 'from stderr' } },
|
|
]);
|
|
});
|
|
|
|
describe('updateLockFile', () => {
|
|
it('writes the package file, deletes the lock file and returns the update', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('new content');
|
|
const run = vi.fn().mockResolvedValue(undefined);
|
|
|
|
const res = await updateLockFile({
|
|
lockFileName: 'foo.lock',
|
|
existingLockFileContent: 'old content',
|
|
packageFile: { path: 'foo.json', contents: 'new package file' },
|
|
deleteLockFile: true,
|
|
run,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
type: 'addition',
|
|
path: 'foo.lock',
|
|
contents: 'new content',
|
|
},
|
|
},
|
|
]);
|
|
expect(fs.writeLocalFile).toHaveBeenCalledWith(
|
|
'foo.json',
|
|
'new package file',
|
|
);
|
|
expect(fs.deleteLocalFile).toHaveBeenCalledWith('foo.lock');
|
|
expect(fs.readLocalFile).toHaveBeenCalledWith('foo.lock', 'utf8');
|
|
expect(run).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('keeps the package and lock file when not asked to touch them', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('new content');
|
|
|
|
await updateLockFile({
|
|
lockFileName: 'foo.lock',
|
|
existingLockFileContent: 'old content',
|
|
run: () => Promise.resolve(),
|
|
});
|
|
|
|
expect(fs.writeLocalFile).not.toHaveBeenCalled();
|
|
expect(fs.deleteLocalFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns null when the lock file is gone', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce(null);
|
|
|
|
await expect(
|
|
updateLockFile({
|
|
lockFileName: 'foo.lock',
|
|
existingLockFileContent: 'old content',
|
|
run: () => Promise.resolve(),
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null when the lock file is unchanged', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('old content');
|
|
|
|
await expect(
|
|
updateLockFile({
|
|
lockFileName: 'foo.lock',
|
|
existingLockFileContent: 'old content',
|
|
run: () => Promise.resolve(),
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('reads and returns the lock file as bytes when given a Buffer', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce(
|
|
Buffer.from('new content') as never,
|
|
);
|
|
|
|
const res = await updateLockFile({
|
|
lockFileName: 'foo.lockb',
|
|
existingLockFileContent: Buffer.from('old content'),
|
|
run: () => Promise.resolve(),
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
type: 'addition',
|
|
path: 'foo.lockb',
|
|
contents: Buffer.from('new content'),
|
|
},
|
|
},
|
|
]);
|
|
expect(fs.readLocalFile).toHaveBeenCalledWith('foo.lockb');
|
|
});
|
|
|
|
it('returns null when a binary lock file is unchanged', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce(
|
|
Buffer.from('old content') as never,
|
|
);
|
|
|
|
await expect(
|
|
updateLockFile({
|
|
lockFileName: 'foo.lockb',
|
|
existingLockFileContent: Buffer.from('old content'),
|
|
run: () => Promise.resolve(),
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
});
|
|
});
|