diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 40c840d934a5b4..fcb954b4df86c3 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1090,6 +1090,11 @@ For instance if you have a project with an `"examples/"` directory you wish to i Useful to know: Renovate's default ignore is `node_modules` and `bower_components` only, however if you are extending the popular `config:base` preset then it adds ignore patterns for `vendor`, `examples`, `test(s)` and `fixtures` directories too. +## ignorePlugins + +Set this to `true` if running plugins causes problems. +Applicable for Composer only for now. + ## ignorePrAuthor This is usually needed if someone needs to migrate bot accounts, including from hosted app to self-hosted. diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 8a90bff42655e6..362f3af83f65fd 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -11,6 +11,8 @@ Please also see [Self-Hosted Experimental Options](./self-hosted-experimental.md ## allowCustomCrateRegistries +## allowPlugins + ## allowPostUpgradeCommandTemplating Set to true to allow templating of dependency level post-upgrade commands. diff --git a/lib/config/global.ts b/lib/config/global.ts index 38f557c52fdb9c..0aefa514bdf309 100644 --- a/lib/config/global.ts +++ b/lib/config/global.ts @@ -5,6 +5,7 @@ let repoGlobalConfig: RepoGlobalConfig = {}; // TODO: once global config work is complete, add a test to make sure this list includes all options with globalOnly=true (#9603) const repoGlobalOptions = [ 'allowCustomCrateRegistries', + 'allowPlugins', 'allowPostUpgradeCommandTemplating', 'allowScripts', 'allowedPostUpgradeCommands', diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 7185155a796584..2321dbbc47af8a 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -548,6 +548,14 @@ const options: RenovateOptions[] = [ type: 'boolean', default: false, }, + { + name: 'allowPlugins', + description: + 'Configure this to true if repositories are allowed to run install plugins.', + globalOnly: true, + type: 'boolean', + default: false, + }, { name: 'allowScripts', description: @@ -564,6 +572,13 @@ const options: RenovateOptions[] = [ type: 'boolean', default: false, }, + { + name: 'ignorePlugins', + description: + 'Configure this to true if allowPlugins=true but you wish to skip running plugins when updating lock files.', + type: 'boolean', + default: false, + }, { name: 'ignoreScripts', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index f01119b0221f1a..470709ae7e8b68 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -91,6 +91,7 @@ export interface GlobalOnlyConfig { // The below should contain config options where globalOnly=true export interface RepoGlobalConfig { allowCustomCrateRegistries?: boolean; + allowPlugins?: boolean; allowPostUpgradeCommandTemplating?: boolean; allowScripts?: boolean; allowedPostUpgradeCommands?: string[]; diff --git a/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap b/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap index 51f7538dda1a63..7025b13c106048 100644 --- a/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap +++ b/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap @@ -46,6 +46,30 @@ Array [ ] `; +exports[`manager/composer/artifacts disable plugins when configured locally 1`] = ` +Array [ + Object { + "cmd": "composer update foo bar --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + exports[`manager/composer/artifacts disables ignorePlatformReqs 1`] = ` Array [ Object { @@ -70,6 +94,116 @@ Array [ ] `; +exports[`manager/composer/artifacts does not disable plugins when configured globally 1`] = ` +Array [ + Object { + "cmd": "composer update foo bar --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`manager/composer/artifacts installs before running the update when symfony flex is installed 1`] = ` +Array [ + Object { + "cmd": "composer install --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`manager/composer/artifacts installs before running the update when symfony flex is installed as dev 1`] = ` +Array [ + Object { + "cmd": "composer install --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + exports[`manager/composer/artifacts performs lockFileMaintenance 1`] = ` Array [ Object { diff --git a/lib/manager/composer/artifacts.spec.ts b/lib/manager/composer/artifacts.spec.ts index 42cfe6ea50ad2f..a0cdc01deb3215 100644 --- a/lib/manager/composer/artifacts.spec.ts +++ b/lib/manager/composer/artifacts.spec.ts @@ -26,6 +26,7 @@ const config: UpdateArtifactsConfig = { }; const adminConfig: RepoGlobalConfig = { + allowPlugins: false, allowScripts: false, // `join` fixes Windows CI localDir: join('/tmp/github/some/repo'), @@ -79,8 +80,8 @@ describe('manager/composer/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(exec); fs.readLocalFile.mockResolvedValueOnce('{}'); - git.getRepoStatus.mockResolvedValue(repoStatus); - setGlobalConfig({ ...adminConfig, allowScripts: true }); + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + setGlobalConfig({ ...adminConfig, allowScripts: true, allowPlugins: true }); expect( await composer.updateArtifacts({ packageFileName: 'composer.json', @@ -132,7 +133,7 @@ describe('manager/composer/artifacts', () => { ...config, registryUrls: ['https://packagist.renovatebot.com'], }; - git.getRepoStatus.mockResolvedValue(repoStatus); + git.getRepoStatus.mockResolvedValueOnce(repoStatus); expect( await composer.updateArtifacts({ packageFileName: 'composer.json', @@ -148,7 +149,7 @@ describe('manager/composer/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(exec); fs.readLocalFile.mockResolvedValueOnce('{}'); - git.getRepoStatus.mockResolvedValue({ + git.getRepoStatus.mockResolvedValueOnce({ ...repoStatus, modified: ['composer.lock'], }); @@ -200,7 +201,7 @@ describe('manager/composer/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(exec); fs.readLocalFile.mockResolvedValueOnce('{ }'); - git.getRepoStatus.mockResolvedValue({ + git.getRepoStatus.mockResolvedValueOnce({ ...repoStatus, modified: ['composer.lock'], }); @@ -236,7 +237,7 @@ describe('manager/composer/artifacts', () => { ], }); - git.getRepoStatus.mockResolvedValue({ + git.getRepoStatus.mockResolvedValueOnce({ ...repoStatus, modified: ['composer.lock'], }); @@ -258,7 +259,7 @@ describe('manager/composer/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(exec); fs.readLocalFile.mockResolvedValueOnce('{ }'); - git.getRepoStatus.mockResolvedValue({ + git.getRepoStatus.mockResolvedValueOnce({ ...repoStatus, modified: ['composer.lock'], }); @@ -328,7 +329,7 @@ describe('manager/composer/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(exec); fs.readLocalFile.mockResolvedValueOnce('{ }'); - git.getRepoStatus.mockResolvedValue({ + git.getRepoStatus.mockResolvedValueOnce({ ...repoStatus, modified: ['composer.lock'], }); @@ -350,7 +351,7 @@ describe('manager/composer/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(exec); fs.readLocalFile.mockResolvedValueOnce('{ }'); - git.getRepoStatus.mockResolvedValue({ + git.getRepoStatus.mockResolvedValueOnce({ ...repoStatus, modified: ['composer.lock'], }); @@ -367,4 +368,89 @@ describe('manager/composer/artifacts', () => { ).not.toBeNull(); expect(execSnapshots).toMatchSnapshot(); }); + + it('installs before running the update when symfony flex is installed', async () => { + fs.readLocalFile.mockResolvedValueOnce( + '{"packages":[{"name":"symfony/flex","version":"1.17.1"}]}' + ); + const execSnapshots = mockExecAll(exec); + fs.readLocalFile.mockResolvedValueOnce('{ }'); + git.getRepoStatus.mockResolvedValueOnce({ + ...repoStatus, + modified: ['composer.lock'], + }); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: { + ...config, + }, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + expect(execSnapshots).toHaveLength(2); + }); + + it('installs before running the update when symfony flex is installed as dev', async () => { + fs.readLocalFile.mockResolvedValueOnce( + '{"packages-dev":[{"name":"symfony/flex","version":"1.17.1"}]}' + ); + const execSnapshots = mockExecAll(exec); + fs.readLocalFile.mockResolvedValueOnce('{ }'); + git.getRepoStatus.mockResolvedValueOnce({ + ...repoStatus, + modified: ['composer.lock'], + }); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: { + ...config, + }, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + expect(execSnapshots).toHaveLength(2); + }); + + it('does not disable plugins when configured globally', async () => { + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(exec); + fs.readLocalFile.mockResolvedValueOnce('{}'); + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + setGlobalConfig({ ...adminConfig, allowPlugins: true }); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }], + newPackageFileContent: '{}', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + + it('disable plugins when configured locally', async () => { + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(exec); + fs.readLocalFile.mockResolvedValueOnce('{}'); + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + setGlobalConfig({ ...adminConfig, allowPlugins: true }); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }], + newPackageFileContent: '{}', + config: { + ...config, + ignorePlugins: true, + }, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); }); diff --git a/lib/manager/composer/artifacts.ts b/lib/manager/composer/artifacts.ts index 27a5378f624fa3..9c9d210f5f3cb0 100644 --- a/lib/manager/composer/artifacts.ts +++ b/lib/manager/composer/artifacts.ts @@ -21,12 +21,13 @@ import { getRepoStatus } from '../../util/git'; import * as hostRules from '../../util/host-rules'; import { regEx } from '../../util/regex'; import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; -import type { AuthJson } from './types'; +import type { AuthJson, ComposerLock } from './types'; import { composerVersioningId, extractContraints, getComposerArguments, getPhpConstraint, + requireComposerDependencyInstallation, } from './utils'; function getAuthJson(): string | null { @@ -94,11 +95,9 @@ export async function updateArtifacts({ try { await writeLocalFile(packageFileName, newPackageFileContent); + const existingLockFile: ComposerLock = JSON.parse(existingLockFileContent); const constraints = { - ...extractContraints( - JSON.parse(newPackageFileContent), - JSON.parse(existingLockFileContent) - ), + ...extractContraints(JSON.parse(newPackageFileContent), existingLockFile), ...config.constraints, }; @@ -120,6 +119,17 @@ export async function updateArtifacts({ tagScheme: composerVersioningId, }, }; + + const commands: string[] = []; + + // Determine whether install is required before update + if (requireComposerDependencyInstallation(existingLockFile)) { + const preCmd = 'composer'; + const preArgs = 'install' + getComposerArguments(config); + logger.debug({ preCmd, preArgs }, 'composer pre-update command'); + commands.push(`${preCmd} ${preArgs}`); + } + const cmd = 'composer'; let args: string; if (config.isLockFileMaintenance) { @@ -132,7 +142,9 @@ export async function updateArtifacts({ } args += getComposerArguments(config); logger.debug({ cmd, args }, 'composer command'); - await exec(`${cmd} ${args}`, execOptions); + commands.push(`${cmd} ${args}`); + + await exec(commands, execOptions); const status = await getRepoStatus(); if (!status.modified.includes(lockFileName)) { return null; diff --git a/lib/manager/composer/utils.spec.ts b/lib/manager/composer/utils.spec.ts index 6970039a65ffde..7213bf1333bd5f 100644 --- a/lib/manager/composer/utils.spec.ts +++ b/lib/manager/composer/utils.spec.ts @@ -1,5 +1,11 @@ import { setGlobalConfig } from '../../config/global'; -import { extractContraints, getComposerArguments } from './utils'; +import { + extractContraints, + getComposerArguments, + requireComposerDependencyInstallation, +} from './utils'; + +jest.mock('../../../lib/datasource'); describe('manager/composer/utils', () => { describe('extractContraints', () => { @@ -75,13 +81,15 @@ describe('manager/composer/utils', () => { ' --ignore-platform-req ext-intl --ignore-platform-req ext-icu --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins' ); }); - it('allows scripts/plugins when configured', () => { + it('allows scripts when configured', () => { setGlobalConfig({ allowScripts: true, }); - expect(getComposerArguments({})).toBe(' --no-ansi --no-interaction'); + expect(getComposerArguments({})).toBe( + ' --no-ansi --no-interaction --no-plugins' + ); }); - it('disables scripts/plugins when configured locally', () => { + it('disables scripts when configured locally', () => { setGlobalConfig({ allowScripts: true, }); @@ -93,5 +101,51 @@ describe('manager/composer/utils', () => { ' --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins' ); }); + it('allows plugins when configured', () => { + setGlobalConfig({ + allowPlugins: true, + }); + expect(getComposerArguments({})).toBe( + ' --no-ansi --no-interaction --no-scripts --no-autoloader' + ); + }); + it('disables plugins when configured locally', () => { + setGlobalConfig({ + allowPlugins: true, + }); + expect( + getComposerArguments({ + ignorePlugins: true, + }) + ).toBe( + ' --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins' + ); + }); + }); + + describe('requireComposerDependencyInstallation', () => { + it('returns true when symfony/flex has been installed', () => { + expect( + requireComposerDependencyInstallation({ + packages: [{ name: 'symfony/flex', version: '1.17.1' }], + }) + ).toBeTrue(); + }); + + it('returns true when symfony/flex has been installed as dev dependency', () => { + expect( + requireComposerDependencyInstallation({ + 'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }], + }) + ).toBeTrue(); + }); + + it('returns false when symfony/flex has not been installed', () => { + expect( + requireComposerDependencyInstallation({ + packages: [{ name: 'symfony/console', version: '5.4.0' }], + }) + ).toBeFalse(); + }); }); }); diff --git a/lib/manager/composer/utils.ts b/lib/manager/composer/utils.ts index 68684d9ba7cd1c..d88609f129b046 100644 --- a/lib/manager/composer/utils.ts +++ b/lib/manager/composer/utils.ts @@ -7,6 +7,8 @@ import type { ComposerConfig, ComposerLock } from './types'; export { composerVersioningId }; +const depRequireInstall = new Set(['symfony/flex']); + export function getComposerArguments(config: UpdateArtifactsConfig): string { let args = ''; @@ -22,7 +24,11 @@ export function getComposerArguments(config: UpdateArtifactsConfig): string { args += ' --no-ansi --no-interaction'; if (!getGlobalConfig().allowScripts || config.ignoreScripts) { - args += ' --no-scripts --no-autoloader --no-plugins'; + args += ' --no-scripts --no-autoloader'; + } + + if (!getGlobalConfig().allowPlugins || config.ignorePlugins) { + args += ' --no-plugins'; } return args; @@ -39,6 +45,15 @@ export function getPhpConstraint(constraints: Record): string { return null; } +export function requireComposerDependencyInstallation( + lock: ComposerLock +): boolean { + return ( + lock.packages?.some((p) => depRequireInstall.has(p.name)) === true || + lock['packages-dev']?.some((p) => depRequireInstall.has(p.name)) === true + ); +} + export function extractContraints( composerJson: ComposerConfig, lockParsed: ComposerLock diff --git a/lib/manager/types.ts b/lib/manager/types.ts index 90706462d01325..326c07093bd377 100644 --- a/lib/manager/types.ts +++ b/lib/manager/types.ts @@ -44,6 +44,7 @@ export interface UpdateArtifactsConfig { composerIgnorePlatformReqs?: string[]; currentValue?: string; postUpdateOptions?: string[]; + ignorePlugins?: boolean; ignoreScripts?: boolean; updateType?: UpdateType; newValue?: string;