Repository: renovatebot/renovate
Pull request #35564: feat(config)!: validate allowedCommands against post-compiled commands
Author: login: rarkins
Labels: name: breaking
description: Breaking change, requires major version bump
Created: 2025-04-26T05:03:05Z | Merged: 2025-04-30T08:54:47Z

<!-- If this is your first pull request: sign the CLA with this GitHub app: https://cla-assistant.io/renovatebot/renovate -->
<!-- Make sure the `Allow edits and access to secrets by maintainers` checkbox is checked on this pull request. -->
<!-- Please read https://github.com/renovatebot/renovate/blob/main/.github/contributing.md before you create your pull request.-->

## Changes

- Removes `allowCommandTemplating` (it is now always allowed)
- Modifies `allowedCommands` logic so that it is evaluated against the _post_-compiled command not pre-compiled command

BREAKING CHANGE: allowedCommands need to be specified against the final post-compiled command and not the pre-compilation templated command.

## Context

It is safer for admins to be able to enforce allowed commands against the final post-compiled command not try to "lock down" templates

## Documentation (please check one with an [x])

- [x] I have updated the documentation, or
- [ ] No documentation update is required

## How I've tested my work (please select one)

I have verified these changes via:

- [ ] Code inspection only, or
- [x] Newly added/modified unit tests, or
- [ ] No unit tests but ran on a real repository, or
- [ ] Both unit tests + ran on a real repository

<!-- Do you have any suggestions about this PR template? Edit it here: https://github.com/renovatebot/renovate/edit/main/.github/pull_request_template.md -->

<!-- Please do not force push to your PR's branch after you have created your PR, as doing so forces us to review the whole PR again. This makes it harder for us to review your work because we don't know what has changed. -->
<!-- PRs will always be squashed by us when we merge your work. Commit as many times as you need in this branch. -->



Diff:
diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 9620553a954..f1b168c062c 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -3558,7 +3558,8 @@ The `postUpgradeTasks` configuration consists of three fields:
 
 A list of commands that are executed after Renovate has updated a dependency but before the commit is made.
 
-You can use variable templating in your commands as long as [`allowCommandTemplating`](./self-hosted-configuration.md#allowcommandtemplating) is enabled.
+You can use handlebars templating in these commands.
+They will be compiled _prior_ to the comparison against [`allowedCommands`](./self-hosted-configuration.md#allowedcommands).
 
 <!-- prettier-ignore -->
 !!! note
diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 0346442ed73..a05154b55c4 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -17,48 +17,6 @@ Please also see [Self-Hosted Experimental Options](./self-hosted-experimental.md
 !!! note
     Config options with `type=string` are always non-mergeable, so `mergeable=false`.
 
-## allowCommandTemplating
-
-Let's look at an example of configuring packages with existing Angular migrations.
-
-```javascript
-module.exports = {
-  allowedCommands: ['^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 (`npm ci --ignore-scripts`) is needed because, by default, the installation of dependencies is skipped (see the `skipInstalls` global option).
-
-```json
-{
-  "packageRules": [
-    {
-      "matchPackageNames": ["@angular/core"],
-      "postUpgradeTasks": {
-        "commands": [
-          "npm ci --ignore-scripts",
-          "npx ng update {{{depName}}} --from={{{currentVersion}}} --to={{{newVersion}}} --migrate-only --allow-dirty --force"
-        ]
-      }
-    }
-  ]
-}
-```
-
-With this configuration, the executable command for `@angular/core` looks like this:
-
-```bash
-npm ci --ignore-scripts
-npx ng update @angular/core --from=10.0.0 --to=11.0.0 --migrate-only --allow-dirty --force
-```
-
-If you wish to disable templating because of any security or performance concern, you may set `allowCommandTemplating` to `false`.
-But before you disable templating completely, try the `allowedCommands` config option to limit what commands are allowed to run.
-
-This configuration option was previously named `allowPostUpgradeCommandTemplating`.
-
 ## allowCustomCrateRegistries
 
 ## allowPlugins
@@ -69,7 +27,7 @@ This configuration option was previously named `allowPostUpgradeCommandTemplatin
 
 A list of regular expressions that decide which commands in `postUpgradeTasks` are allowed to run.
 
-If you are using a template command, the regular expression should match the template itself, not the final resolved value.
+If you are using a template command, the regular expression should match the final resolved value.
 If this list is empty then no tasks will be executed.
 
 For example:
diff --git a/lib/config/global.ts b/lib/config/global.ts
index 928f1c17939..06070f1089e 100644
--- a/lib/config/global.ts
+++ b/lib/config/global.ts
@@ -5,7 +5,6 @@ export class GlobalConfig {
   private static readonly OPTIONS: (keyof RepoGlobalConfig)[] = [
     'allowedCommands',
     'allowedEnv',
-    'allowCommandTemplating',
     'allowCustomCrateRegistries',
     'allowedHeaders',
     'allowPlugins',
diff --git a/lib/config/migrations/migrations-service.ts b/lib/config/migrations/migrations-service.ts
index 6516aa1bfd4..c8b75252869 100644
--- a/lib/config/migrations/migrations-service.ts
+++ b/lib/config/migrations/migrations-service.ts
@@ -61,6 +61,8 @@ import type { Migration, MigrationConstructor } from './types';
 
 export class MigrationsService {
   static readonly removedProperties: ReadonlySet<string> = new Set([
+    'allowCommandTemplating',
+    'allowPostUpgradeCommandTemplating',
     'deepExtract',
     'gitFs',
     'groupBranchName',
@@ -69,6 +71,7 @@ export class MigrationsService {
     'groupPrTitle',
     'lazyGrouping',
     'maintainYarnLock',
+    'raiseDeprecationWarnings',
     'statusCheckVerify',
     'supportPolicy',
     'transitiveRemediation',
@@ -77,12 +80,10 @@ export class MigrationsService {
     'yarnMaintenanceCommitMessage',
     'yarnMaintenancePrBody',
     'yarnMaintenancePrTitle',
-    'raiseDeprecationWarnings',
   ]);
 
   static readonly renamedProperties: ReadonlyMap<string, string> = new Map([
     ['adoptium-java', 'java-version'],
-    ['allowPostUpgradeCommandTemplating', 'allowCommandTemplating'],
     ['allowedPostUpgradeCommands', 'allowedCommands'],
     ['azureAutoApprove', 'autoApprove'],
     ['customChangelogUrl', 'changelogUrl'],
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 8785b378ba6..9dc92bfc792 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -107,14 +107,6 @@ const options: RenovateOptions[] = [
     default: null,
     globalOnly: true,
   },
-  {
-    name: 'allowCommandTemplating',
-    description:
-      'Set this to `false` to disable template compilation for post-upgrade commands.',
-    type: 'boolean',
-    default: true,
-    globalOnly: true,
-  },
   {
     name: 'allowedCommands',
     description:
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 7b2802afd45..f963f3da138 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -133,7 +133,6 @@ export interface GlobalOnlyConfig {
 // The below should contain config options where globalOnly=true
 export interface RepoGlobalConfig {
   allowedCommands?: string[];
-  allowCommandTemplating?: boolean;
   allowCustomCrateRegistries?: boolean;
   allowPlugins?: boolean;
   allowScripts?: boolean;
diff --git a/lib/workers/global/config/parse/env.ts b/lib/workers/global/config/parse/env.ts
index 5ac65b01405..84ce5c8f101 100644
--- a/lib/workers/global/config/parse/env.ts
+++ b/lib/workers/global/config/parse/env.ts
@@ -42,7 +42,6 @@ const renameKeys = {
   gitLabAutomerge: 'platformAutomerge', // migrate: gitLabAutomerge
   mergeConfidenceApiBaseUrl: 'mergeConfidenceEndpoint',
   mergeConfidenceSupportedDatasources: 'mergeConfidenceDatasources',
-  allowPostUpgradeCommandTemplating: 'allowCommandTemplating',
   allowedPostUpgradeCommands: 'allowedCommands',
 };
 
diff --git a/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts b/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
index 83b3763a0c9..346d0daea9a 100644
--- a/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
+++ b/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
@@ -31,7 +31,6 @@ export async function postUpgradeCommandsExecutor(
   let updatedArtifacts = [...(config.updatedArtifacts ?? [])];
   const artifactErrors = [...(config.artifactErrors ?? [])];
   const allowedCommands = GlobalConfig.get('allowedCommands');
-  const allowCommandTemplating = GlobalConfig.get('allowCommandTemplating');
 
   for (const upgrade of filteredUpgradeCommands) {
     addMeta({ dep: upgrade.depName });
@@ -63,12 +62,17 @@ export async function postUpgradeCommandsExecutor(
       }
 
       for (const cmd of commands) {
-        if (allowedCommands!.some((pattern) => regEx(pattern).test(cmd))) {
+        const compiledCmd = compile(cmd, mergeChildConfig(config, upgrade));
+        if (compiledCmd !== cmd) {
+          logger.debug(
+            { rawCmd: cmd, compiledCmd },
+            'Post-upgrade command has been compiled',
+          );
+        }
+        if (
+          allowedCommands!.some((pattern) => regEx(pattern).test(compiledCmd))
+        ) {
           try {
-            const compiledCmd = allowCommandTemplating
-              ? compile(cmd, mergeChildConfig(config, upgrade))
-              : cmd;
-
             logger.trace({ cmd: compiledCmd }, 'Executing post-upgrade task');
             const execResult = await exec(compiledCmd, {
               cwd: GlobalConfig.get('localDir'),
@@ -87,7 +91,7 @@ export async function postUpgradeCommandsExecutor(
         } else {
           logger.warn(
             {
-              cmd,
+              cmd: compiledCmd,
               allowedCommands,
             },
             'Post-upgrade task did not match any on allowedCommands list',
@@ -95,7 +99,7 @@ export async function postUpgradeCommandsExecutor(
           artifactErrors.push({
             lockFile: upgrade.packageFile,
             stderr: sanitize(
-              `Post-upgrade command '${cmd}' has not been added to the allowed list in allowedCommands`,
+              `Post-upgrade command '${compiledCmd}' has not been added to the allowed list in allowedCommands`,
             ),
           });
         }
diff --git a/lib/workers/repository/update/branch/index.spec.ts b/lib/workers/repository/update/branch/index.spec.ts
index def9b256992..a8cb1d193f2 100644
--- a/lib/workers/repository/update/branch/index.spec.ts
+++ b/lib/workers/repository/update/branch/index.spec.ts
@@ -1626,12 +1626,12 @@ describe('workers/repository/update/branch/index', () => {
       GlobalConfig.set({
         ...adminConfig,
         allowedCommands: ['^echo {{{versioning}}}$'],
-        allowCommandTemplating: true,
         exposeAllEnv: true,
         localDir: '/localDir',
       });
       const inconfig = {
         ...config,
+        versioning: 'semver',
         postUpgradeTasks: {
           executionMode: 'update',
           commands: ['echo {{{versioning}}}', 'disallowed task'],
@@ -1724,7 +1724,6 @@ describe('workers/repository/update/branch/index', () => {
       GlobalConfig.set({
         ...adminConfig,
         allowedCommands: ['^exit 1$'],
-        allowCommandTemplating: true,
         exposeAllEnv: true,
         localDir: '/localDir',
       });
@@ -1808,8 +1807,7 @@ describe('workers/repository/update/branch/index', () => {
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
       GlobalConfig.set({
         ...adminConfig,
-        allowedCommands: ['^echo {{{versioning}}}$'],
-        allowCommandTemplating: false,
+        allowedCommands: ['^echo'],
         exposeAllEnv: true,
         localDir: '/localDir',
       });
@@ -1823,6 +1821,7 @@ describe('workers/repository/update/branch/index', () => {
         upgrades: [
           partial<BranchUpgradeConfig>({
             depName: 'some-dep-name',
+            versioning: 'semver',
             postUpgradeTasks: {
               executionMode: 'update',
               commands: ['echo {{{versioning}}}', 'disallowed task'],
@@ -1839,7 +1838,7 @@ describe('workers/repository/update/branch/index', () => {
         result: 'done',
         commitSha: null,
       });
-      expect(exec.exec).toHaveBeenCalledWith('echo {{{versioning}}}', {
+      expect(exec.exec).toHaveBeenCalledWith('echo semver', {
         cwd: '/localDir',
       });
     });
@@ -1910,8 +1909,7 @@ describe('workers/repository/update/branch/index', () => {
 
       GlobalConfig.set({
         ...adminConfig,
-        allowedCommands: ['^echo {{{depName}}}$'],
-        allowCommandTemplating: true,
+        allowedCommands: ['^echo some'],
         exposeAllEnv: true,
         localDir: '/localDir',
       });
@@ -2061,7 +2059,6 @@ describe('workers/repository/update/branch/index', () => {
       GlobalConfig.set({
         ...adminConfig,
         allowedCommands: ['^echo hardcoded-string$'],
-        allowCommandTemplating: true,
         trustLevel: 'high',
         localDir: '/localDir',
       });


Reviews:
user: login: secustor
body: Shouldn't be this targeted against the v40 branch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T06:18:06Z
user: login: viceice
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T06:57:29Z
user: login: viceice
body: you should target main, not next if you don't plan to release this as pre release 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T08:13:40Z
user: login: viceice
body: otherwise lgtm
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-30T07:19:57Z
user: login: viceice
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T07:51:39Z

Review comments:
diff_hunk: @@ -63,12 +62,17 @@ export async function postUpgradeCommandsExecutor(
       }
 
       for (const cmd of commands) {
-        if (allowedCommands!.some((pattern) => regEx(pattern).test(cmd))) {
+        const compiledCmd = compile(cmd, mergeChildConfig(config, upgrade));
+        if (compiledCmd !== cmd) {
+          logger.debug(
+            { rawCmd: cmd, compiledCmd },
+            'Post-upgrade command has been compiled',
+          );
+        }
+        if (
+          allowedCommands!.some((pattern) => regEx(pattern).test(compiledCmd))
path: lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
user: login: secustor
body: ```suggestion
          allowedCommands?.some((pattern) => regEx(pattern).test(compiledCmd))
```

created_at: 2025-04-26T06:17:39Z
author_association: MEMBER
diff_hunk: @@ -82,7 +82,6 @@ export class MigrationsService {
 
   static readonly renamedProperties: ReadonlyMap<string, string> = new Map([
     ['adoptium-java', 'java-version'],
-    ['allowPostUpgradeCommandTemplating', 'allowCommandTemplating'],
path: lib/config/migrations/migrations-service.ts
user: login: viceice
body: add to removed properties above 
created_at: 2025-04-26T06:55:18Z
author_association: MEMBER
diff_hunk: @@ -63,12 +62,17 @@ export async function postUpgradeCommandsExecutor(
       }
 
       for (const cmd of commands) {
-        if (allowedCommands!.some((pattern) => regEx(pattern).test(cmd))) {
+        const compiledCmd = compile(cmd, mergeChildConfig(config, upgrade));
+        if (compiledCmd !== cmd) {
+          logger.debug(
+            { rawCmd: cmd, compiledCmd },
+            'Post-upgrade command has been compiled',
+          );
+        }
+        if (
+          allowedCommands!.some((pattern) => regEx(pattern).test(compiledCmd))
path: lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
user: login: viceice
body: no, it's already validated here, but typescript didn't know it
created_at: 2025-04-26T06:56:30Z
author_association: MEMBER
diff_hunk: @@ -91,6 +98,42 @@ module.exports = {
 
 For the `repositories` key, replace `YOUR-PROJECT/YOUR-REPO` with your Azure DevOps project and repository.
 
+### Using Azure DevOps internal API for pipeline tasks versions
path: lib/modules/platform/azure/readme.md
user: login: viceice
body: unrelated? 🤔
created_at: 2025-04-27T08:11:46Z
author_association: MEMBER
diff_hunk: @@ -50,14 +50,21 @@ steps:
     inputs:
       workingFile: .npmrc
 
+  - bash: |
+      add-apt-repository ppa:git-core/ppa
+      apt update && apt install git -y
+    displayName: 'Install latest version of Git'
path: lib/modules/platform/azure/readme.md
user: login: viceice
body: ```suggestion
  - bash: |
      add-apt-repository ppa:git-core/ppa
      apt update && apt install git -y
    name: 'Install latest version of Git'
```
why do we need this? isn't the git from Ubuntu 24.04 new enough?
created_at: 2025-04-27T08:12:48Z
author_association: MEMBER
diff_hunk: @@ -78,11 +78,12 @@ export class MigrationsService {
     'yarnMaintenancePrBody',
     'yarnMaintenancePrTitle',
     'raiseDeprecationWarnings',
+    'allowPostUpgradeCommandTemplating',
path: lib/config/migrations/migrations-service.ts
user: login: viceice
body: should be sorted alphabetically
created_at: 2025-04-30T07:18:37Z
author_association: MEMBER

PR comments:
user: login: rarkins
created_at: 2025-04-26T06:30:00Z
body: Yes it's for v40
author_association: CONTRIBUTOR
user: login: secustor
created_at: 2025-04-26T10:52:05Z
body: > Yes it's for v40

I'm asking because you are targeting `main`. 
author_association: MEMBER
user: login: rarkins
created_at: 2025-04-26T11:03:38Z
body: Changed to `next` but the diff is now incomprehensible and there's conflicts. What's the plan?
author_association: CONTRIBUTOR
user: login: renovate-release
created_at: 2025-04-30T09:09:31Z
body: :tada: This PR is included in version 40.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/renovatebot/renovate/releases/tag/40.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/renovate/v/40.0.0)
- `40.0.0`

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE