diff --git a/lib/modules/datasource/util.ts b/lib/modules/datasource/util.ts index abd252282ab05c81850756307a9578a2b7fd888e..b418bb9bb7c560172c4116ba168f158248ed2c35 100644 --- a/lib/modules/datasource/util.ts +++ b/lib/modules/datasource/util.ts @@ -12,7 +12,7 @@ export function isArtifactoryServer<T = unknown>( return is.string(res?.headers[JFROG_ARTIFACTORY_RES_HEADER]); } -export async function getGoogleAuthToken(): Promise<string | null> { +export async function getGoogleAuthTokenRaw(): Promise<string | null> { try { const googleAuth: GoogleAuth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', @@ -21,7 +21,7 @@ export async function getGoogleAuthToken(): Promise<string | null> { if (accessToken) { // sanitize token addSecretForSanitizing(accessToken); - return Buffer.from(`oauth2accesstoken:${accessToken}`).toString('base64'); + return accessToken; } else { logger.warn( 'Could not retrieve access token using google-auth-library getAccessToken', @@ -36,3 +36,11 @@ export async function getGoogleAuthToken(): Promise<string | null> { } return null; } + +export async function getGoogleAuthToken(): Promise<string | null> { + const accessToken = await getGoogleAuthTokenRaw(); + if (accessToken) { + return Buffer.from(`oauth2accesstoken:${accessToken}`).toString('base64'); + } + return null; +} diff --git a/lib/modules/manager/pep621/processors/uv.spec.ts b/lib/modules/manager/pep621/processors/uv.spec.ts index 90d7595422cac995cf4e882cfd5552bf66a80fef..772a3050644d1dcb291c4d9ed2625fb90e16a782 100644 --- a/lib/modules/manager/pep621/processors/uv.spec.ts +++ b/lib/modules/manager/pep621/processors/uv.spec.ts @@ -1,6 +1,12 @@ +import { GoogleAuth as _googleAuth } from 'google-auth-library'; import { join } from 'upath'; import { mockExecAll } from '../../../../../test/exec-util'; -import { fs, hostRules, mockedFunction } from '../../../../../test/util'; +import { + fs, + hostRules, + mocked, + mockedFunction, +} from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { RepoGlobalConfig } from '../../../../config/types'; import { getPkgReleases as _getPkgReleases } from '../../../datasource'; @@ -13,9 +19,11 @@ import type { UpdateArtifactsConfig } from '../../types'; import { depTypes } from '../utils'; import { UvProcessor } from './uv'; +jest.mock('google-auth-library'); jest.mock('../../../../util/fs'); jest.mock('../../../datasource'); +const googleAuth = mocked(_googleAuth); const getPkgReleases = mockedFunction(_getPkgReleases); const config: UpdateArtifactsConfig = {}; @@ -295,6 +303,11 @@ describe('modules/manager/pep621/processors/uv', () => { username: 'user', password: 'pass', }); + googleAuth.mockImplementationOnce( + jest.fn().mockImplementationOnce(() => ({ + getAccessToken: jest.fn().mockResolvedValue('some-token'), + })), + ); fs.getSiblingFileName.mockReturnValueOnce('uv.lock'); fs.readLocalFile.mockResolvedValueOnce('test content'); fs.readLocalFile.mockResolvedValueOnce('changed test content'); @@ -332,6 +345,14 @@ describe('modules/manager/pep621/processors/uv', () => { datasource: GithubTagsDatasource.id, registryUrls: ['https://github.com'], }, + { + packageName: 'dep5', + depType: depTypes.dependencies, + datasource: PypiDatasource.id, + registryUrls: [ + 'https://someregion-python.pkg.dev/some-project/some-repo/', + ], + }, ]; const result = await processor.updateArtifacts( { @@ -353,7 +374,7 @@ describe('modules/manager/pep621/processors/uv', () => { ]); expect(execSnapshots).toMatchObject([ { - cmd: 'uv lock --upgrade-package dep1 --upgrade-package dep2 --upgrade-package dep3 --upgrade-package dep4', + cmd: 'uv lock --upgrade-package dep1 --upgrade-package dep2 --upgrade-package dep3 --upgrade-package dep4 --upgrade-package dep5', options: { env: { GIT_CONFIG_COUNT: '3', @@ -364,7 +385,68 @@ describe('modules/manager/pep621/processors/uv', () => { GIT_CONFIG_VALUE_1: 'git@example.com:', GIT_CONFIG_VALUE_2: 'https://example.com/', UV_EXTRA_INDEX_URL: - 'https://foobar.com/ https://user:pass@example.com/', + 'https://foobar.com/ https://user:pass@example.com/ https://oauth2accesstoken:some-token@someregion-python.pkg.dev/some-project/some-repo/', + }, + }, + }, + ]); + }); + + it('continues if Google auth is not configured', async () => { + const execSnapshots = mockExecAll(); + GlobalConfig.set(adminConfig); + googleAuth.mockImplementation( + jest.fn().mockImplementation(() => ({ + getAccessToken: jest.fn().mockResolvedValue(undefined), + })), + ); + 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: 'dep', + depType: depTypes.dependencies, + datasource: PypiDatasource.id, + registryUrls: [ + 'https://someregion-python.pkg.dev/some-project/some-repo/simple/', + ], + }, + ]; + 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 dep', + options: { + env: { + UV_EXTRA_INDEX_URL: + 'https://someregion-python.pkg.dev/some-project/some-repo/simple/', }, }, }, diff --git a/lib/modules/manager/pep621/processors/uv.ts b/lib/modules/manager/pep621/processors/uv.ts index 10ea762448149306e0ce27708bbdce138f0d6a4f..5de57d565e5c52510669ac483a608c161fa83970 100644 --- a/lib/modules/manager/pep621/processors/uv.ts +++ b/lib/modules/manager/pep621/processors/uv.ts @@ -11,6 +11,7 @@ import { find } from '../../../../util/host-rules'; import { Result } from '../../../../util/result'; import { parseUrl } from '../../../../util/url'; import { PypiDatasource } from '../../../datasource/pypi'; +import { getGoogleAuthTokenRaw } from '../../../datasource/util'; import type { PackageDependency, UpdateArtifact, @@ -131,7 +132,7 @@ export class UvProcessor implements PyProjectProcessor { const extraEnv = { ...getGitEnvironmentVariables(['pep621']), - ...getUvExtraIndexUrl(updateArtifact.updatedDeps), + ...(await getUvExtraIndexUrl(updateArtifact.updatedDeps)), }; const execOptions: ExecOptions = { cwdFile: packageFileName, @@ -216,7 +217,7 @@ function getMatchingHostRule(url: string | undefined): HostRule { return find({ hostType: PypiDatasource.id, url }); } -function getUvExtraIndexUrl(deps: Upgrade[]): NodeJS.ProcessEnv { +async function getUvExtraIndexUrl(deps: Upgrade[]): Promise<NodeJS.ProcessEnv> { const pyPiRegistryUrls = deps .filter((dep) => dep.datasource === PypiDatasource.id) .map((dep) => dep.registryUrls) @@ -231,11 +232,21 @@ function getUvExtraIndexUrl(deps: Upgrade[]): NodeJS.ProcessEnv { } const rule = getMatchingHostRule(parsedUrl.toString()); - if (rule.username) { - parsedUrl.username = rule.username; - } - if (rule.password) { - parsedUrl.password = rule.password; + if (rule.username || rule.password) { + if (rule.username) { + parsedUrl.username = rule.username; + } + if (rule.password) { + parsedUrl.password = rule.password; + } + } else if (parsedUrl.hostname.endsWith('.pkg.dev')) { + const accessToken = await getGoogleAuthTokenRaw(); + if (accessToken) { + parsedUrl.username = 'oauth2accesstoken'; + parsedUrl.password = accessToken; + } else { + logger.once.debug({ registryUrl }, 'Could not get Google access token'); + } } extraIndexUrls.push(parsedUrl.toString());