feat(npm): append corepack hashes when updating package managers (#30552)

This commit is contained in:
RahulGautamSingh 2024-08-19 19:07:25 +05:30 committed by GitHub
parent 463f8f2ded
commit c2821134f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 329 additions and 0 deletions

View 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' }]);
});
});

View 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,
},
},
];
}
}

View file

@ -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;

View file

@ -189,6 +189,7 @@ export interface ArtifactNotice {
} }
export interface ArtifactError { export interface ArtifactError {
fileName?: string;
lockFile?: string; lockFile?: string;
stderr?: string; stderr?: string;
} }