mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 06:56:24 +00:00
feat(npm): append corepack hashes when updating package managers (#30552)
This commit is contained in:
parent
463f8f2ded
commit
c2821134f1
4 changed files with 329 additions and 0 deletions
230
lib/modules/manager/npm/artifacts.spec.ts
Normal file
230
lib/modules/manager/npm/artifacts.spec.ts
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import { join } from 'upath';
|
||||||
|
import {
|
||||||
|
envMock,
|
||||||
|
mockExecAll,
|
||||||
|
mockExecSequence,
|
||||||
|
} from '../../../../test/exec-util';
|
||||||
|
import { env, fs } from '../../../../test/util';
|
||||||
|
import { GlobalConfig } from '../../../config/global';
|
||||||
|
import type { RepoGlobalConfig } from '../../../config/types';
|
||||||
|
import * as docker from '../../../util/exec/docker';
|
||||||
|
import type { UpdateArtifactsConfig, Upgrade } from '../types';
|
||||||
|
import { updateArtifacts } from '.';
|
||||||
|
|
||||||
|
jest.mock('../../../util/exec/env');
|
||||||
|
jest.mock('../../../util/fs');
|
||||||
|
|
||||||
|
const adminConfig: RepoGlobalConfig = {
|
||||||
|
// `join` fixes Windows CI
|
||||||
|
localDir: join('/tmp/github/some/repo'),
|
||||||
|
cacheDir: join('/tmp/renovate/cache'),
|
||||||
|
containerbaseDir: join('/tmp/renovate/cache/containerbase'),
|
||||||
|
};
|
||||||
|
const dockerAdminConfig = {
|
||||||
|
...adminConfig,
|
||||||
|
binarySource: 'docker',
|
||||||
|
dockerSidecarImage: 'ghcr.io/containerbase/sidecar',
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env.CONTAINERBASE = 'true';
|
||||||
|
|
||||||
|
const config: UpdateArtifactsConfig = {};
|
||||||
|
const validDepUpdate = {
|
||||||
|
depName: 'pnpm',
|
||||||
|
depType: 'packageManager',
|
||||||
|
currentValue:
|
||||||
|
'8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589',
|
||||||
|
newVersion: '8.15.6',
|
||||||
|
} satisfies Upgrade<Record<string, unknown>>;
|
||||||
|
|
||||||
|
describe('modules/manager/npm/artifacts', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
env.getChildProcessEnv.mockReturnValue({
|
||||||
|
...envMock.basic,
|
||||||
|
LANG: 'en_US.UTF-8',
|
||||||
|
LC_ALL: 'en_US',
|
||||||
|
});
|
||||||
|
GlobalConfig.set(adminConfig);
|
||||||
|
docker.resetPrefetchedImages();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if no packageManager updates present', async () => {
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'flake.nix',
|
||||||
|
updatedDeps: [{ ...validDepUpdate, depName: 'xmldoc', depType: 'patch' }],
|
||||||
|
newPackageFileContent: 'some new content',
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if currentValue is undefined', async () => {
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'flake.nix',
|
||||||
|
updatedDeps: [{ ...validDepUpdate, currentValue: undefined }],
|
||||||
|
newPackageFileContent: 'some new content',
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if currentValue has no hash', async () => {
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'flake.nix',
|
||||||
|
updatedDeps: [{ ...validDepUpdate, currentValue: '8.15.5' }],
|
||||||
|
newPackageFileContent: 'some new content',
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if unchanged', async () => {
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce('some content');
|
||||||
|
const execSnapshots = mockExecAll();
|
||||||
|
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'package.json',
|
||||||
|
updatedDeps: [validDepUpdate],
|
||||||
|
newPackageFileContent: 'some content',
|
||||||
|
config: { ...config },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
expect(execSnapshots).toMatchObject([{ cmd: 'corepack use pnpm@8.15.6' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns updated package.json', async () => {
|
||||||
|
fs.readLocalFile
|
||||||
|
.mockResolvedValueOnce('{}') // for node constraints
|
||||||
|
.mockResolvedValue('some new content'); // for updated package.json
|
||||||
|
const execSnapshots = mockExecAll();
|
||||||
|
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'package.json',
|
||||||
|
updatedDeps: [validDepUpdate],
|
||||||
|
newPackageFileContent: 'some content',
|
||||||
|
config: { ...config },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual([
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
contents: 'some new content',
|
||||||
|
path: 'package.json',
|
||||||
|
type: 'addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(execSnapshots).toMatchObject([{ cmd: 'corepack use pnpm@8.15.6' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports docker mode', async () => {
|
||||||
|
GlobalConfig.set(dockerAdminConfig);
|
||||||
|
const execSnapshots = mockExecAll();
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce('some new content');
|
||||||
|
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'package.json',
|
||||||
|
updatedDeps: [validDepUpdate],
|
||||||
|
newPackageFileContent: 'some content',
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
constraints: { node: '20.1.0', corepack: '0.29.3' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual([
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
contents: 'some new content',
|
||||||
|
path: 'package.json',
|
||||||
|
type: 'addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(execSnapshots).toMatchObject([
|
||||||
|
{ cmd: 'docker pull ghcr.io/containerbase/sidecar' },
|
||||||
|
{ cmd: 'docker ps --filter name=renovate_sidecar -aq' },
|
||||||
|
{
|
||||||
|
cmd:
|
||||||
|
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
|
||||||
|
'-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
|
||||||
|
'-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
|
||||||
|
'-e CONTAINERBASE_CACHE_DIR ' +
|
||||||
|
'-w "/tmp/github/some/repo" ' +
|
||||||
|
'ghcr.io/containerbase/sidecar ' +
|
||||||
|
'bash -l -c "' +
|
||||||
|
'install-tool node 20.1.0 ' +
|
||||||
|
'&& ' +
|
||||||
|
'install-tool corepack 0.29.3 ' +
|
||||||
|
'&& ' +
|
||||||
|
'corepack use pnpm@8.15.6' +
|
||||||
|
'"',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports install mode', async () => {
|
||||||
|
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
|
||||||
|
const execSnapshots = mockExecAll();
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce('some new content');
|
||||||
|
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'package.json',
|
||||||
|
updatedDeps: [validDepUpdate],
|
||||||
|
newPackageFileContent: 'some content',
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
constraints: { node: '20.1.0', corepack: '0.29.3' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual([
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
contents: 'some new content',
|
||||||
|
path: 'package.json',
|
||||||
|
type: 'addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(execSnapshots).toMatchObject([
|
||||||
|
{
|
||||||
|
cmd: 'install-tool node 20.1.0',
|
||||||
|
options: { cwd: '/tmp/github/some/repo' },
|
||||||
|
},
|
||||||
|
{ cmd: 'install-tool corepack 0.29.3' },
|
||||||
|
|
||||||
|
{
|
||||||
|
cmd: 'corepack use pnpm@8.15.6',
|
||||||
|
options: { cwd: '/tmp/github/some/repo' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catches errors', async () => {
|
||||||
|
const execSnapshots = mockExecSequence([new Error('exec error')]);
|
||||||
|
|
||||||
|
const res = await updateArtifacts({
|
||||||
|
packageFileName: 'package.json',
|
||||||
|
updatedDeps: [validDepUpdate],
|
||||||
|
newPackageFileContent: 'some content',
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
constraints: { node: '20.1.0', corepack: '0.29.3' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual([
|
||||||
|
{
|
||||||
|
artifactError: { fileName: 'package.json', stderr: 'exec error' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(execSnapshots).toMatchObject([{ cmd: 'corepack use pnpm@8.15.6' }]);
|
||||||
|
});
|
||||||
|
});
|
97
lib/modules/manager/npm/artifacts.ts
Normal file
97
lib/modules/manager/npm/artifacts.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import upath from 'upath';
|
||||||
|
import { logger } from '../../../logger';
|
||||||
|
import { exec } from '../../../util/exec';
|
||||||
|
import type { ExecOptions } from '../../../util/exec/types';
|
||||||
|
import { readLocalFile } from '../../../util/fs';
|
||||||
|
import { regEx } from '../../../util/regex';
|
||||||
|
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
|
||||||
|
import { getNodeToolConstraint } from './post-update/node-version';
|
||||||
|
import { lazyLoadPackageJson } from './post-update/utils';
|
||||||
|
|
||||||
|
// eg. 8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589
|
||||||
|
const versionWithHashRegString = '^(?<version>.*)\\+(?<hash>.*)';
|
||||||
|
|
||||||
|
// Execute 'corepack use' command for npm manager updates
|
||||||
|
// This step is necessary because Corepack recommends attaching a hash after the version
|
||||||
|
// The hash is generated only after running 'corepack use' and cannot be fetched from the npm registry
|
||||||
|
export async function updateArtifacts({
|
||||||
|
packageFileName,
|
||||||
|
config,
|
||||||
|
updatedDeps,
|
||||||
|
newPackageFileContent: existingPackageFileContent,
|
||||||
|
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
|
||||||
|
logger.debug(`npm.updateArtifacts(${packageFileName})`);
|
||||||
|
const packageManagerUpdate = updatedDeps.find(
|
||||||
|
(dep) => dep.depType === 'packageManager',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!packageManagerUpdate) {
|
||||||
|
logger.debug('No packageManager updates - returning null');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentValue, depName, newVersion } = packageManagerUpdate;
|
||||||
|
|
||||||
|
// Execute 'corepack use' command only if the currentValue already has hash in it
|
||||||
|
if (!currentValue || !regEx(versionWithHashRegString).test(currentValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asumming that corepack only needs to modify the package.json file in the root folder
|
||||||
|
// As it should not be regular practice to have different package managers in different workspaces
|
||||||
|
const pkgFileDir = upath.dirname(packageFileName);
|
||||||
|
const lazyPkgJson = lazyLoadPackageJson(pkgFileDir);
|
||||||
|
const cmd = `corepack use ${depName}@${newVersion}`;
|
||||||
|
|
||||||
|
const nodeConstraints = await getNodeToolConstraint(
|
||||||
|
config,
|
||||||
|
updatedDeps,
|
||||||
|
pkgFileDir,
|
||||||
|
lazyPkgJson,
|
||||||
|
);
|
||||||
|
|
||||||
|
const execOptions: ExecOptions = {
|
||||||
|
cwdFile: packageFileName,
|
||||||
|
toolConstraints: [
|
||||||
|
nodeConstraints,
|
||||||
|
{
|
||||||
|
toolName: 'corepack',
|
||||||
|
constraint: config.constraints?.corepack,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
docker: {},
|
||||||
|
userConfiguredEnv: config.env,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await exec(cmd, execOptions);
|
||||||
|
|
||||||
|
const newPackageFileContent = await readLocalFile(packageFileName, 'utf8');
|
||||||
|
if (
|
||||||
|
!newPackageFileContent ||
|
||||||
|
existingPackageFileContent === newPackageFileContent
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
logger.debug('Returning updated package.json');
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
type: 'addition',
|
||||||
|
path: packageFileName,
|
||||||
|
contents: newPackageFileContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ err }, 'Error updating package.json');
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
artifactError: {
|
||||||
|
fileName: packageFileName,
|
||||||
|
stderr: err.message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ export {
|
||||||
updateLockedDependency,
|
updateLockedDependency,
|
||||||
} from './update';
|
} from './update';
|
||||||
export { getRangeStrategy } from './range';
|
export { getRangeStrategy } from './range';
|
||||||
|
export { updateArtifacts } from './artifacts';
|
||||||
|
|
||||||
export const supportsLockFileMaintenance = true;
|
export const supportsLockFileMaintenance = true;
|
||||||
|
|
||||||
|
|
|
@ -189,6 +189,7 @@ export interface ArtifactNotice {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtifactError {
|
export interface ArtifactError {
|
||||||
|
fileName?: string;
|
||||||
lockFile?: string;
|
lockFile?: string;
|
||||||
stderr?: string;
|
stderr?: string;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue