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): append corepack hashes when updating package managers #30552

Merged
merged 11 commits into from
Aug 19, 2024
230 changes: 230 additions & 0 deletions lib/modules/manager/npm/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
rangeStrategy: 'pin',
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 and rangeStrategy is not pin', async () => {
const res = await updateArtifacts({
packageFileName: 'flake.nix',
updatedDeps: [
{ ...validDepUpdate, currentValue: '8.15.5', rangeStrategy: 'auto' },
],
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 enable' },
{ cmd: 'corepack use pnpm@8.15.6' },
]);
});

it('returns updated package.json', async () => {
fs.readLocalFile.mockResolvedValueOnce('some new content');
const execSnapshots = mockExecAll();

const res = await updateArtifacts({
packageFileName: 'package.json',
updatedDeps: [validDepUpdate],
newPackageFileContent: 'some content',
config: { ...config },
});

expect(res).toEqual([

Check failure on line 116 in lib/modules/manager/npm/artifacts.spec.ts

View workflow job for this annotation

GitHub Actions / test (5/16)

modules/manager/npm/artifacts › returns updated package.json

expect(received).toEqual(expected) // deep equality Expected: [{"file": {"contents": "some new content", "path": "package.json", "type": "addition"}}] Received: null at Object.<anonymous> (lib/modules/manager/npm/artifacts.spec.ts:116:17)
{
file: {
contents: 'some new content',
path: 'package.json',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: 'corepack enable' },
{ 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' } },
});

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 ' +
'&& ' +
'corepack enable ' +
'&& ' +
'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' } },
});

expect(res).toEqual([
{
file: {
contents: 'some new content',
path: 'package.json',
type: 'addition',
},
},
]);

expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool node 20.1.0' },
{
cmd: 'corepack enable',
options: { cwd: '/tmp/github/some/repo' },
},
{
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,
});

expect(res).toEqual([
{
artifactError: { fileName: 'package.json', stderr: 'exec error' },
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: 'corepack enable', options: { cwd: '/tmp/github/some/repo' } },
// { cmd: 'corepack use pnpm@8.15.6' },
]);
});
});
95 changes: 95 additions & 0 deletions lib/modules/manager/npm/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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({
viceice marked this conversation as resolved.
Show resolved Hide resolved
packageFileName,
config,
updatedDeps,
newPackageFileContent: existingPackageFileContent,
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
logger.debug(`npm.updateArtifacts(${packageFileName})`);
const packageManagerUpdate = updatedDeps.find(
(dep) => dep.depType === 'packageManager',
);

if (!packageManagerUpdate) {
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
logger.debug('No packageManager updates - returning null');
return null;
}

const { currentValue, depName, newVersion, rangeStrategy } =
packageManagerUpdate;

// Execute 'corepack use' command only if:
// 1. The package manager version is pinned in the project, or
// 2. The package manager version already has a hash appended
if (
!currentValue ||
(rangeStrategy !== 'pin' &&
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
!regEx(versionWithHashRegString).test(currentValue))
viceice marked this conversation as resolved.
Show resolved Hide resolved
) {
return null;
}

// asuuming that corepack only modified the root package.json
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
// as it should not be normal practice to have different package maangers in different workspaces
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
const pkgFileDir = upath.dirname(packageFileName);
const lazyPkgJson = lazyLoadPackageJson(pkgFileDir);
const cmd = ['corepack enable'];
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved

cmd.push(`corepack use ${depName}@${newVersion}`);

const execOptions: ExecOptions = {
cwdFile: packageFileName,
toolConstraints: [
await getNodeToolConstraint(config, updatedDeps, pkgFileDir, lazyPkgJson),
],
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,
},
},
];
}
}
1 change: 1 addition & 0 deletions lib/modules/manager/npm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
updateLockedDependency,
} from './update';
export { getRangeStrategy } from './range';
export { updateArtifacts } from './artifacts';

export const supportsLockFileMaintenance = true;

Expand Down
1 change: 1 addition & 0 deletions lib/modules/manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export interface ArtifactNotice {
}

export interface ArtifactError {
fileName?: string;
lockFile?: string;
stderr?: string;
}
Expand Down
Loading