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