From ad3df33c09709fd5d30562baf0442204826859d7 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Tue, 21 May 2024 17:41:24 +0200 Subject: [PATCH 01/11] Add copier manager --- docs/usage/configuration-options.md | 34 ++ docs/usage/self-hosted-configuration.md | 4 + lib/config/options/index.ts | 72 ++++ lib/config/types.ts | 14 +- lib/modules/manager/api.ts | 2 + lib/modules/manager/copier/artifacts.spec.ts | 409 +++++++++++++++++++ lib/modules/manager/copier/artifacts.ts | 179 ++++++++ lib/modules/manager/copier/extract.spec.ts | 58 +++ lib/modules/manager/copier/extract.ts | 31 ++ lib/modules/manager/copier/index.ts | 12 + lib/modules/manager/copier/readme.md | 7 + lib/modules/manager/copier/schema.ts | 11 + lib/modules/manager/copier/update.spec.ts | 25 ++ lib/modules/manager/copier/update.ts | 22 + lib/modules/manager/types.ts | 3 + 15 files changed, 882 insertions(+), 1 deletion(-) create mode 100644 lib/modules/manager/copier/artifacts.spec.ts create mode 100644 lib/modules/manager/copier/artifacts.ts create mode 100644 lib/modules/manager/copier/extract.spec.ts create mode 100644 lib/modules/manager/copier/extract.ts create mode 100644 lib/modules/manager/copier/index.ts create mode 100644 lib/modules/manager/copier/readme.md create mode 100644 lib/modules/manager/copier/schema.ts create mode 100644 lib/modules/manager/copier/update.spec.ts create mode 100644 lib/modules/manager/copier/update.ts diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 2f573e4ea19637..8ad38c5ea258fe 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -667,6 +667,40 @@ When using with `npm`, we recommend you: - Use `constraintsFiltering` on `dependencies`, not `devDependencies` (usually you do not need to be strict about development dependencies) - Do _not_ enable `rollbackPrs` at the same time (otherwise your _current_ version may be rolled back if it's incompatible) +## copierOptions + +Configure how the `copier` manager invokes the CLI tool. + +The `copierOptions` configuration consists of six fields: + +### recopy + +By default, the manager uses `copier update --skip-answered --defaults` to update a templated project. +Merge conflicts are warned about, but still result in a PR. +This switch instructs the manager to use `copier recopy --skip-answered --defaults --overwrite` instead, which disables the smart update algorithm and overwrites any customizations. + +### skipTasks + +Instruct Copier to skip template tasks execution. +Mind that they are only executed if the admin has enabled `copierTrust` in the self-hosted configuration. + +### data + +A mapping of arbitrary template variable names to their (string) values to pass to Copier. + +### dataFile + +The relative path of a YAML file to load Copier template variables from. +Must be part of the repository. + +### skip + +An array of paths (globs) to skip updates from the Copier template on if they exist already in the respository. + +### exclude + +An array of paths (globs) that should not be copied from the Copier template. + ## customDatasources Use `customDatasources` to fetch releases from APIs or statically hosted sites and Renovate has no own datasource. diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 0a0bd42b6bb530..4a485060584daa 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -355,6 +355,10 @@ This directory is used to cache downloads when `binarySource=docker` or `binaryS Use this option if you need such downloads to be stored outside of Renovate's regular cache directory (`cacheDir`). +## copierTrust + +When using the `copier` manager, whether to instruct it to allow templates with unsafe features (Jinja extensions, migrations, tasks). + ## customEnvVariables This configuration will be applied after all other environment variables so you can use it to override defaults. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 812c4b53948be4..b06da35a4cc32d 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -3088,6 +3088,78 @@ const options: RenovateOptions[] = [ default: 90, globalOnly: true, }, + { + name: 'copierOptions', + description: + 'Configure how Copier is invoked. Unsafe features (--trust) can only be enabled in the self-hosted configuration.', + type: 'object', + default: { + recopy: false, + skipTasks: false, + data: {}, + dataFile: '', + skip: [], + exclude: [], + }, + }, + { + name: 'recopy', + description: + 'Request Copier to recopy a template instead of using the smart update algorithm.', + parents: ['copierOptions'], + type: 'boolean', + default: false, + }, + { + name: 'skipTasks', + description: 'Skip Copier template tasks execution.', + parents: ['copierOptions'], + type: 'boolean', + default: false, + }, + { + name: 'data', + description: 'Pass arbitrary Copier template variables with their values.', + parents: ['copierOptions'], + type: 'object', + freeChoice: true, + mergeable: true, + default: {}, + }, + { + name: 'dataFile', + description: + 'An optional relative path of a YAML file to load Copier template variables from.', + parents: ['copierOptions'], + type: 'string', + default: '', + }, + { + name: 'skip', + description: + 'An array of paths/globs to skip during Copier template rendering if they exist already.', + parents: ['copierOptions'], + type: 'array', + subType: 'string', + default: [], + }, + { + name: 'exclude', + description: + 'An array of paths/globs that should never be copied during Copier template rendering.', + parents: ['copierOptions'], + type: 'array', + subType: 'string', + default: [], + }, + { + name: 'copierTrust', + description: + 'Allow templates with unsafe features (Jinja extensions, migrations, tasks).', + type: 'boolean', + default: false, + globalOnly: true, + }, ]; export function getOptions(): RenovateOptions[] { diff --git a/lib/config/types.ts b/lib/config/types.ts index 2129fd9b7ecc2b..14bdacb9dac93d 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -141,6 +141,7 @@ export interface RepoGlobalConfig { cacheHardTtlMinutes?: number; cacheTtlOverride?: Record; containerbaseDir?: string; + copierTrust?: boolean; customEnvVariables?: Record; dockerChildPrefix?: string; dockerCliOptions?: string; @@ -266,6 +267,7 @@ export interface RenovateConfig prConcurrentLimit?: number; prHourlyLimit?: number; forkModeDisallowMaintainerEdits?: boolean; + copierOptions?: CopierOptions; defaultRegistryUrls?: string[]; registryUrls?: string[] | null; @@ -401,7 +403,8 @@ export type AllowedParents = | 'hostRules' | 'postUpgradeTasks' | 'packageRules' - | 'logLevelRemap'; + | 'logLevelRemap' + | 'copierOptions'; export interface RenovateOptionBase { /** * If true, the option can only be configured by people with access to the Renovate instance. @@ -577,3 +580,12 @@ export interface ValidationResult { errors: ValidationMessage[]; warnings: ValidationMessage[]; } + +export interface CopierOptions extends Record { + recopy?: boolean; + skipTasks?: boolean; + data?: Record; + dataFile?: string; + skip?: string[]; + exclude?: string[]; +} diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 66cac2fdf84bef..b44e97fc86daba 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -21,6 +21,7 @@ import * as cloudbuild from './cloudbuild'; import * as cocoapods from './cocoapods'; import * as composer from './composer'; import * as conan from './conan'; +import * as copier from './copier'; import * as cpanfile from './cpanfile'; import * as crossplane from './crossplane'; import * as depsEdn from './deps-edn'; @@ -117,6 +118,7 @@ api.set('cloudbuild', cloudbuild); api.set('cocoapods', cocoapods); api.set('composer', composer); api.set('conan', conan); +api.set('copier', copier); api.set('cpanfile', cpanfile); api.set('crossplane', crossplane); api.set('deps-edn', depsEdn); diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts new file mode 100644 index 00000000000000..cfd0a87ceaebc6 --- /dev/null +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -0,0 +1,409 @@ +import { join } from 'upath'; +import { mockExecAll } from '../../../../test/exec-util'; +import { fs, git, partial } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import type { RepoGlobalConfig } from '../../../config/types'; +import { logger } from '../../../logger'; +import type { StatusResult } from '../../../util/git/types'; +import type { UpdateArtifactsConfig, Upgrade } from '../types'; +import { updateArtifacts } from '.'; + +jest.mock('../../../util/git'); +jest.mock('../../../util/fs'); + +const config: UpdateArtifactsConfig = { + copierOptions: { + recopy: false, + skipTasks: false, + data: {}, + dataFile: '', + skip: [], + exclude: [], + }, + copierTrust: false, +}; + +const upgrades: Upgrade[] = [ + { + depName: 'https://github.com/foo/bar', + currentValue: '1.0.0', + newValue: '1.1.0', + }, +]; + +const adminConfig: RepoGlobalConfig = { + localDir: join('/tmp/github/some/repo'), + cacheDir: join('/tmp/cache'), +}; + +describe('modules/manager/copier/artifacts', () => { + beforeEach(() => { + GlobalConfig.set(adminConfig); + + // Mock git repo status + git.getRepoStatus.mockResolvedValue( + partial({ + conflicted: [], + modified: ['.copier-answers.yml'], + not_added: [], + deleted: [], + }), + ); + }); + + afterEach(() => { + GlobalConfig.reset(); + fs.readLocalFile.mockClear(); + git.getRepoStatus.mockClear(); + }); + + describe('updateArtifacts()', () => { + it('returns null if newVersion is not provided', async () => { + const execSnapshots = mockExecAll(); + + const invalidUpgrade = [ + { ...upgrades[0], newValue: undefined, newVersion: undefined }, + ]; + + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: invalidUpgrade, + newPackageFileContent: '', + config, + }); + + expect(result).toEqual([ + { + artifactError: { + lockFile: '.copier-answers.yml', + stderr: 'Missing copier template version to update to', + }, + }, + ]); + expect(execSnapshots).toMatchObject([]); + }); + + it('reports an error if no upgrade is specified', async () => { + const execSnapshots = mockExecAll(); + + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: [], + newPackageFileContent: '', + config, + }); + + expect(result).toEqual([ + { + artifactError: { + lockFile: '.copier-answers.yml', + stderr: 'Unexpected number of dependencies: 0 (should be 1)', + }, + }, + ]); + expect(execSnapshots).toMatchObject([]); + }); + + it('invokes copier update with the correct options by default', async () => { + const execSnapshots = mockExecAll(); + + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: {}, + }); + + expect(execSnapshots).toMatchObject([ + { + cmd: 'copier update --skip-answered --defaults --answers-file .copier-answers.yml --vcs-ref 1.1.0', + }, + ]); + }); + + it('includes --trust when copierTrust is true', async () => { + const execSnapshots = mockExecAll(); + + const trustConfig = { + ...config, + copierTrust: true, + }; + + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: trustConfig, + }); + + expect(execSnapshots).toMatchObject([ + { + cmd: 'copier update --skip-answered --defaults --trust --answers-file .copier-answers.yml --vcs-ref 1.1.0', + }, + ]); + }); + + it('handles data and list options correctly', async () => { + const execSnapshots = mockExecAll(); + + const optionsConfig = { + ...config, + copierOptions: { + ...config.copierOptions, + data: { + variable1: 'value1', + variable2: 'value2', + }, + dataFile: 'foo/bar.yaml', + skip: ['file1.txt', 'file2.txt'], + exclude: ['*.tmp', 'backup/*'], + }, + }; + + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: optionsConfig, + }); + + expect(execSnapshots).toMatchObject([ + { + cmd: "copier update --skip-answered --defaults --data-file foo/bar.yaml --data variable1=value1 --data variable2=value2 --skip file1.txt --skip file2.txt --exclude '*.tmp' --exclude 'backup/*' --answers-file .copier-answers.yml --vcs-ref 1.1.0", + }, + ]); + }); + + it('handles boolean options correctly', async () => { + const execSnapshots = mockExecAll(); + + const optionsConfig = { + ...config, + copierOptions: { + skipTasks: true, + }, + }; + + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: optionsConfig, + }); + + expect(execSnapshots).toMatchObject([ + { + cmd: 'copier update --skip-answered --defaults --skip-tasks --answers-file .copier-answers.yml --vcs-ref 1.1.0', + }, + ]); + }); + + it('does not allow a dataFile from outside the repository', async () => { + const execSnapshots = mockExecAll(); + + const optionsConfig = { + ...config, + copierOptions: { + dataFile: '/foo/bar.yml', + }, + }; + + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: optionsConfig, + }); + + expect(result).toEqual([ + { + artifactError: { + lockFile: '.copier-answers.yml', + stderr: 'copierOptions.dataFile is not part of the repository', + }, + }, + ]); + expect(execSnapshots).toMatchObject([]); + }); + + it('supports recopy instead of update', async () => { + const execSnapshots = mockExecAll(); + + const optionsConfig = { + ...config, + copierOptions: { + recopy: true, + }, + }; + + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: optionsConfig, + }); + + expect(execSnapshots).toMatchObject([ + { + cmd: 'copier recopy --skip-answered --defaults --overwrite --answers-file .copier-answers.yml --vcs-ref 1.1.0', + }, + ]); + }); + + it('handles exec errors', async () => { + mockExecAll(new Error('exec exception')); + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config, + }); + + expect(result).toEqual([ + { + artifactError: { + lockFile: '.copier-answers.yml', + stderr: 'exec exception', + }, + }, + ]); + }); + + it('does not report changes if answers-file was not changed', async () => { + mockExecAll(); + + git.getRepoStatus.mockResolvedValueOnce( + partial({ + conflicted: [], + modified: [], + not_added: ['new_file.py'], + deleted: ['old_file.py'], + }), + ); + + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config, + }); + + expect(result).toBeNull(); + }); + + it('returns updated artifacts if repo status has changes', async () => { + mockExecAll(); + + git.getRepoStatus.mockResolvedValueOnce( + partial({ + conflicted: [], + modified: ['.copier-answers.yml'], + not_added: ['new_file.py'], + deleted: ['old_file.py'], + }), + ); + + fs.readLocalFile.mockResolvedValueOnce( + '_src: https://github.com/foo/bar\n_commit: 1.1.0', + ); + fs.readLocalFile.mockResolvedValueOnce('new file contents'); + + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config, + }); + + expect(result).toEqual([ + { + file: { + type: 'addition', + path: '.copier-answers.yml', + contents: '_src: https://github.com/foo/bar\n_commit: 1.1.0', + }, + }, + { + file: { + type: 'addition', + path: 'new_file.py', + contents: 'new file contents', + }, + }, + { + file: { + type: 'deletion', + path: 'old_file.py', + }, + }, + ]); + }); + + it('warns about, but adds conflicts', async () => { + mockExecAll(); + + git.getRepoStatus.mockResolvedValueOnce( + partial({ + conflicted: ['conflict_file.py'], + modified: ['.copier-answers.yml'], + not_added: ['new_file.py'], + deleted: ['old_file.py'], + }), + ); + + fs.readLocalFile.mockResolvedValueOnce( + '_src: https://github.com/foo/bar\n_commit: 1.1.0', + ); + fs.readLocalFile.mockResolvedValueOnce('new file contents'); + fs.readLocalFile.mockResolvedValueOnce('conflict file contents'); + + const result = await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config, + }); + expect(logger.warn).toHaveBeenCalledWith( + { + depName: 'https://github.com/foo/bar', + packageFileName: '.copier-answers.yml', + }, + 'Updating the Copier template yielded 1 merge conflicts. Please check the proposed changes carefully! Conflicting files:\n * conflict_file.py', + ); + expect(result).toEqual([ + { + file: { + type: 'addition', + path: '.copier-answers.yml', + contents: '_src: https://github.com/foo/bar\n_commit: 1.1.0', + }, + }, + { + file: { + type: 'addition', + path: 'new_file.py', + contents: 'new file contents', + }, + }, + { + file: { + type: 'addition', + path: 'conflict_file.py', + contents: 'conflict file contents', + }, + notice: { + file: 'conflict_file.py', + message: + 'This file had merge conflicts. Please check the proposed changes carefully!', + }, + }, + { + file: { + type: 'deletion', + path: 'old_file.py', + }, + }, + ]); + }); + }); +}); diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts new file mode 100644 index 00000000000000..14a24840bdbca5 --- /dev/null +++ b/lib/modules/manager/copier/artifacts.ts @@ -0,0 +1,179 @@ +import { quote } from 'shlex'; +import { FILE_ACCESS_VIOLATION_ERROR } from '../../../constants/error-messages'; +import { logger } from '../../../logger'; +import { coerceArray } from '../../../util/array'; +import { exec } from '../../../util/exec'; +import { readLocalFile } from '../../../util/fs'; +import { ensureLocalPath } from '../../../util/fs/util'; +import { getRepoStatus } from '../../../util/git'; +import type { + UpdateArtifact, + UpdateArtifactsConfig, + UpdateArtifactsResult, +} from '../types'; + +type CopierBoolOpt = 'skipTasks'; +type CopierListOpt = 'skip' | 'exclude'; + +const boolOpts: Record = { + skipTasks: '--skip-tasks', +}; + +const listOpts: Record = { + skip: '--skip', + exclude: '--exclude', +}; + +const DEFAULT_COMMAND_OPTIONS = ['--skip-answered', '--defaults']; + +function buildCommand( + config: UpdateArtifactsConfig, + packageFileName: string, + newVersion: string, +): string { + const command = ['copier']; + if (config?.copierOptions?.recopy) { + command.push('recopy', ...DEFAULT_COMMAND_OPTIONS, '--overwrite'); + } else { + command.push('update', ...DEFAULT_COMMAND_OPTIONS); + } + if (config?.copierTrust) { + command.push('--trust'); + } + for (const [opt, param] of Object.entries(boolOpts)) { + if (config.copierOptions?.[opt]) { + command.push(param); + } + } + if (config.copierOptions?.dataFile) { + try { + ensureLocalPath(config.copierOptions.dataFile); + } catch (err) { + if (err.message === FILE_ACCESS_VIOLATION_ERROR) { + throw new Error( + 'copierOptions.dataFile is not part of the repository', + { cause: err }, + ); + } + // istanbul ignore next + throw err; + } + command.push('--data-file', quote(config.copierOptions.dataFile)); + } + for (const [key, value] of Object.entries(config.copierOptions?.data ?? {})) { + command.push('--data', quote(`${key}=${value}`)); + } + for (const [opt, param] of Object.entries(listOpts)) { + config.copierOptions?.[opt]?.forEach((item: string) => { + command.push(param, quote(item)); + }); + } + command.push( + '--answers-file', + quote(packageFileName), + '--vcs-ref', + quote(newVersion), + ); + return command.join(' '); +} + +function artifactError( + packageFileName: string, + message: string, +): UpdateArtifactsResult[] { + return [ + { + artifactError: { + lockFile: packageFileName, + stderr: message, + }, + }, + ]; +} + +export async function updateArtifacts({ + packageFileName, + updatedDeps, + config, +}: UpdateArtifact): Promise { + if (!updatedDeps || updatedDeps.length !== 1) { + // Each answers file (~ packageFileName) has exactly one dependency to update. + return artifactError( + packageFileName, + `Unexpected number of dependencies: ${updatedDeps.length} (should be 1)`, + ); + } + + const newVersion = updatedDeps[0]?.newVersion ?? updatedDeps[0]?.newValue; + if (!newVersion) { + return artifactError( + packageFileName, + 'Missing copier template version to update to', + ); + } + + let command: string; + try { + command = buildCommand(config, packageFileName, newVersion); + } catch (err) { + logger.error({ err }, `Failed to build copier command: ${err.message}`); + return artifactError(packageFileName, err.message); + } + + try { + await exec(command); + } catch (err) { + logger.error({ err }, `Failed to update copier template: ${err.message}`); + return artifactError(packageFileName, err.message); + } + + const status = await getRepoStatus(); + // If the answers file didn't change, Copier did not update anything. + if (!status.modified.includes(packageFileName)) { + return null; + } + + if (status.conflicted.length > 0) { + // Sometimes, Copier erroneously reports conflicts. + const msg = + `Updating the Copier template yielded ${status.conflicted.length} merge conflicts. ` + + 'Please check the proposed changes carefully! Conflicting files:\n * ' + + status.conflicted.join('\n * '); + logger.warn({ packageFileName, depName: updatedDeps[0]?.depName }, msg); + } + + const res: UpdateArtifactsResult[] = []; + + for (const f of [ + ...status.modified, + ...status.not_added, + ...status.conflicted, + ]) { + const fileRes: UpdateArtifactsResult = { + file: { + type: 'addition', + path: f, + contents: await readLocalFile(f), + }, + }; + if (status.conflicted.includes(f)) { + // Make the reviewer aware of the conflicts. + // This will be posted in a comment. + fileRes.notice = { + file: f, + message: + 'This file had merge conflicts. Please check the proposed changes carefully!', + }; + } + res.push(fileRes); + } + for (const f of coerceArray(status.deleted)) { + res.push({ + file: { + type: 'deletion', + path: f, + }, + }); + } + return res; +} diff --git a/lib/modules/manager/copier/extract.spec.ts b/lib/modules/manager/copier/extract.spec.ts new file mode 100644 index 00000000000000..d0bc17dc95d998 --- /dev/null +++ b/lib/modules/manager/copier/extract.spec.ts @@ -0,0 +1,58 @@ +import { extractPackageFile } from '.'; + +describe('modules/manager/copier/extract', () => { + describe('extractPackageFile()', () => { + it('extracts repository and version from .copier-answers.yml', () => { + const content = ` + _commit: v1.0.0 + _src_path: https://github.com/username/template-repo + `; + const result = extractPackageFile(content); + expect(result).toEqual({ + deps: [ + { + depName: 'https://github.com/username/template-repo', + packageName: 'https://github.com/username/template-repo', + currentValue: 'v1.0.0', + datasource: 'git-tags', + depType: 'template', + }, + ], + }); + }); + + it('returns null for invalid .copier-answers.yml', () => { + const content = ` + not_valid: + key: value + `; + const result = extractPackageFile(content); + expect(result).toBeNull(); + }); + + it('returns null for invalid _src_path', () => { + const content = ` + _commit: v1.0.0 + _src_path: notaurl + `; + const result = extractPackageFile(content); + expect(result).toBeNull(); + }); + + it('returns null for missing _commit field', () => { + const content = ` + _src_path: https://github.com/username/template-repo + `; + const result = extractPackageFile(content); + expect(result).toBeNull(); + }); + + it('returns null for missing _src_path field', () => { + const content = ` + _commit: v1.0.0 + `; + const result = extractPackageFile(content); + expect(result).toBeNull(); + }); + }); +}); diff --git a/lib/modules/manager/copier/extract.ts b/lib/modules/manager/copier/extract.ts new file mode 100644 index 00000000000000..bb58c4e43ab649 --- /dev/null +++ b/lib/modules/manager/copier/extract.ts @@ -0,0 +1,31 @@ +import { logger } from '../../../logger'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import type { PackageDependency, PackageFileContent } from '../types'; +import { CopierAnswersFile } from './schema'; + +export function extractPackageFile( + content: string, + packageFile?: string, +): PackageFileContent | null { + let parsed: CopierAnswersFile; + try { + parsed = CopierAnswersFile.parse(content); + } catch (err) { + logger.debug({ err, packageFile }, `Parsing Copier answers YAML failed`); + return null; + } + + const deps: PackageDependency[] = [ + { + datasource: GitTagsDatasource.id, + depName: parsed._src_path, + packageName: parsed._src_path, + depType: 'template', + currentValue: parsed._commit, + }, + ]; + + return { + deps, + }; +} diff --git a/lib/modules/manager/copier/index.ts b/lib/modules/manager/copier/index.ts new file mode 100644 index 00000000000000..a10f78f09c5d51 --- /dev/null +++ b/lib/modules/manager/copier/index.ts @@ -0,0 +1,12 @@ +import { GitTagsDatasource } from '../../datasource/git-tags'; +import * as pep440 from '../../versioning/pep440'; +export { updateArtifacts } from './artifacts'; +export { extractPackageFile } from './extract'; +export { updateDependency } from './update'; + +export const defaultConfig = { + fileMatch: ['(^|/)\\.copier-answers(\\..+)?\\.ya?ml'], + versioning: pep440.id, +}; + +export const supportedDatasources = [GitTagsDatasource.id]; diff --git a/lib/modules/manager/copier/readme.md b/lib/modules/manager/copier/readme.md new file mode 100644 index 00000000000000..124db48a54c245 --- /dev/null +++ b/lib/modules/manager/copier/readme.md @@ -0,0 +1,7 @@ +Keeps Copier templates up to date. +Supports multiple `.copier-answers(...).y(a)ml` files in a single repository. +You can customize how Copier is invoked using the repository-scoped configuration named [copierOptions](/configuration-options/#copieroptions). +If a template requires unsafe features, Copier must be invoked with the `--trust` flag. +This can be enabled in the self-hosted configuration via [copierTrust](/self-hosted-configuration/#copiertrust). + +If you need to change the versioning format, read the [versioning](../../versioning/index.md) documentation to learn more. diff --git a/lib/modules/manager/copier/schema.ts b/lib/modules/manager/copier/schema.ts new file mode 100644 index 00000000000000..27e0db3459ff27 --- /dev/null +++ b/lib/modules/manager/copier/schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Yaml } from '../../../util/schema-utils'; + +export const CopierAnswersFile = Yaml.pipe( + z.object({ + _commit: z.string(), + _src_path: z.string().url(), + }), +); + +export type CopierAnswersFile = z.infer; diff --git a/lib/modules/manager/copier/update.spec.ts b/lib/modules/manager/copier/update.spec.ts new file mode 100644 index 00000000000000..29c1c344484349 --- /dev/null +++ b/lib/modules/manager/copier/update.spec.ts @@ -0,0 +1,25 @@ +import { codeBlock } from 'common-tags'; +import { updateDependency } from '.'; + +describe('modules/manager/copier/update', () => { + describe('updateDependency', () => { + it('should append a new marking line at the end to trigger the artifact update', () => { + const fileContent = codeBlock` + _src_path: https://foo.bar/baz/quux + _commit: 1.0.0 + `; + const ret = updateDependency({ fileContent, upgrade: {} }); + expect(ret).toBe(`${fileContent}\n#copier updated`); + }); + + it('should not update again if the new line has been appended', () => { + const fileContent = codeBlock` + _src_path: https://foo.bar/baz/quux + _commit: 1.0.0 + #copier updated + `; + const ret = updateDependency({ fileContent, upgrade: {} }); + expect(ret).toBe(fileContent); + }); + }); +}); diff --git a/lib/modules/manager/copier/update.ts b/lib/modules/manager/copier/update.ts new file mode 100644 index 00000000000000..3de43be3a739cc --- /dev/null +++ b/lib/modules/manager/copier/update.ts @@ -0,0 +1,22 @@ +import { logger } from '../../../logger'; +import type { UpdateDependencyConfig } from '../types'; + +const updateLine = '#copier updated'; + +/** + * updateDependency appends a comment line once. + * This is only for the purpose of triggering the artifact update. + * Copier needs to update its answers file itself. + */ +export function updateDependency({ + fileContent, + upgrade, +}: UpdateDependencyConfig): string | null { + logger.trace({ upgrade }, `copier.updateDependency()`); + if (!fileContent.endsWith(updateLine)) { + logger.debug(`append update line to the fileContent if it hasn't been`); + return `${fileContent}\n${updateLine}`; + } + + return fileContent; +} diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index b6a93703f254f4..3c791f8d71f359 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -1,5 +1,6 @@ import type { ReleaseType } from 'semver'; import type { + CopierOptions, MatchStringsStrategy, UpdateType, UserEnv, @@ -41,6 +42,8 @@ export interface UpdateArtifactsConfig { registryAliases?: Record; lockFiles?: string[]; env?: UserEnv; + copierOptions?: CopierOptions; + copierTrust?: boolean; } export interface RangeConfig> extends ManagerData { From 6714ac10da3c6fe511125dd106f660ed06c355a7 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Sun, 26 May 2024 11:01:43 +0200 Subject: [PATCH 02/11] Replace `copierTrust` with existing `allowScripts` --- docs/usage/configuration-options.md | 2 +- docs/usage/self-hosted-configuration.md | 4 ---- lib/config/options/index.ts | 8 -------- lib/config/types.ts | 1 - lib/modules/manager/copier/artifacts.spec.ts | 6 +++--- lib/modules/manager/copier/artifacts.ts | 2 +- lib/modules/manager/copier/readme.md | 2 +- lib/modules/manager/types.ts | 2 +- 8 files changed, 7 insertions(+), 20 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 8ad38c5ea258fe..504da40e2ba37a 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -682,7 +682,7 @@ This switch instructs the manager to use `copier recopy --skip-answered --defaul ### skipTasks Instruct Copier to skip template tasks execution. -Mind that they are only executed if the admin has enabled `copierTrust` in the self-hosted configuration. +Mind that they are only executed if the admin has enabled `allowScripts` in the self-hosted configuration. ### data diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 4a485060584daa..0a0bd42b6bb530 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -355,10 +355,6 @@ This directory is used to cache downloads when `binarySource=docker` or `binaryS Use this option if you need such downloads to be stored outside of Renovate's regular cache directory (`cacheDir`). -## copierTrust - -When using the `copier` manager, whether to instruct it to allow templates with unsafe features (Jinja extensions, migrations, tasks). - ## customEnvVariables This configuration will be applied after all other environment variables so you can use it to override defaults. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index b06da35a4cc32d..1b167eac6161ed 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -3152,14 +3152,6 @@ const options: RenovateOptions[] = [ subType: 'string', default: [], }, - { - name: 'copierTrust', - description: - 'Allow templates with unsafe features (Jinja extensions, migrations, tasks).', - type: 'boolean', - default: false, - globalOnly: true, - }, ]; export function getOptions(): RenovateOptions[] { diff --git a/lib/config/types.ts b/lib/config/types.ts index 14bdacb9dac93d..4cdd8165fc28ce 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -141,7 +141,6 @@ export interface RepoGlobalConfig { cacheHardTtlMinutes?: number; cacheTtlOverride?: Record; containerbaseDir?: string; - copierTrust?: boolean; customEnvVariables?: Record; dockerChildPrefix?: string; dockerCliOptions?: string; diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index cfd0a87ceaebc6..75c5241b5898b7 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -20,7 +20,7 @@ const config: UpdateArtifactsConfig = { skip: [], exclude: [], }, - copierTrust: false, + allowScripts: false, }; const upgrades: Upgrade[] = [ @@ -121,12 +121,12 @@ describe('modules/manager/copier/artifacts', () => { ]); }); - it('includes --trust when copierTrust is true', async () => { + it('includes --trust when allowScripts is true', async () => { const execSnapshots = mockExecAll(); const trustConfig = { ...config, - copierTrust: true, + allowScripts: true, }; await updateArtifacts({ diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index 14a24840bdbca5..b9ee4db0da546f 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -37,7 +37,7 @@ function buildCommand( } else { command.push('update', ...DEFAULT_COMMAND_OPTIONS); } - if (config?.copierTrust) { + if (config?.allowScripts) { command.push('--trust'); } for (const [opt, param] of Object.entries(boolOpts)) { diff --git a/lib/modules/manager/copier/readme.md b/lib/modules/manager/copier/readme.md index 124db48a54c245..fbdeb076b33c8d 100644 --- a/lib/modules/manager/copier/readme.md +++ b/lib/modules/manager/copier/readme.md @@ -2,6 +2,6 @@ Keeps Copier templates up to date. Supports multiple `.copier-answers(...).y(a)ml` files in a single repository. You can customize how Copier is invoked using the repository-scoped configuration named [copierOptions](/configuration-options/#copieroptions). If a template requires unsafe features, Copier must be invoked with the `--trust` flag. -This can be enabled in the self-hosted configuration via [copierTrust](/self-hosted-configuration/#copiertrust). +This can be enabled in the [self-hosted configuration](../../../self-hosted-configuration.md) via `allowScripts`. If you need to change the versioning format, read the [versioning](../../versioning/index.md) documentation to learn more. diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index 3c791f8d71f359..c52e1a9fc9ffa4 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -43,7 +43,7 @@ export interface UpdateArtifactsConfig { lockFiles?: string[]; env?: UserEnv; copierOptions?: CopierOptions; - copierTrust?: boolean; + allowScripts?: boolean; } export interface RangeConfig> extends ManagerData { From 3c6c6d5c6f76b6e85946e4ad018c68aeb571bfa5 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Sun, 26 May 2024 11:11:29 +0200 Subject: [PATCH 03/11] Remove dataFile and skipTasks options --- docs/usage/configuration-options.md | 10 ---- lib/config/options/index.ts | 15 ----- lib/config/types.ts | 6 +- lib/modules/manager/copier/artifacts.spec.ts | 61 +------------------- lib/modules/manager/copier/artifacts.ts | 27 --------- 5 files changed, 5 insertions(+), 114 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 504da40e2ba37a..289dc134c43068 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -679,20 +679,10 @@ By default, the manager uses `copier update --skip-answered --defaults` to updat Merge conflicts are warned about, but still result in a PR. This switch instructs the manager to use `copier recopy --skip-answered --defaults --overwrite` instead, which disables the smart update algorithm and overwrites any customizations. -### skipTasks - -Instruct Copier to skip template tasks execution. -Mind that they are only executed if the admin has enabled `allowScripts` in the self-hosted configuration. - ### data A mapping of arbitrary template variable names to their (string) values to pass to Copier. -### dataFile - -The relative path of a YAML file to load Copier template variables from. -Must be part of the repository. - ### skip An array of paths (globs) to skip updates from the Copier template on if they exist already in the respository. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 1b167eac6161ed..84b6f936bf4e01 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -3110,13 +3110,6 @@ const options: RenovateOptions[] = [ type: 'boolean', default: false, }, - { - name: 'skipTasks', - description: 'Skip Copier template tasks execution.', - parents: ['copierOptions'], - type: 'boolean', - default: false, - }, { name: 'data', description: 'Pass arbitrary Copier template variables with their values.', @@ -3126,14 +3119,6 @@ const options: RenovateOptions[] = [ mergeable: true, default: {}, }, - { - name: 'dataFile', - description: - 'An optional relative path of a YAML file to load Copier template variables from.', - parents: ['copierOptions'], - type: 'string', - default: '', - }, { name: 'skip', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 4cdd8165fc28ce..8b33572a344688 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -581,10 +581,8 @@ export interface ValidationResult { } export interface CopierOptions extends Record { - recopy?: boolean; - skipTasks?: boolean; data?: Record; - dataFile?: string; - skip?: string[]; exclude?: string[]; + recopy?: boolean; + skip?: string[]; } diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index 75c5241b5898b7..bd53294979085c 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -13,12 +13,10 @@ jest.mock('../../../util/fs'); const config: UpdateArtifactsConfig = { copierOptions: { - recopy: false, - skipTasks: false, data: {}, - dataFile: '', - skip: [], exclude: [], + recopy: false, + skip: [], }, allowScripts: false, }; @@ -154,7 +152,6 @@ describe('modules/manager/copier/artifacts', () => { variable1: 'value1', variable2: 'value2', }, - dataFile: 'foo/bar.yaml', skip: ['file1.txt', 'file2.txt'], exclude: ['*.tmp', 'backup/*'], }, @@ -169,63 +166,11 @@ describe('modules/manager/copier/artifacts', () => { expect(execSnapshots).toMatchObject([ { - cmd: "copier update --skip-answered --defaults --data-file foo/bar.yaml --data variable1=value1 --data variable2=value2 --skip file1.txt --skip file2.txt --exclude '*.tmp' --exclude 'backup/*' --answers-file .copier-answers.yml --vcs-ref 1.1.0", - }, - ]); - }); - - it('handles boolean options correctly', async () => { - const execSnapshots = mockExecAll(); - - const optionsConfig = { - ...config, - copierOptions: { - skipTasks: true, - }, - }; - - await updateArtifacts({ - packageFileName: '.copier-answers.yml', - updatedDeps: upgrades, - newPackageFileContent: '', - config: optionsConfig, - }); - - expect(execSnapshots).toMatchObject([ - { - cmd: 'copier update --skip-answered --defaults --skip-tasks --answers-file .copier-answers.yml --vcs-ref 1.1.0', + cmd: "copier update --skip-answered --defaults --data variable1=value1 --data variable2=value2 --skip file1.txt --skip file2.txt --exclude '*.tmp' --exclude 'backup/*' --answers-file .copier-answers.yml --vcs-ref 1.1.0", }, ]); }); - it('does not allow a dataFile from outside the repository', async () => { - const execSnapshots = mockExecAll(); - - const optionsConfig = { - ...config, - copierOptions: { - dataFile: '/foo/bar.yml', - }, - }; - - const result = await updateArtifacts({ - packageFileName: '.copier-answers.yml', - updatedDeps: upgrades, - newPackageFileContent: '', - config: optionsConfig, - }); - - expect(result).toEqual([ - { - artifactError: { - lockFile: '.copier-answers.yml', - stderr: 'copierOptions.dataFile is not part of the repository', - }, - }, - ]); - expect(execSnapshots).toMatchObject([]); - }); - it('supports recopy instead of update', async () => { const execSnapshots = mockExecAll(); diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index b9ee4db0da546f..3111791df54188 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -1,10 +1,8 @@ import { quote } from 'shlex'; -import { FILE_ACCESS_VIOLATION_ERROR } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import { coerceArray } from '../../../util/array'; import { exec } from '../../../util/exec'; import { readLocalFile } from '../../../util/fs'; -import { ensureLocalPath } from '../../../util/fs/util'; import { getRepoStatus } from '../../../util/git'; import type { UpdateArtifact, @@ -12,13 +10,8 @@ import type { UpdateArtifactsResult, } from '../types'; -type CopierBoolOpt = 'skipTasks'; type CopierListOpt = 'skip' | 'exclude'; -const boolOpts: Record = { - skipTasks: '--skip-tasks', -}; - const listOpts: Record = { skip: '--skip', exclude: '--exclude', @@ -40,26 +33,6 @@ function buildCommand( if (config?.allowScripts) { command.push('--trust'); } - for (const [opt, param] of Object.entries(boolOpts)) { - if (config.copierOptions?.[opt]) { - command.push(param); - } - } - if (config.copierOptions?.dataFile) { - try { - ensureLocalPath(config.copierOptions.dataFile); - } catch (err) { - if (err.message === FILE_ACCESS_VIOLATION_ERROR) { - throw new Error( - 'copierOptions.dataFile is not part of the repository', - { cause: err }, - ); - } - // istanbul ignore next - throw err; - } - command.push('--data-file', quote(config.copierOptions.dataFile)); - } for (const [key, value] of Object.entries(config.copierOptions?.data ?? {})) { command.push('--data', quote(`${key}=${value}`)); } From dd13a44527e14360b05cd6507b93f59e68cf76dc Mon Sep 17 00:00:00 2001 From: jeanluc Date: Sun, 26 May 2024 11:36:12 +0200 Subject: [PATCH 04/11] Remove removed options from defaults as well --- lib/config/options/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 84b6f936bf4e01..c4fd02afcf78ea 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -3095,9 +3095,7 @@ const options: RenovateOptions[] = [ type: 'object', default: { recopy: false, - skipTasks: false, data: {}, - dataFile: '', skip: [], exclude: [], }, From 2277d5a8acec4cd2718773dcbd6d3f012eb6de89 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Sun, 26 May 2024 11:56:24 +0200 Subject: [PATCH 05/11] Don't catch errors when building command ... since we don't expect any now. --- lib/modules/manager/copier/artifacts.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index 3111791df54188..c92fcae5314826 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -85,14 +85,7 @@ export async function updateArtifacts({ ); } - let command: string; - try { - command = buildCommand(config, packageFileName, newVersion); - } catch (err) { - logger.error({ err }, `Failed to build copier command: ${err.message}`); - return artifactError(packageFileName, err.message); - } - + const command = buildCommand(config, packageFileName, newVersion); try { await exec(command); } catch (err) { From b9b32300966141511d5788bf0aa2204b791698fa Mon Sep 17 00:00:00 2001 From: jeanluc Date: Wed, 26 Jun 2024 18:32:59 +0200 Subject: [PATCH 06/11] Remove all Copier-specific configuration --- docs/usage/configuration-options.md | 24 -------- lib/config/options/index.ts | 47 --------------- lib/config/types.ts | 11 +--- lib/modules/manager/copier/artifacts.spec.ts | 60 -------------------- lib/modules/manager/copier/artifacts.ts | 22 +------ lib/modules/manager/copier/readme.md | 1 - lib/modules/manager/types.ts | 2 - 7 files changed, 2 insertions(+), 165 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 289dc134c43068..2f573e4ea19637 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -667,30 +667,6 @@ When using with `npm`, we recommend you: - Use `constraintsFiltering` on `dependencies`, not `devDependencies` (usually you do not need to be strict about development dependencies) - Do _not_ enable `rollbackPrs` at the same time (otherwise your _current_ version may be rolled back if it's incompatible) -## copierOptions - -Configure how the `copier` manager invokes the CLI tool. - -The `copierOptions` configuration consists of six fields: - -### recopy - -By default, the manager uses `copier update --skip-answered --defaults` to update a templated project. -Merge conflicts are warned about, but still result in a PR. -This switch instructs the manager to use `copier recopy --skip-answered --defaults --overwrite` instead, which disables the smart update algorithm and overwrites any customizations. - -### data - -A mapping of arbitrary template variable names to their (string) values to pass to Copier. - -### skip - -An array of paths (globs) to skip updates from the Copier template on if they exist already in the respository. - -### exclude - -An array of paths (globs) that should not be copied from the Copier template. - ## customDatasources Use `customDatasources` to fetch releases from APIs or statically hosted sites and Renovate has no own datasource. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index c4fd02afcf78ea..812c4b53948be4 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -3088,53 +3088,6 @@ const options: RenovateOptions[] = [ default: 90, globalOnly: true, }, - { - name: 'copierOptions', - description: - 'Configure how Copier is invoked. Unsafe features (--trust) can only be enabled in the self-hosted configuration.', - type: 'object', - default: { - recopy: false, - data: {}, - skip: [], - exclude: [], - }, - }, - { - name: 'recopy', - description: - 'Request Copier to recopy a template instead of using the smart update algorithm.', - parents: ['copierOptions'], - type: 'boolean', - default: false, - }, - { - name: 'data', - description: 'Pass arbitrary Copier template variables with their values.', - parents: ['copierOptions'], - type: 'object', - freeChoice: true, - mergeable: true, - default: {}, - }, - { - name: 'skip', - description: - 'An array of paths/globs to skip during Copier template rendering if they exist already.', - parents: ['copierOptions'], - type: 'array', - subType: 'string', - default: [], - }, - { - name: 'exclude', - description: - 'An array of paths/globs that should never be copied during Copier template rendering.', - parents: ['copierOptions'], - type: 'array', - subType: 'string', - default: [], - }, ]; export function getOptions(): RenovateOptions[] { diff --git a/lib/config/types.ts b/lib/config/types.ts index 8b33572a344688..2129fd9b7ecc2b 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -266,7 +266,6 @@ export interface RenovateConfig prConcurrentLimit?: number; prHourlyLimit?: number; forkModeDisallowMaintainerEdits?: boolean; - copierOptions?: CopierOptions; defaultRegistryUrls?: string[]; registryUrls?: string[] | null; @@ -402,8 +401,7 @@ export type AllowedParents = | 'hostRules' | 'postUpgradeTasks' | 'packageRules' - | 'logLevelRemap' - | 'copierOptions'; + | 'logLevelRemap'; export interface RenovateOptionBase { /** * If true, the option can only be configured by people with access to the Renovate instance. @@ -579,10 +577,3 @@ export interface ValidationResult { errors: ValidationMessage[]; warnings: ValidationMessage[]; } - -export interface CopierOptions extends Record { - data?: Record; - exclude?: string[]; - recopy?: boolean; - skip?: string[]; -} diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index bd53294979085c..87825b71e95dd6 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -12,12 +12,6 @@ jest.mock('../../../util/git'); jest.mock('../../../util/fs'); const config: UpdateArtifactsConfig = { - copierOptions: { - data: {}, - exclude: [], - recopy: false, - skip: [], - }, allowScripts: false, }; @@ -141,60 +135,6 @@ describe('modules/manager/copier/artifacts', () => { ]); }); - it('handles data and list options correctly', async () => { - const execSnapshots = mockExecAll(); - - const optionsConfig = { - ...config, - copierOptions: { - ...config.copierOptions, - data: { - variable1: 'value1', - variable2: 'value2', - }, - skip: ['file1.txt', 'file2.txt'], - exclude: ['*.tmp', 'backup/*'], - }, - }; - - await updateArtifacts({ - packageFileName: '.copier-answers.yml', - updatedDeps: upgrades, - newPackageFileContent: '', - config: optionsConfig, - }); - - expect(execSnapshots).toMatchObject([ - { - cmd: "copier update --skip-answered --defaults --data variable1=value1 --data variable2=value2 --skip file1.txt --skip file2.txt --exclude '*.tmp' --exclude 'backup/*' --answers-file .copier-answers.yml --vcs-ref 1.1.0", - }, - ]); - }); - - it('supports recopy instead of update', async () => { - const execSnapshots = mockExecAll(); - - const optionsConfig = { - ...config, - copierOptions: { - recopy: true, - }, - }; - - await updateArtifacts({ - packageFileName: '.copier-answers.yml', - updatedDeps: upgrades, - newPackageFileContent: '', - config: optionsConfig, - }); - - expect(execSnapshots).toMatchObject([ - { - cmd: 'copier recopy --skip-answered --defaults --overwrite --answers-file .copier-answers.yml --vcs-ref 1.1.0', - }, - ]); - }); - it('handles exec errors', async () => { mockExecAll(new Error('exec exception')); const result = await updateArtifacts({ diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index c92fcae5314826..a792148ef3f080 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -10,13 +10,6 @@ import type { UpdateArtifactsResult, } from '../types'; -type CopierListOpt = 'skip' | 'exclude'; - -const listOpts: Record = { - skip: '--skip', - exclude: '--exclude', -}; - const DEFAULT_COMMAND_OPTIONS = ['--skip-answered', '--defaults']; function buildCommand( @@ -24,23 +17,10 @@ function buildCommand( packageFileName: string, newVersion: string, ): string { - const command = ['copier']; - if (config?.copierOptions?.recopy) { - command.push('recopy', ...DEFAULT_COMMAND_OPTIONS, '--overwrite'); - } else { - command.push('update', ...DEFAULT_COMMAND_OPTIONS); - } + const command = ['copier', 'update', ...DEFAULT_COMMAND_OPTIONS]; if (config?.allowScripts) { command.push('--trust'); } - for (const [key, value] of Object.entries(config.copierOptions?.data ?? {})) { - command.push('--data', quote(`${key}=${value}`)); - } - for (const [opt, param] of Object.entries(listOpts)) { - config.copierOptions?.[opt]?.forEach((item: string) => { - command.push(param, quote(item)); - }); - } command.push( '--answers-file', quote(packageFileName), diff --git a/lib/modules/manager/copier/readme.md b/lib/modules/manager/copier/readme.md index fbdeb076b33c8d..8243aa065b85d0 100644 --- a/lib/modules/manager/copier/readme.md +++ b/lib/modules/manager/copier/readme.md @@ -1,6 +1,5 @@ Keeps Copier templates up to date. Supports multiple `.copier-answers(...).y(a)ml` files in a single repository. -You can customize how Copier is invoked using the repository-scoped configuration named [copierOptions](/configuration-options/#copieroptions). If a template requires unsafe features, Copier must be invoked with the `--trust` flag. This can be enabled in the [self-hosted configuration](../../../self-hosted-configuration.md) via `allowScripts`. diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index c52e1a9fc9ffa4..37224f2d7f568b 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -1,6 +1,5 @@ import type { ReleaseType } from 'semver'; import type { - CopierOptions, MatchStringsStrategy, UpdateType, UserEnv, @@ -42,7 +41,6 @@ export interface UpdateArtifactsConfig { registryAliases?: Record; lockFiles?: string[]; env?: UserEnv; - copierOptions?: CopierOptions; allowScripts?: boolean; } From f739dc2cab0d38b449ae0e808f675787a5a43b5c Mon Sep 17 00:00:00 2001 From: jeanluc Date: Mon, 1 Jul 2024 12:33:00 +0200 Subject: [PATCH 07/11] Apply review comments * correctly inquire for `allowScripts` * respect `ignoreScripts` * only log to debug --- docs/usage/configuration-options.md | 2 +- lib/config/options/index.ts | 2 +- lib/modules/manager/copier/artifacts.spec.ts | 28 +++++++++++++++++--- lib/modules/manager/copier/artifacts.ts | 7 ++--- lib/modules/manager/copier/readme.md | 3 ++- lib/modules/manager/types.ts | 1 - 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 2f573e4ea19637..136d02885a70be 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2096,7 +2096,7 @@ In the case that a user is automatically added as reviewer (such as Renovate App ## ignoreScripts -Applicable for npm and Composer only for now. Set this to `true` if running scripts causes problems. +Applicable for npm, Composer and Copier only for now. Set this to `true` if running scripts causes problems. ## ignoreTests diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 812c4b53948be4..0e613a27c88443 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -893,7 +893,7 @@ const options: RenovateOptions[] = [ 'Set this to `false` if `allowScripts=true` and you wish to run scripts when updating lock files.', type: 'boolean', default: true, - supportedManagers: ['npm', 'composer'], + supportedManagers: ['npm', 'composer', 'copier'], }, { name: 'platform', diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index 87825b71e95dd6..665d31bd7bf27f 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -12,7 +12,7 @@ jest.mock('../../../util/git'); jest.mock('../../../util/fs'); const config: UpdateArtifactsConfig = { - allowScripts: false, + ignoreScripts: true, }; const upgrades: Upgrade[] = [ @@ -26,6 +26,7 @@ const upgrades: Upgrade[] = [ const adminConfig: RepoGlobalConfig = { localDir: join('/tmp/github/some/repo'), cacheDir: join('/tmp/cache'), + allowScripts: false, }; describe('modules/manager/copier/artifacts', () => { @@ -113,12 +114,13 @@ describe('modules/manager/copier/artifacts', () => { ]); }); - it('includes --trust when allowScripts is true', async () => { + it('includes --trust when allowScripts is true and ignoreScripts is false', async () => { + GlobalConfig.set({ ...adminConfig, allowScripts: true }); const execSnapshots = mockExecAll(); const trustConfig = { ...config, - allowScripts: true, + ignoreScripts: false, }; await updateArtifacts({ @@ -135,6 +137,24 @@ describe('modules/manager/copier/artifacts', () => { ]); }); + it('does not include --trust when ignoreScripts is true', async () => { + GlobalConfig.set({ ...adminConfig, allowScripts: true }); + const execSnapshots = mockExecAll(); + + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config, + }); + + expect(execSnapshots).toMatchObject([ + { + cmd: 'copier update --skip-answered --defaults --answers-file .copier-answers.yml --vcs-ref 1.1.0', + }, + ]); + }); + it('handles exec errors', async () => { mockExecAll(new Error('exec exception')); const result = await updateArtifacts({ @@ -248,7 +268,7 @@ describe('modules/manager/copier/artifacts', () => { newPackageFileContent: '', config, }); - expect(logger.warn).toHaveBeenCalledWith( + expect(logger.debug).toHaveBeenCalledWith( { depName: 'https://github.com/foo/bar', packageFileName: '.copier-answers.yml', diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index a792148ef3f080..e9c02dfc00120b 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -1,4 +1,5 @@ import { quote } from 'shlex'; +import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; import { coerceArray } from '../../../util/array'; import { exec } from '../../../util/exec'; @@ -18,7 +19,7 @@ function buildCommand( newVersion: string, ): string { const command = ['copier', 'update', ...DEFAULT_COMMAND_OPTIONS]; - if (config?.allowScripts) { + if (GlobalConfig.get('allowScripts') && !config.ignoreScripts) { command.push('--trust'); } command.push( @@ -69,7 +70,7 @@ export async function updateArtifacts({ try { await exec(command); } catch (err) { - logger.error({ err }, `Failed to update copier template: ${err.message}`); + logger.debug({ err }, `Failed to update copier template: ${err.message}`); return artifactError(packageFileName, err.message); } @@ -85,7 +86,7 @@ export async function updateArtifacts({ `Updating the Copier template yielded ${status.conflicted.length} merge conflicts. ` + 'Please check the proposed changes carefully! Conflicting files:\n * ' + status.conflicted.join('\n * '); - logger.warn({ packageFileName, depName: updatedDeps[0]?.depName }, msg); + logger.debug({ packageFileName, depName: updatedDeps[0]?.depName }, msg); } const res: UpdateArtifactsResult[] = []; diff --git a/lib/modules/manager/copier/readme.md b/lib/modules/manager/copier/readme.md index 8243aa065b85d0..53db498735db24 100644 --- a/lib/modules/manager/copier/readme.md +++ b/lib/modules/manager/copier/readme.md @@ -1,6 +1,7 @@ Keeps Copier templates up to date. Supports multiple `.copier-answers(...).y(a)ml` files in a single repository. If a template requires unsafe features, Copier must be invoked with the `--trust` flag. -This can be enabled in the [self-hosted configuration](../../../self-hosted-configuration.md) via `allowScripts`. +Enabling this behavior must be allowed in the [self-hosted configuration](../../../self-hosted-configuration.md) via `allowScripts`. +Actually enable it in the [configuration](../../../configuration-options.md) by setting `ignoreScripts` to `false`. If you need to change the versioning format, read the [versioning](../../versioning/index.md) documentation to learn more. diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index 37224f2d7f568b..b6a93703f254f4 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -41,7 +41,6 @@ export interface UpdateArtifactsConfig { registryAliases?: Record; lockFiles?: string[]; env?: UserEnv; - allowScripts?: boolean; } export interface RangeConfig> extends ManagerData { From a24c47d7729ec9cf743da765fd479fa14b59aad8 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Mon, 1 Jul 2024 14:31:20 +0200 Subject: [PATCH 08/11] Support dynamic install --- lib/modules/manager/copier/artifacts.spec.ts | 57 +++++++++++++++++++- lib/modules/manager/copier/artifacts.ts | 46 +++++++++++++++- lib/util/exec/containerbase.ts | 5 ++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index 665d31bd7bf27f..7bb6eda18439a0 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -1,15 +1,22 @@ +import { mockDeep } from 'jest-mock-extended'; import { join } from 'upath'; import { mockExecAll } from '../../../../test/exec-util'; -import { fs, git, partial } from '../../../../test/util'; +import { fs, git, mocked, partial } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import { logger } from '../../../logger'; import type { StatusResult } from '../../../util/git/types'; +import * as _datasource from '../../datasource'; import type { UpdateArtifactsConfig, Upgrade } from '../types'; import { updateArtifacts } from '.'; +const datasource = mocked(_datasource); + jest.mock('../../../util/git'); jest.mock('../../../util/fs'); +jest.mock('../../datasource', () => mockDeep()); + +process.env.CONTAINERBASE = 'true'; const config: UpdateArtifactsConfig = { ignoreScripts: true, @@ -26,6 +33,7 @@ const upgrades: Upgrade[] = [ const adminConfig: RepoGlobalConfig = { localDir: join('/tmp/github/some/repo'), cacheDir: join('/tmp/cache'), + containerbaseDir: join('/tmp/renovate/cache/containerbase'), allowScripts: false, }; @@ -114,6 +122,53 @@ describe('modules/manager/copier/artifacts', () => { ]); }); + it.each([ + [null, null], + ['3.11.3', null], + [null, '9.1.0'], + ['3.11.3', '9.1.0'], + ])( + 'supports dynamic install with constraints python=%o copier=%o', + async (pythonConstraint, copierConstraint) => { + GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + const constraintConfig = { + python: pythonConstraint ?? '', + copier: copierConstraint ?? '', + }; + if (!pythonConstraint) { + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '3.12.4' }], + }); + } + if (!copierConstraint) { + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '9.2.0' }], + }); + } + const execSnapshots = mockExecAll(); + + expect( + await updateArtifacts({ + packageFileName: '.copier-answers.yml', + updatedDeps: upgrades, + newPackageFileContent: '', + config: { + ...config, + constraints: constraintConfig, + }, + }), + ).not.toBeNull(); + + expect(execSnapshots).toMatchObject([ + { cmd: 'install-tool python ' + (pythonConstraint ?? '3.12.4') }, + { cmd: 'install-tool copier ' + (copierConstraint ?? '9.2.0') }, + { + cmd: 'copier update --skip-answered --defaults --answers-file .copier-answers.yml --vcs-ref 1.1.0', + }, + ]); + }, + ); + it('includes --trust when allowScripts is true and ignoreScripts is false', async () => { GlobalConfig.set({ ...adminConfig, allowScripts: true }); const execSnapshots = mockExecAll(); diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index e9c02dfc00120b..97f1c919ad3580 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -1,8 +1,10 @@ +import is from '@sindresorhus/is'; import { quote } from 'shlex'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; import { coerceArray } from '../../../util/array'; import { exec } from '../../../util/exec'; +import type { ExecOptions } from '../../../util/exec/types'; import { readLocalFile } from '../../../util/fs'; import { getRepoStatus } from '../../../util/git'; import type { @@ -45,6 +47,32 @@ function artifactError( ]; } +function getPythonVersionConstraint( + config: UpdateArtifactsConfig, +): string | undefined | null { + const { constraints = {} } = config; + const { python } = constraints; + + if (python) { + logger.debug('Using python constraint from config'); + return python; + } + + return undefined; +} + +function getCopierVersionConstraint(config: UpdateArtifactsConfig): string { + const { constraints = {} } = config; + const { copier } = constraints; + + if (is.string(copier)) { + logger.debug('Using copier constraint from config'); + return copier; + } + + return ''; +} + export async function updateArtifacts({ packageFileName, updatedDeps, @@ -67,8 +95,24 @@ export async function updateArtifacts({ } const command = buildCommand(config, packageFileName, newVersion); + const pythonConstraint = getPythonVersionConstraint(config); + const copierConstraint = getCopierVersionConstraint(config); + const execOptions: ExecOptions = { + docker: {}, + userConfiguredEnv: config.env, + toolConstraints: [ + { + toolName: 'python', + constraint: pythonConstraint, + }, + { + toolName: 'copier', + constraint: copierConstraint, + }, + ], + }; try { - await exec(command); + await exec(command, execOptions); } catch (err) { logger.debug({ err }, `Failed to update copier template: ${err.message}`); return artifactError(packageFileName, err.message); diff --git a/lib/util/exec/containerbase.ts b/lib/util/exec/containerbase.ts index 1b5585effc6410..04540874e4ba5e 100644 --- a/lib/util/exec/containerbase.ts +++ b/lib/util/exec/containerbase.ts @@ -38,6 +38,11 @@ const allToolConfig: Record = { packageName: 'composer/composer', versioning: composerVersioningId, }, + copier: { + datasource: 'pypi', + packageName: 'copier', + versioning: pep440VersioningId, + }, corepack: { datasource: 'npm', packageName: 'corepack', From ac185623d6707a93d289e24ecb086e8d1853a954 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Mon, 5 Aug 2024 11:23:01 +0200 Subject: [PATCH 09/11] Account for renames In case of simple renames, `git status` might detect it. It's not reported in the other fields, so we need to account for that. --- lib/modules/manager/copier/artifacts.spec.ts | 18 ++++++++++++++++++ lib/modules/manager/copier/artifacts.ts | 20 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index 7bb6eda18439a0..37f6158621dd46 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -48,6 +48,7 @@ describe('modules/manager/copier/artifacts', () => { modified: ['.copier-answers.yml'], not_added: [], deleted: [], + renamed: [], }), ); }); @@ -238,6 +239,7 @@ describe('modules/manager/copier/artifacts', () => { modified: [], not_added: ['new_file.py'], deleted: ['old_file.py'], + renamed: [], }), ); @@ -260,6 +262,7 @@ describe('modules/manager/copier/artifacts', () => { modified: ['.copier-answers.yml'], not_added: ['new_file.py'], deleted: ['old_file.py'], + renamed: [{ from: 'renamed_old.py', to: 'renamed_new.py' }], }), ); @@ -267,6 +270,7 @@ describe('modules/manager/copier/artifacts', () => { '_src: https://github.com/foo/bar\n_commit: 1.1.0', ); fs.readLocalFile.mockResolvedValueOnce('new file contents'); + fs.readLocalFile.mockResolvedValueOnce('renamed file contents'); const result = await updateArtifacts({ packageFileName: '.copier-answers.yml', @@ -296,6 +300,19 @@ describe('modules/manager/copier/artifacts', () => { path: 'old_file.py', }, }, + { + file: { + type: 'deletion', + path: 'renamed_old.py', + }, + }, + { + file: { + type: 'addition', + path: 'renamed_new.py', + contents: 'renamed file contents', + }, + }, ]); }); @@ -308,6 +325,7 @@ describe('modules/manager/copier/artifacts', () => { modified: ['.copier-answers.yml'], not_added: ['new_file.py'], deleted: ['old_file.py'], + renamed: [], }), ); diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index 97f1c919ad3580..92971e2fa5d3db 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -2,7 +2,6 @@ import is from '@sindresorhus/is'; import { quote } from 'shlex'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; -import { coerceArray } from '../../../util/array'; import { exec } from '../../../util/exec'; import type { ExecOptions } from '../../../util/exec/types'; import { readLocalFile } from '../../../util/fs'; @@ -158,7 +157,7 @@ export async function updateArtifacts({ } res.push(fileRes); } - for (const f of coerceArray(status.deleted)) { + for (const f of status.deleted) { res.push({ file: { type: 'deletion', @@ -166,5 +165,22 @@ export async function updateArtifacts({ }, }); } + // `git status` might detect a rename, which is then not contained + // in not_added/deleted. Ensure we respect renames as well if they happen. + for (const f of status.renamed) { + res.push({ + file: { + type: 'deletion', + path: f.from, + }, + }); + res.push({ + file: { + type: 'addition', + path: f.to, + contents: await readLocalFile(f.to), + }, + }); + } return res; } From 62d191c3abeaf4ac933f54d091d32470c2bf3687 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Mon, 5 Aug 2024 13:10:22 +0200 Subject: [PATCH 10/11] Apply review comments --- lib/modules/manager/copier/artifacts.ts | 37 ++++--------------------- lib/modules/manager/copier/utils.ts | 31 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 lib/modules/manager/copier/utils.ts diff --git a/lib/modules/manager/copier/artifacts.ts b/lib/modules/manager/copier/artifacts.ts index 92971e2fa5d3db..1b6aa236cf9bfe 100644 --- a/lib/modules/manager/copier/artifacts.ts +++ b/lib/modules/manager/copier/artifacts.ts @@ -1,4 +1,3 @@ -import is from '@sindresorhus/is'; import { quote } from 'shlex'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; @@ -11,6 +10,10 @@ import type { UpdateArtifactsConfig, UpdateArtifactsResult, } from '../types'; +import { + getCopierVersionConstraint, + getPythonVersionConstraint, +} from './utils'; const DEFAULT_COMMAND_OPTIONS = ['--skip-answered', '--defaults']; @@ -46,32 +49,6 @@ function artifactError( ]; } -function getPythonVersionConstraint( - config: UpdateArtifactsConfig, -): string | undefined | null { - const { constraints = {} } = config; - const { python } = constraints; - - if (python) { - logger.debug('Using python constraint from config'); - return python; - } - - return undefined; -} - -function getCopierVersionConstraint(config: UpdateArtifactsConfig): string { - const { constraints = {} } = config; - const { copier } = constraints; - - if (is.string(copier)) { - logger.debug('Using copier constraint from config'); - return copier; - } - - return ''; -} - export async function updateArtifacts({ packageFileName, updatedDeps, @@ -94,19 +71,17 @@ export async function updateArtifacts({ } const command = buildCommand(config, packageFileName, newVersion); - const pythonConstraint = getPythonVersionConstraint(config); - const copierConstraint = getCopierVersionConstraint(config); const execOptions: ExecOptions = { docker: {}, userConfiguredEnv: config.env, toolConstraints: [ { toolName: 'python', - constraint: pythonConstraint, + constraint: getPythonVersionConstraint(config), }, { toolName: 'copier', - constraint: copierConstraint, + constraint: getCopierVersionConstraint(config), }, ], }; diff --git a/lib/modules/manager/copier/utils.ts b/lib/modules/manager/copier/utils.ts new file mode 100644 index 00000000000000..156507c0981bd0 --- /dev/null +++ b/lib/modules/manager/copier/utils.ts @@ -0,0 +1,31 @@ +import is from '@sindresorhus/is'; +import { logger } from '../../../logger'; +import type { UpdateArtifactsConfig } from '../types'; + +export function getPythonVersionConstraint( + config: UpdateArtifactsConfig, +): string | undefined | null { + const { constraints = {} } = config; + const { python } = constraints; + + if (is.nonEmptyString(python)) { + logger.debug('Using python constraint from config'); + return python; + } + + return undefined; +} + +export function getCopierVersionConstraint( + config: UpdateArtifactsConfig, +): string { + const { constraints = {} } = config; + const { copier } = constraints; + + if (is.nonEmptyString(copier)) { + logger.debug('Using copier constraint from config'); + return copier; + } + + return ''; +} From 7da01525ad756371caf6428d79300bb208da025d Mon Sep 17 00:00:00 2001 From: jeanluc Date: Mon, 5 Aug 2024 14:21:32 +0200 Subject: [PATCH 11/11] Apply review comments --- lib/modules/manager/copier/artifacts.spec.ts | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/modules/manager/copier/artifacts.spec.ts b/lib/modules/manager/copier/artifacts.spec.ts index 37f6158621dd46..4c2c9dc83d5273 100644 --- a/lib/modules/manager/copier/artifacts.spec.ts +++ b/lib/modules/manager/copier/artifacts.spec.ts @@ -54,7 +54,6 @@ describe('modules/manager/copier/artifacts', () => { }); afterEach(() => { - GlobalConfig.reset(); fs.readLocalFile.mockClear(); git.getRepoStatus.mockClear(); }); @@ -82,7 +81,7 @@ describe('modules/manager/copier/artifacts', () => { }, }, ]); - expect(execSnapshots).toMatchObject([]); + expect(execSnapshots).toEqual([]); }); it('reports an error if no upgrade is specified', async () => { @@ -103,7 +102,7 @@ describe('modules/manager/copier/artifacts', () => { }, }, ]); - expect(execSnapshots).toMatchObject([]); + expect(execSnapshots).toEqual([]); }); it('invokes copier update with the correct options by default', async () => { @@ -123,14 +122,15 @@ describe('modules/manager/copier/artifacts', () => { ]); }); - it.each([ - [null, null], - ['3.11.3', null], - [null, '9.1.0'], - ['3.11.3', '9.1.0'], - ])( - 'supports dynamic install with constraints python=%o copier=%o', - async (pythonConstraint, copierConstraint) => { + it.each` + pythonConstraint | copierConstraint + ${null} | ${null} + ${'3.11.3'} | ${null} + ${null} | ${'9.1.0'} + ${'3.11.3'} | ${'9.1.0'} + `( + `supports dynamic install with constraints python=$pythonConstraint copier=$copierConstraint`, + async ({ pythonConstraint, copierConstraint }) => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); const constraintConfig = { python: pythonConstraint ?? '', @@ -161,8 +161,8 @@ describe('modules/manager/copier/artifacts', () => { ).not.toBeNull(); expect(execSnapshots).toMatchObject([ - { cmd: 'install-tool python ' + (pythonConstraint ?? '3.12.4') }, - { cmd: 'install-tool copier ' + (copierConstraint ?? '9.2.0') }, + { cmd: `install-tool python ${pythonConstraint ?? '3.12.4'}` }, + { cmd: `install-tool copier ${copierConstraint ?? '9.2.0'}` }, { cmd: 'copier update --skip-answered --defaults --answers-file .copier-answers.yml --vcs-ref 1.1.0', },