From 19a99d2ca9ea382c3bff303a134a104d837aad05 Mon Sep 17 00:00:00 2001
From: Gabriel-Ladzaretti
 <97394622+Gabriel-Ladzaretti@users.noreply.github.com>
Date: Tue, 7 Jan 2025 08:56:48 +0200
Subject: [PATCH] feat(config): add repo phase enviorment config (#33360)

---
 docs/usage/self-hosted-experimental.md     |  7 ++++
 lib/workers/global/config/parse/env.ts     | 20 +++++-----
 lib/workers/repository/init/config.spec.ts | 46 ++++++++++++++++++++++
 lib/workers/repository/init/config.ts      | 22 ++++++++++-
 4 files changed, 85 insertions(+), 10 deletions(-)
 create mode 100644 lib/workers/repository/init/config.spec.ts

diff --git a/docs/usage/self-hosted-experimental.md b/docs/usage/self-hosted-experimental.md
index f80219fd96..78ea76a020 100644
--- a/docs/usage/self-hosted-experimental.md
+++ b/docs/usage/self-hosted-experimental.md
@@ -23,6 +23,13 @@ For more information see [the OpenTelemetry docs](opentelemetry.md).
 
 If set to any value, Renovate will always paginate requests to GitHub fully, instead of stopping after 10 pages.
 
+## `RENOVATE_STATIC_REPO_CONFIG`
+
+If set to a _valid_ `JSON` string containing a _valid_ Renovate configuration, it will be applied to the repository config before resolving the actual configuration file within the repository.
+
+> [!warning]
+> An invalid value will result in the scan being aborted.
+
 ## `RENOVATE_X_DOCKER_HUB_DISABLE_LABEL_LOOKUP`
 
 If set to any value, Renovate will skip attempting to get release labels (e.g. gitRef, sourceUrl) from manifest annotations for `https://index.docker.io`.
diff --git a/lib/workers/global/config/parse/env.ts b/lib/workers/global/config/parse/env.ts
index a5543bb198..79ed110d6b 100644
--- a/lib/workers/global/config/parse/env.ts
+++ b/lib/workers/global/config/parse/env.ts
@@ -121,16 +121,10 @@ export async function getConfig(
   inputEnv: NodeJS.ProcessEnv,
   configEnvKey = 'RENOVATE_CONFIG',
 ): Promise<AllConfig> {
-  let env = normalizePrefixes(inputEnv, inputEnv.ENV_PREFIX);
-  env = massageConvertedExperimentalVars(env);
-  env = renameEnvKeys(env);
-  // massage the values of migrated configuration keys
-  env = massageEnvKeyValues(env);
-
-  const options = getOptions();
-
+  const env = prepareEnv(inputEnv);
   const config = await parseAndValidateOrExit(env, configEnvKey);
 
+  const options = getOptions();
   config.hostRules ??= [];
 
   for (const option of options) {
@@ -235,7 +229,15 @@ export async function getConfig(
   return config;
 }
 
-async function parseAndValidateOrExit(
+export function prepareEnv(inputEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
+  let env = normalizePrefixes(inputEnv, inputEnv.ENV_PREFIX);
+  env = massageConvertedExperimentalVars(env);
+  env = renameEnvKeys(env);
+  // massage the values of migrated configuration keys
+  return massageEnvKeyValues(env);
+}
+
+export async function parseAndValidateOrExit(
   env: NodeJS.ProcessEnv,
   configEnvKey: string,
 ): Promise<AllConfig> {
diff --git a/lib/workers/repository/init/config.spec.ts b/lib/workers/repository/init/config.spec.ts
new file mode 100644
index 0000000000..33cedcd071
--- /dev/null
+++ b/lib/workers/repository/init/config.spec.ts
@@ -0,0 +1,46 @@
+import type { AllConfig } from '../../../config/types';
+import { mergeStaticRepoEnvConfig } from './config';
+
+describe('workers/repository/init/config', () => {
+  describe('mergeRepoEnvConfig()', () => {
+    type MergeRepoEnvTestCase = {
+      name: string;
+      env: NodeJS.ProcessEnv;
+      currentConfig: AllConfig;
+      wantConfig: AllConfig;
+    };
+
+    const testCases: MergeRepoEnvTestCase[] = [
+      {
+        name: 'it does nothing',
+        env: {},
+        currentConfig: { repositories: ['some/repo'] },
+        wantConfig: { repositories: ['some/repo'] },
+      },
+      {
+        name: 'it merges env with the current config',
+        env: { RENOVATE_STATIC_REPO_CONFIG: '{"dependencyDashboard":true}' },
+        currentConfig: { repositories: ['some/repo'] },
+        wantConfig: {
+          dependencyDashboard: true,
+          repositories: ['some/repo'],
+        },
+      },
+      {
+        name: 'it ignores env with other renovate specific configuration options',
+        env: { RENOVATE_CONFIG: '{"dependencyDashboard":true}' },
+        currentConfig: { repositories: ['some/repo'] },
+        wantConfig: { repositories: ['some/repo'] },
+      },
+    ];
+
+    it.each(testCases)(
+      '$name',
+      async ({ env, currentConfig, wantConfig }: MergeRepoEnvTestCase) => {
+        const got = await mergeStaticRepoEnvConfig(currentConfig, env);
+
+        expect(got).toEqual(wantConfig);
+      },
+    );
+  });
+});
diff --git a/lib/workers/repository/init/config.ts b/lib/workers/repository/init/config.ts
index a8f9a14c6d..fe3122896b 100644
--- a/lib/workers/repository/init/config.ts
+++ b/lib/workers/repository/init/config.ts
@@ -1,4 +1,7 @@
-import type { RenovateConfig } from '../../../config/types';
+import is from '@sindresorhus/is';
+import { mergeChildConfig } from '../../../config';
+import type { AllConfig, RenovateConfig } from '../../../config/types';
+import { parseAndValidateOrExit } from '../../global/config/parse/env';
 import { checkOnboardingBranch } from '../onboarding/branch';
 import { mergeInheritedConfig } from './inherited';
 import { mergeRenovateConfig } from './merge';
@@ -10,7 +13,24 @@ export async function getRepoConfig(
   let config = { ...config_ };
   config.baseBranch = config.defaultBranch;
   config = await mergeInheritedConfig(config);
+  config = await mergeStaticRepoEnvConfig(config, process.env);
   config = await checkOnboardingBranch(config);
   config = await mergeRenovateConfig(config);
   return config;
 }
+
+export async function mergeStaticRepoEnvConfig(
+  config: AllConfig,
+  env: NodeJS.ProcessEnv,
+): Promise<AllConfig> {
+  const repoEnvConfig = await parseAndValidateOrExit(
+    env,
+    'RENOVATE_STATIC_REPO_CONFIG',
+  );
+
+  if (!is.nonEmptyObject(repoEnvConfig)) {
+    return config;
+  }
+
+  return mergeChildConfig(config, repoEnvConfig);
+}
-- 
GitLab