From b22ea768fc1baf0014a74e0989f44d61e97fe77d Mon Sep 17 00:00:00 2001 From: Igor Katsuba <katsuba.igor@gmail.com> Date: Tue, 10 Nov 2020 16:25:37 +0300 Subject: [PATCH] feat: allow compilation of post-upgrade commands (#7632) Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- docs/usage/self-hosted-configuration.md | 43 +++++++++++++++++ lib/config/common.ts | 1 + lib/config/definitions.ts | 7 +++ lib/workers/branch/index.spec.ts | 64 +++++++++++++++++++++++-- lib/workers/branch/index.ts | 14 ++++-- 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 9e8c31ac5f..e5c6b2aa13 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -7,6 +7,49 @@ description: Self-Hosted Configuration usable in renovate.json or package.json The below configuration options are applicable only if you are running your own instance ("bot") of Renovate. +## allowPostUpgradeCommandTemplating + +If true allow templating for post-upgrade commands. + +Let's look at an example of configuring packages with existing Angular migrations. + +Add two properties to `config.js`: `allowPostUpgradeCommandTemplating` and `allowedPostUpgradeCommands` + +```javascript +module.export = { + allowPostUpgradeCommandTemplating: true, + allowedPostUpgradeCommands: ['^npm ci --ignore-scripts$', '^npx ng update'], +}; +``` + +In the `renovate.json` file, define the commands and files to be included in the final commit. + +The command to install dependencies is necessary because, by default, the installation of dependencies is skipped (see the `skipInstalls` admin option) + +```json +{ + "packageRules": [ + { + "packageNames": ["@angular/core"], + "postUpgradeTasks": { + "commands": [ + "npm ci --ignore-scripts", + "npx ng update {{{depName}}} --from={{{fromVersion}}} --to={{{toVersion}}} --migrateOnly --allowDirty --force" + ], + "fileFilters": ["**/**"] + } + } + ] +} +``` + +With this configuration, the executable command for `@angular/core` will look like this + +```bash +npm ci --ignore-scripts +npx ng update @angular/core --from=9.0.0 --to=10.0.0 --migrateOnly --allowDirty --force +``` + ## allowedPostUpgradeCommands A list of regular expressions that determine which commands in `postUpgradeTasks` are allowed to be executed. diff --git a/lib/config/common.ts b/lib/config/common.ts index 4bbee3aa5e..e01092a963 100644 --- a/lib/config/common.ts +++ b/lib/config/common.ts @@ -68,6 +68,7 @@ export interface GlobalConfig { } export interface RenovateAdminConfig { + allowPostUpgradeCommandTemplating?: boolean; allowedPostUpgradeCommands?: string[]; autodiscover?: boolean; autodiscoverFilter?: string; diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index 4b76f936e7..8f9e75ed4c 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -92,6 +92,13 @@ export type RenovateOptions = | RenovateObjectOption; const options: RenovateOptions[] = [ + { + name: 'allowPostUpgradeCommandTemplating', + description: 'If true allow templating for post-upgrade commands.', + type: 'boolean', + default: false, + admin: true, + }, { name: 'allowedPostUpgradeCommands', description: diff --git a/lib/workers/branch/index.spec.ts b/lib/workers/branch/index.spec.ts index 50310b2c26..883067782c 100644 --- a/lib/workers/branch/index.spec.ts +++ b/lib/workers/branch/index.spec.ts @@ -681,15 +681,73 @@ describe('workers/branch', () => { const result = await branchWorker.processBranch({ ...config, postUpgradeTasks: { - commands: ['echo 1', 'disallowed task'], + commands: ['echo {{{versioning}}}', 'disallowed task'], fileFilters: ['modified_file', 'deleted_file'], }, localDir: '/localDir', - allowedPostUpgradeCommands: ['^echo 1$'], + allowedPostUpgradeCommands: ['^echo {{{versioning}}}$'], + allowPostUpgradeCommandTemplating: true, }); expect(result).toEqual(ProcessBranchResult.Done); - expect(exec.exec).toHaveBeenCalledWith('echo 1', { cwd: '/localDir' }); + expect(exec.exec).toHaveBeenCalledWith('echo semver', { + cwd: '/localDir', + }); + }); + + it('executes post-upgrade tasks with disabled post-upgrade command templating', async () => { + const updatedPackageFile: File = { + name: 'pom.xml', + contents: 'pom.xml file contents', + }; + getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({ + updatedPackageFiles: [updatedPackageFile], + artifactErrors: [], + } as never); + npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({ + artifactErrors: [], + updatedArtifacts: [ + { + name: 'yarn.lock', + contents: Buffer.from([1, 2, 3]) /* Binary content */, + }, + ], + } as never); + git.branchExists.mockReturnValueOnce(true); + platform.getBranchPr.mockResolvedValueOnce({ + title: 'rebase!', + state: PrState.Open, + body: `- [x] <!-- rebase-check -->`, + } as never); + git.isBranchModified.mockResolvedValueOnce(true); + git.getRepoStatus.mockResolvedValueOnce({ + modified: ['modified_file'], + not_added: [], + deleted: ['deleted_file'], + } as StatusResult); + global.trustLevel = 'high'; + + fs.outputFile.mockReturnValue(); + fs.readFile.mockResolvedValueOnce(Buffer.from('modified file content')); + + schedule.isScheduledNow.mockReturnValueOnce(false); + commit.commitFilesToBranch.mockResolvedValueOnce(null); + + const result = await branchWorker.processBranch({ + ...config, + postUpgradeTasks: { + commands: ['echo {{{versioning}}}', 'disallowed task'], + fileFilters: ['modified_file', 'deleted_file'], + }, + localDir: '/localDir', + allowedPostUpgradeCommands: ['^echo {{{versioning}}}$'], + allowPostUpgradeCommandTemplating: false, + }); + + expect(result).toEqual(ProcessBranchResult.Done); + expect(exec.exec).toHaveBeenCalledWith('echo {{{versioning}}}', { + cwd: '/localDir', + }); }); }); }); diff --git a/lib/workers/branch/index.ts b/lib/workers/branch/index.ts index b8623d9e86..c1c83a5ad2 100644 --- a/lib/workers/branch/index.ts +++ b/lib/workers/branch/index.ts @@ -28,6 +28,7 @@ import { isBranchModified, } from '../../util/git'; import { regEx } from '../../util/regex'; +import * as template from '../../util/template'; import { BranchConfig, PrResult, ProcessBranchResult } from '../common'; import { checkAutoMerge, ensurePr } from '../pr'; import { tryBranchAutomerge } from './automerge'; @@ -381,13 +382,20 @@ export async function processBranch( 'Post-upgrade task did not match any on allowed list' ); } else { - logger.debug({ cmd }, 'Executing post-upgrade task'); + const compiledCmd = config.allowPostUpgradeCommandTemplating + ? template.compile(cmd, config) + : cmd; - const execResult = await exec(cmd, { + logger.debug({ cmd: compiledCmd }, 'Executing post-upgrade task'); + + const execResult = await exec(compiledCmd, { cwd: config.localDir, }); - logger.debug({ cmd, ...execResult }, 'Executed post-upgrade task'); + logger.debug( + { cmd: compiledCmd, ...execResult }, + 'Executed post-upgrade task' + ); } } -- GitLab