From 1ca7d263f0f5038b53f74c5a757f18b8106c9390 Mon Sep 17 00:00:00 2001
From: Tor Arvid Lund <torarvid@gmail.com>
Date: Fri, 11 Oct 2024 11:33:58 +0200
Subject: [PATCH] feat: export UV_EXTRA_INDEX_URL before doing 'uv lock'
 (#31840)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 .../manager/pep621/processors/uv.spec.ts      | 65 ++++++++++++++++++-
 lib/modules/manager/pep621/processors/uv.ts   | 38 +++++++++++
 2 files changed, 102 insertions(+), 1 deletion(-)

diff --git a/lib/modules/manager/pep621/processors/uv.spec.ts b/lib/modules/manager/pep621/processors/uv.spec.ts
index 21141d6250..ba53593d93 100644
--- a/lib/modules/manager/pep621/processors/uv.spec.ts
+++ b/lib/modules/manager/pep621/processors/uv.spec.ts
@@ -1,6 +1,6 @@
 import { join } from 'upath';
 import { mockExecAll } from '../../../../../test/exec-util';
-import { fs, mockedFunction } from '../../../../../test/util';
+import { fs, hostRules, mockedFunction } from '../../../../../test/util';
 import { GlobalConfig } from '../../../../config/global';
 import type { RepoGlobalConfig } from '../../../../config/types';
 import { getPkgReleases as _getPkgReleases } from '../../../datasource';
@@ -268,6 +268,69 @@ describe('modules/manager/pep621/processors/uv', () => {
       ]);
     });
 
+    it('performs update on private package registry', async () => {
+      const execSnapshots = mockExecAll();
+      GlobalConfig.set(adminConfig);
+      hostRules.add({
+        matchHost: 'https://example.com',
+        username: 'user',
+        password: 'pass',
+      });
+      fs.getSiblingFileName.mockReturnValueOnce('uv.lock');
+      fs.readLocalFile.mockResolvedValueOnce('test content');
+      fs.readLocalFile.mockResolvedValueOnce('changed test content');
+      // python
+      getPkgReleases.mockResolvedValueOnce({
+        releases: [{ version: '3.11.1' }, { version: '3.11.2' }],
+      });
+      // uv
+      getPkgReleases.mockResolvedValueOnce({
+        releases: [{ version: '0.2.35' }, { version: '0.2.28' }],
+      });
+
+      const updatedDeps = [
+        {
+          packageName: 'dep1',
+          depType: depTypes.dependencies,
+          registryUrls: ['https://foobar.com'],
+        },
+        {
+          packageName: 'dep2',
+          depType: depTypes.dependencies,
+          registryUrls: ['https://example.com'],
+        },
+      ];
+      const result = await processor.updateArtifacts(
+        {
+          packageFileName: 'pyproject.toml',
+          newPackageFileContent: '',
+          config: {},
+          updatedDeps,
+        },
+        {},
+      );
+      expect(result).toEqual([
+        {
+          file: {
+            contents: 'changed test content',
+            path: 'uv.lock',
+            type: 'addition',
+          },
+        },
+      ]);
+      expect(execSnapshots).toMatchObject([
+        {
+          cmd: 'uv lock --upgrade-package dep1 --upgrade-package dep2',
+          options: {
+            env: {
+              UV_EXTRA_INDEX_URL:
+                'https://foobar.com/ https://user:pass@example.com/',
+            },
+          },
+        },
+      ]);
+    });
+
     it('return update on lockfileMaintenance', async () => {
       const execSnapshots = mockExecAll();
       GlobalConfig.set(adminConfig);
diff --git a/lib/modules/manager/pep621/processors/uv.ts b/lib/modules/manager/pep621/processors/uv.ts
index e59ac557dc..c328e687fd 100644
--- a/lib/modules/manager/pep621/processors/uv.ts
+++ b/lib/modules/manager/pep621/processors/uv.ts
@@ -2,10 +2,14 @@ import is from '@sindresorhus/is';
 import { quote } from 'shlex';
 import { TEMPORARY_ERROR } from '../../../../constants/error-messages';
 import { logger } from '../../../../logger';
+import type { HostRule } from '../../../../types';
 import { exec } from '../../../../util/exec';
 import type { ExecOptions, ToolConstraint } from '../../../../util/exec/types';
 import { getSiblingFileName, readLocalFile } from '../../../../util/fs';
+import { find } from '../../../../util/host-rules';
 import { Result } from '../../../../util/result';
+import { parseUrl } from '../../../../util/url';
+import { PypiDatasource } from '../../../datasource/pypi';
 import type {
   PackageDependency,
   UpdateArtifact,
@@ -115,8 +119,12 @@ export class UvProcessor implements PyProjectProcessor {
         constraint: config.constraints?.uv,
       };
 
+      const extraEnv = {
+        ...getUvExtraIndexUrl(updateArtifact.updatedDeps),
+      };
       const execOptions: ExecOptions = {
         cwdFile: packageFileName,
+        extraEnv,
         docker: {},
         userConfiguredEnv: config.env,
         toolConstraints: [pythonConstraint, uvConstraint],
@@ -191,3 +199,33 @@ function generateCMD(updatedDeps: Upgrade[]): string {
 
   return `${uvUpdateCMD} ${deps.map((dep) => `--upgrade-package ${quote(dep)}`).join(' ')}`;
 }
+
+function getMatchingHostRule(url: string | undefined): HostRule {
+  return find({ hostType: PypiDatasource.id, url });
+}
+
+function getUvExtraIndexUrl(deps: Upgrade[]): NodeJS.ProcessEnv {
+  const registryUrls = new Set(deps.map((dep) => dep.registryUrls).flat());
+  const extraIndexUrls: string[] = [];
+
+  for (const registryUrl of registryUrls) {
+    const parsedUrl = parseUrl(registryUrl);
+    if (!parsedUrl) {
+      continue;
+    }
+
+    const rule = getMatchingHostRule(parsedUrl.toString());
+    if (rule.username) {
+      parsedUrl.username = rule.username;
+    }
+    if (rule.password) {
+      parsedUrl.password = rule.password;
+    }
+
+    extraIndexUrls.push(parsedUrl.toString());
+  }
+
+  return {
+    UV_EXTRA_INDEX_URL: extraIndexUrls.join(' '),
+  };
+}
-- 
GitLab