diff --git a/lib/modules/manager/pep621/artifacts.spec.ts b/lib/modules/manager/pep621/artifacts.spec.ts new file mode 100644 index 00000000000000..f104c7e51f4327 --- /dev/null +++ b/lib/modules/manager/pep621/artifacts.spec.ts @@ -0,0 +1,105 @@ +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 = [ + { + packageName: '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 = [{ packageName: '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' + + '"', + options: { + cwd: '/tmp/github/some/repo', + encoding: 'utf-8', + env: { + BUILDPACK_CACHE_DIR: '/tmp/cache/containerbase', + CONTAINERBASE_CACHE_DIR: '/tmp/cache/containerbase', + }, + }, + }, + ]); + }); + }); +}); diff --git a/lib/modules/manager/pep621/artifacts.ts b/lib/modules/manager/pep621/artifacts.ts new file mode 100644 index 00000000000000..21145c463c14e2 --- /dev/null +++ b/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 { + const { packageFileName, newPackageFileContent } = updateArtifact; + + await writeLocalFile(packageFileName, newPackageFileContent); + + // 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; +} diff --git a/lib/modules/manager/pep621/index.ts b/lib/modules/manager/pep621/index.ts index 9b1adb54893e40..62a464046df595 100644 --- a/lib/modules/manager/pep621/index.ts +++ b/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$'], }; diff --git a/lib/modules/manager/pep621/processors/pdm.spec.ts b/lib/modules/manager/pep621/processors/pdm.spec.ts new file mode 100644 index 00000000000000..d455b0b722b576 --- /dev/null +++ b/lib/modules/manager/pep621/processors/pdm.spec.ts @@ -0,0 +1,172 @@ +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 = [{ packageName: '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 = [{ packageName: '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' + + '"', + }, + ]); + }); + + it('returns artifact error', async () => { + const execSnapshots = mockExecAll(); + GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); + fs.getSiblingFileName.mockReturnValueOnce('pdm.lock'); + fs.readLocalFile.mockImplementationOnce(() => { + throw new Error('test error'); + }); + + const updatedDeps = [{ packageName: '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 = [{ packageName: 'dep1' }, { packageName: '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', + }, + ]); + }); + }); +}); diff --git a/lib/modules/manager/pep621/processors/pdm.ts b/lib/modules/manager/pep621/processors/pdm.ts index e362694b55a745..cf717b6dbfcfd0 100644 --- a/lib/modules/manager/pep621/processors/pdm.ts +++ b/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'; @@ -39,4 +48,74 @@ export class PdmProcessor implements PyProjectProcessor { return deps; } + + async updateArtifacts( + updateArtifact: UpdateArtifact + ): Promise { + 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 + // else only update specific packages + let packageList = ''; + if (!isLockFileMaintenance) { + packageList = ' '; + packageList += updatedDeps.map((value) => value.packageName).join(' '); + } + const cmd = `pdm update${packageList}`; + await exec(cmd, execOptions); + + // 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 ? fileChanges : null; + } 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, + }, + }, + ]; + } + } } diff --git a/lib/modules/manager/pep621/processors/types.ts b/lib/modules/manager/pep621/processors/types.ts index dbe645396e76c2..37798951b7e161 100644 --- a/lib/modules/manager/pep621/processors/types.ts +++ b/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; + /** * 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. diff --git a/lib/modules/manager/pep621/readme.md b/lib/modules/manager/pep621/readme.md index 96fcde1007a12a..073e1bf787c172 100644 --- a/lib/modules/manager/pep621/readme.md +++ b/lib/modules/manager/pep621/readme.md @@ -2,7 +2,7 @@ This manager supports updating dependencies inside `pyproject.toml` files. In addition to standard dependencies, these toolsets are also supported: -- `pdm` +- `pdm` ( including `pdm.lock` files ) Available `depType`s: diff --git a/lib/util/exec/containerbase.ts b/lib/util/exec/containerbase.ts index bcb21adab9dee1..d9efe6285ac82b 100644 --- a/lib/util/exec/containerbase.ts +++ b/lib/util/exec/containerbase.ts @@ -130,6 +130,11 @@ const allToolConfig: Record = { hash: true, versioning: npmVersioningId, }, + pdm: { + datasource: 'github-releases', + packageName: 'pdm-project/pdm', + versioning: semverVersioningId, + }, php: { datasource: 'github-releases', packageName: 'containerbase/php-prebuild',