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(manager/pep621): support pdm lock files #22244

Merged
merged 5 commits into from May 18, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 89 additions & 0 deletions lib/modules/manager/pep621/artifacts.spec.ts
@@ -0,0 +1,89 @@
import { join } from 'upath';
import { mockExecAll } from '../../../../test/exec-util';
import { fs, mockedFunction } from '../../../../test/util';
import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import { getPkgReleases as _getPkgReleases } from '../../datasource';
import type { UpdateArtifactsConfig } from '../types';
import { updateArtifacts } from './artifacts';

jest.mock('../../../util/fs');
jest.mock('../../datasource');

const getPkgReleases = mockedFunction(_getPkgReleases);

const config: UpdateArtifactsConfig = {};
const adminConfig: RepoGlobalConfig = {
localDir: join('/tmp/github/some/repo'),
cacheDir: join('/tmp/cache'),
containerbaseDir: join('/tmp/cache/containerbase'),
};

describe('modules/manager/pep621/artifacts', () => {
describe('updateArtifacts()', () => {
it('return null if all processors returns are empty', async () => {
const updatedDeps = [{ depName: 'dep1' }];
const result = await updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config,
updatedDeps,
});
expect(result).toBeNull();
});

it('return processor result', async () => {
const execSnapshots = mockExecAll();
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.getSiblingFileName.mockReturnValueOnce('pdm.lock');
fs.readLocalFile.mockResolvedValueOnce('old test content');
fs.readLocalFile.mockResolvedValueOnce('new test content');
// pdm
getPkgReleases.mockResolvedValueOnce({
releases: [{ version: 'v2.6.1' }, { version: 'v2.5.0' }],
});

const updatedDeps = [{ depName: 'dep1' }];
const result = await updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config: {},
updatedDeps,
});
expect(result).toEqual([
{
file: {
contents: 'new test content',
path: 'pdm.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'docker pull containerbase/sidecar',
options: {
encoding: 'utf-8',
},
},
{
cmd: 'docker ps --filter name=renovate_sidecar -aq',
options: {
encoding: 'utf-8',
},
},
{
cmd: 'docker run --rm --name=renovate_sidecar --label=renovate_child -v "/tmp/github/some/repo":"/tmp/github/some/repo" -v "/tmp/cache":"/tmp/cache" -e BUILDPACK_CACHE_DIR -e CONTAINERBASE_CACHE_DIR -w "/tmp/github/some/repo" containerbase/sidecar bash -l -c "install-tool pdm v2.5.0 && pdm update dep1"',
secustor marked this conversation as resolved.
Show resolved Hide resolved
options: {
cwd: '/tmp/github/some/repo',
encoding: 'utf-8',
env: {
BUILDPACK_CACHE_DIR: '/tmp/cache/containerbase',
CONTAINERBASE_CACHE_DIR: '/tmp/cache/containerbase',
},
},
},
]);
});
});
});
23 changes: 23 additions & 0 deletions lib/modules/manager/pep621/artifacts.ts
@@ -0,0 +1,23 @@
import is from '@sindresorhus/is';
import { writeLocalFile } from '../../../util/fs';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import { processors } from './processors';

export async function updateArtifacts(
updateArtifact: UpdateArtifact
): Promise<UpdateArtifactsResult[] | null> {
const { packageFileName, newPackageFileContent } = updateArtifact;

await writeLocalFile(packageFileName, newPackageFileContent);
secustor marked this conversation as resolved.
Show resolved Hide resolved

// process specific tool sets
const result: UpdateArtifactsResult[] = [];
for (const processor of processors) {
const artifactUpdates = await processor.updateArtifacts(updateArtifact);
if (is.array(artifactUpdates)) {
result.push(...artifactUpdates);
}
}

return result.length > 0 ? result : null;
}
3 changes: 3 additions & 0 deletions lib/modules/manager/pep621/index.ts
@@ -1,8 +1,11 @@
import { PypiDatasource } from '../../datasource/pypi';
export { extractPackageFile } from './extract';
export { updateArtifacts } from './artifacts';

export const supportedDatasources = [PypiDatasource.id];

export const supportsLockFileMaintenance = true;

export const defaultConfig = {
fileMatch: ['(^|/)pyproject\\.toml$'],
};
160 changes: 160 additions & 0 deletions lib/modules/manager/pep621/processors/pdm.spec.ts
@@ -0,0 +1,160 @@
import { join } from 'upath';
import { mockExecAll } from '../../../../../test/exec-util';
import { fs, mockedFunction } from '../../../../../test/util';
import { GlobalConfig } from '../../../../config/global';
import type { RepoGlobalConfig } from '../../../../config/types';
import { getPkgReleases as _getPkgReleases } from '../../../datasource';
import type { UpdateArtifactsConfig } from '../../types';
import { PdmProcessor } from './pdm';

jest.mock('../../../../util/fs');
jest.mock('../../../datasource');

const getPkgReleases = mockedFunction(_getPkgReleases);

const config: UpdateArtifactsConfig = {};
const adminConfig: RepoGlobalConfig = {
localDir: join('/tmp/github/some/repo'),
cacheDir: join('/tmp/cache'),
containerbaseDir: join('/tmp/cache/containerbase'),
};

const processor = new PdmProcessor();

describe('modules/manager/pep621/processors/pdm', () => {
describe('updateArtifacts()', () => {
it('return null if there is no lock file', async () => {
fs.getSiblingFileName.mockReturnValueOnce('pdm.lock');
const updatedDeps = [{ depName: 'dep1' }];
const result = await processor.updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config,
updatedDeps,
});
expect(result).toBeNull();
});

it('return null if the lock file is unchanged', async () => {
const execSnapshots = mockExecAll();
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.getSiblingFileName.mockReturnValueOnce('pdm.lock');
fs.readLocalFile.mockResolvedValueOnce('test content');
fs.readLocalFile.mockResolvedValueOnce('test content');
// pdm
getPkgReleases.mockResolvedValueOnce({
releases: [{ version: 'v2.6.1' }, { version: 'v2.5.0' }],
});

const updatedDeps = [{ depName: 'dep1' }];
const result = await processor.updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config: {},
updatedDeps,
});
expect(result).toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'docker pull 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/cache":"/tmp/cache" -e BUILDPACK_CACHE_DIR -e CONTAINERBASE_CACHE_DIR -w "/tmp/github/some/repo" containerbase/sidecar bash -l -c "install-tool pdm v2.5.0 && pdm update dep1"',
secustor marked this conversation as resolved.
Show resolved Hide resolved
},
]);
});

it('rethrow error', async () => {
secustor marked this conversation as resolved.
Show resolved Hide resolved
const execSnapshots = mockExecAll();
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.getSiblingFileName.mockReturnValueOnce('pdm.lock');
fs.readLocalFile.mockImplementationOnce(() => {
throw new Error('test error');
});

const updatedDeps = [{ depName: 'dep1' }];
const result = await processor.updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config: {},
updatedDeps,
});
expect(result).toEqual([
{ artifactError: { lockFile: 'pdm.lock', stderr: 'test error' } },
]);
expect(execSnapshots).toEqual([]);
});

it('return update dep update', async () => {
const execSnapshots = mockExecAll();
GlobalConfig.set(adminConfig);
fs.getSiblingFileName.mockReturnValueOnce('pdm.lock');
fs.readLocalFile.mockResolvedValueOnce('test content');
fs.readLocalFile.mockResolvedValueOnce('changed test content');
// pdm
getPkgReleases.mockResolvedValueOnce({
releases: [{ version: 'v2.6.1' }, { version: 'v2.5.0' }],
});

const updatedDeps = [{ depName: 'dep1' }, { depName: 'dep2' }];
const result = await processor.updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config: {},
updatedDeps,
});
expect(result).toEqual([
{
file: {
contents: 'changed test content',
path: 'pdm.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'pdm update dep1 dep2',
},
]);
});

it('return update on lockfileMaintenance', async () => {
const execSnapshots = mockExecAll();
GlobalConfig.set(adminConfig);
fs.getSiblingFileName.mockReturnValueOnce('pdm.lock');
fs.readLocalFile.mockResolvedValueOnce('test content');
fs.readLocalFile.mockResolvedValueOnce('changed test content');
// pdm
getPkgReleases.mockResolvedValueOnce({
releases: [{ version: 'v2.6.1' }, { version: 'v2.5.0' }],
});

const result = await processor.updateArtifacts({
packageFileName: 'pyproject.toml',
newPackageFileContent: '',
config: {
updateType: 'lockFileMaintenance',
},
updatedDeps: [],
});
expect(result).toEqual([
{
file: {
contents: 'changed test content',
path: 'pdm.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'pdm update ',
},
]);
});
});
});
82 changes: 81 additions & 1 deletion lib/modules/manager/pep621/processors/pdm.ts
@@ -1,6 +1,15 @@
import is from '@sindresorhus/is';
import { TEMPORARY_ERROR } from '../../../../constants/error-messages';
import { logger } from '../../../../logger';
import { exec } from '../../../../util/exec';
import type { ExecOptions, ToolConstraint } from '../../../../util/exec/types';
import { getSiblingFileName, readLocalFile } from '../../../../util/fs';
import { PypiDatasource } from '../../../datasource/pypi';
import type { PackageDependency } from '../../types';
import type {
PackageDependency,
UpdateArtifact,
UpdateArtifactsResult,
} from '../../types';
import type { PyProject } from '../schema';
import { parseDependencyGroupRecord } from '../utils';
import type { PyProjectProcessor } from './types';
Expand Down Expand Up @@ -39,4 +48,75 @@ export class PdmProcessor implements PyProjectProcessor {

return deps;
}

async updateArtifacts(
updateArtifact: UpdateArtifact
): Promise<UpdateArtifactsResult[] | null> {
const { config, updatedDeps, packageFileName } = updateArtifact;

const isLockFileMaintenance = config.updateType === 'lockFileMaintenance';

// abort if no lockfile is defined
const lockFileName = getSiblingFileName(packageFileName, 'pdm.lock');
try {
const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
if (is.nullOrUndefined(existingLockFileContent)) {
logger.debug('No pdm.lock found');
return null;
}

const toolConstraint: ToolConstraint = {
toolName: 'pdm',
constraint: config.constraints?.pdm,
};

const execOptions: ExecOptions = {
docker: {},
toolConstraints: [toolConstraint],
};

// on lockFileMaintenance do not specify any packages and update the complete lock file
secustor marked this conversation as resolved.
Show resolved Hide resolved
// else only update specific packages
let packageList = '';
if (!isLockFileMaintenance) {
packageList = updatedDeps
secustor marked this conversation as resolved.
Show resolved Hide resolved
.map((value) => value.packageName ?? value.depName)
secustor marked this conversation as resolved.
Show resolved Hide resolved
.join(' ');
}
const cmd = `pdm update ${packageList}`;
secustor marked this conversation as resolved.
Show resolved Hide resolved
await exec([cmd], execOptions);
secustor marked this conversation as resolved.
Show resolved Hide resolved

// check for changes
const fileChanges: UpdateArtifactsResult[] = [];
const newLockContent = await readLocalFile(lockFileName, 'utf8');
const isLockFileChanged = existingLockFileContent !== newLockContent;
if (isLockFileChanged) {
fileChanges.push({
file: {
type: 'addition',
path: lockFileName,
contents: newLockContent,
},
});
} else {
logger.debug('pdm.lock is unchanged');
}

return fileChanges.length > 0 ? fileChanges : null;
secustor marked this conversation as resolved.
Show resolved Hide resolved
} catch (err) {
// istanbul ignore if
if (err.message === TEMPORARY_ERROR) {
throw err;
}
logger.debug({ err }, 'Failed to update PDM lock file');
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}
}
}
10 changes: 9 additions & 1 deletion lib/modules/manager/pep621/processors/types.ts
@@ -1,7 +1,15 @@
import type { PackageDependency } from '../../types';
import type {
PackageDependency,
UpdateArtifact,
UpdateArtifactsResult,
} from '../../types';
import type { PyProject } from '../schema';

export interface PyProjectProcessor {
updateArtifacts(
updateArtifact: UpdateArtifact
): Promise<UpdateArtifactsResult[] | null>;

/**
* Extracts additional dependencies and/or modifies existing ones based on the tool configuration.
* If no relevant section for the processor exists, then it should return the received dependencies unmodified.
Expand Down