From 91fd1757f604df263a7c39cec818bbc2bfcd5ded Mon Sep 17 00:00:00 2001
From: Adam Setch <adam.setch@outlook.com>
Date: Fri, 31 Mar 2023 01:39:06 -0400
Subject: [PATCH] feat(template): indentation capture group (#21193)

---
 lib/modules/manager/regex/index.spec.ts | 52 +++++++++++++++++++++++++
 lib/modules/manager/regex/readme.md     |  2 +
 lib/modules/manager/regex/utils.ts      |  4 ++
 lib/modules/manager/types.ts            |  1 +
 lib/util/template/index.ts              |  1 +
 5 files changed, 60 insertions(+)

diff --git a/lib/modules/manager/regex/index.spec.ts b/lib/modules/manager/regex/index.spec.ts
index 1146c533f2..e5374ea511 100644
--- a/lib/modules/manager/regex/index.spec.ts
+++ b/lib/modules/manager/regex/index.spec.ts
@@ -214,6 +214,58 @@ describe('modules/manager/regex/index', () => {
     expect(res?.deps).toHaveLength(1);
   });
 
+  it('extracts indentation: maintains indentation value if whitespace or empty', async () => {
+    const config = {
+      matchStrings: [
+        '(?<indentation>\\s*)image:\\s+(?<depName>[^\\s]+):(?<currentValue>[^\\s]+)',
+      ],
+      autoReplaceStringTemplate:
+        'image:\n{{{indentation}}}  name: {{{depName}}}:{{{newValue}}}',
+      datasourceTemplate: 'docker',
+    };
+    const res = await extractPackageFile(
+      '     image: eclipse-temurin:17.0.0-alpine',
+      'bitbucket-pipelines.yml',
+      config
+    );
+    expect(res).toMatchObject({
+      deps: [
+        {
+          depName: 'eclipse-temurin',
+          currentValue: '17.0.0-alpine',
+          datasource: 'docker',
+          indentation: '     ',
+        },
+      ],
+    });
+  });
+
+  it('extracts indentation: discards non-whitespace content', async () => {
+    const config = {
+      matchStrings: [
+        '(?<indentation>.*)image:\\s+(?<depName>[^\\s]+):(?<currentValue>[^\\s]+)',
+      ],
+      autoReplaceStringTemplate:
+        'image:\n{{{indentation}}}  name: {{{depName}}}:{{{newValue}}}',
+      datasourceTemplate: 'docker',
+    };
+    const res = await extractPackageFile(
+      'name: image: eclipse-temurin:17.0.0-alpine',
+      'bitbucket-pipelines.yml',
+      config
+    );
+    expect(res).toMatchObject({
+      deps: [
+        {
+          depName: 'eclipse-temurin',
+          currentValue: '17.0.0-alpine',
+          datasource: 'docker',
+          indentation: '',
+        },
+      ],
+    });
+  });
+
   it('extracts with combination strategy', async () => {
     const config: CustomExtractConfig = {
       matchStrings: [
diff --git a/lib/modules/manager/regex/readme.md b/lib/modules/manager/regex/readme.md
index e2a7efe66f..e2c0fe27f1 100644
--- a/lib/modules/manager/regex/readme.md
+++ b/lib/modules/manager/regex/readme.md
@@ -34,6 +34,8 @@ Configuration-wise, it works like this:
 - You can optionally have a `currentDigest` capture group.
 - You can optionally have a `registryUrl` capture group or a `registryUrlTemplate` config field
   - If it's a valid URL, it will be converted to the `registryUrls` field as a single-length array.
+- You can optionally have an `indentation` capture group.
+  - If it's not empty or whitespace, it will be reset to an empty string.
 
 ### Regular Expression Capture Groups
 
diff --git a/lib/modules/manager/regex/utils.ts b/lib/modules/manager/regex/utils.ts
index 1d909a6e80..8f9a2743fc 100644
--- a/lib/modules/manager/regex/utils.ts
+++ b/lib/modules/manager/regex/utils.ts
@@ -17,6 +17,7 @@ export const validMatchFields = [
   'extractVersion',
   'registryUrl',
   'depType',
+  'indentation',
 ] as const;
 
 type ValidMatchFields = (typeof validMatchFields)[number];
@@ -39,6 +40,9 @@ function updateDependency(
     case 'datasource':
       dependency.datasource = migrateDatasource(value);
       break;
+    case 'indentation':
+      dependency.indentation = is.emptyStringOrWhitespace(value) ? value : '';
+      break;
     default:
       dependency[field] = value;
       break;
diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts
index 80f84fe480..abd361b4b1 100644
--- a/lib/modules/manager/types.ts
+++ b/lib/modules/manager/types.ts
@@ -150,6 +150,7 @@ export interface PackageDependency<T = Record<string, any>>
   extractVersion?: string;
   isInternal?: boolean;
   variableName?: string;
+  indentation?: string;
 }
 
 export interface Upgrade<T = Record<string, any>> extends PackageDependency<T> {
diff --git a/lib/util/template/index.ts b/lib/util/template/index.ts
index 6034e2ce38..c1b72dc9b8 100644
--- a/lib/util/template/index.ts
+++ b/lib/util/template/index.ts
@@ -87,6 +87,7 @@ export const allowedFields = {
   displayPending: 'Latest pending update, if internalChecksFilter is in use',
   displayTo: 'The to value, formatted for display',
   hasReleaseNotes: 'true if the upgrade has release notes',
+  indentation: 'The indentation of the dependency being updated',
   isLockfileUpdate: 'true if the branch is a lock file update',
   isMajor: 'true if the upgrade is major',
   isPatch: 'true if the upgrade is a patch upgrade',
-- 
GitLab