This commit is contained in:
Maxime Brunet 2025-01-01 01:10:17 +01:00 committed by GitHub
commit 27add0b743
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 142 additions and 8 deletions

View file

@ -16,6 +16,10 @@ url = "last.url"
[[tool.poetry.source]]
name = "five"
[[tool.poetry.source]]
name = "invalid-url"
url = "invalid-url"
[build-system]
requires = ["poetry_core>=1.0", "wheel"]
build-backend = "poetry.masonry.api"

View file

@ -1,4 +1,5 @@
import { codeBlock } from 'common-tags';
import { GoogleAuth as _googleAuth } from 'google-auth-library';
import { mockDeep } from 'jest-mock-extended';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../../test/exec-util';
@ -15,16 +16,26 @@ import { updateArtifacts } from '.';
const pyproject1toml = Fixtures.get('pyproject.1.toml');
const pyproject10toml = Fixtures.get('pyproject.10.toml');
const pyproject13toml = `[[tool.poetry.source]]
name = "some-gar-repo"
url = "https://someregion-python.pkg.dev/some-project/some-repo/simple/"
[build-system]
requires = ["poetry_core>=1.0", "wheel"]
build-backend = "poetry.masonry.api"
`;
jest.mock('../../../util/exec/env');
jest.mock('../../../util/fs');
jest.mock('../../datasource', () => mockDeep());
jest.mock('../../../util/host-rules', () => mockDeep());
jest.mock('google-auth-library');
process.env.CONTAINERBASE = 'true';
const datasource = mocked(_datasource);
const hostRules = mocked(_hostRules);
const googleAuth = mocked(_googleAuth);
const adminConfig: RepoGlobalConfig = {
localDir: join('/tmp/github/some/repo'),
@ -198,7 +209,99 @@ describe('modules/manager/poetry/artifacts', () => {
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(5);
expect(hostRules.find.mock.calls).toHaveLength(7);
expect(execSnapshots).toMatchObject([
{
cmd: 'poetry update --lock --no-interaction dep1',
options: {
env: {
POETRY_HTTP_BASIC_ONE_PASSWORD: 'passwordOne',
POETRY_HTTP_BASIC_ONE_USERNAME: 'usernameOne',
POETRY_HTTP_BASIC_TWO_USERNAME: 'usernameTwo',
POETRY_HTTP_BASIC_FOUR_OH_FOUR_PASSWORD: 'passwordFour',
},
},
},
]);
});
it('passes Google Artifact Registry credentials environment vars', async () => {
// poetry.lock
fs.getSiblingFileName.mockReturnValueOnce('poetry.lock');
fs.readLocalFile.mockResolvedValueOnce(null);
// pyproject.lock
fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock');
fs.readLocalFile.mockResolvedValueOnce('[metadata]\n');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('New poetry.lock');
googleAuth.mockImplementationOnce(
jest.fn().mockImplementationOnce(() => ({
getAccessToken: jest.fn().mockResolvedValue('some-token'),
})),
);
const updatedDeps = [{ depName: 'dep1' }];
expect(
await updateArtifacts({
packageFileName: 'pyproject.toml',
updatedDeps,
newPackageFileContent: pyproject13toml,
config,
}),
).toEqual([
{
file: {
type: 'addition',
path: 'pyproject.lock',
contents: 'New poetry.lock',
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(3);
expect(execSnapshots).toMatchObject([
{
cmd: 'poetry update --lock --no-interaction dep1',
options: {
env: {
POETRY_HTTP_BASIC_SOME_GAR_REPO_USERNAME: 'oauth2accesstoken',
POETRY_HTTP_BASIC_SOME_GAR_REPO_PASSWORD: 'some-token',
},
},
},
]);
});
it('continues if Google auth is not configured', async () => {
// poetry.lock
fs.getSiblingFileName.mockReturnValueOnce('poetry.lock');
fs.readLocalFile.mockResolvedValueOnce(null);
// pyproject.lock
fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock');
fs.readLocalFile.mockResolvedValueOnce('[metadata]\n');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('New poetry.lock');
googleAuth.mockImplementation(
jest.fn().mockImplementation(() => ({
getAccessToken: jest.fn().mockResolvedValue(undefined),
})),
);
const updatedDeps = [{ depName: 'dep1' }];
expect(
await updateArtifacts({
packageFileName: 'pyproject.toml',
updatedDeps,
newPackageFileContent: pyproject13toml,
config,
}),
).toEqual([
{
file: {
type: 'addition',
path: 'pyproject.lock',
contents: 'New poetry.lock',
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(3);
expect(execSnapshots).toMatchObject([
{ cmd: 'poetry update --lock --no-interaction dep1' },
]);

View file

@ -17,7 +17,9 @@ import { find } from '../../../util/host-rules';
import { regEx } from '../../../util/regex';
import { Result } from '../../../util/result';
import { parse as parseToml } from '../../../util/toml';
import { parseUrl } from '../../../util/url';
import { PypiDatasource } from '../../datasource/pypi';
import { getGoogleAuthTokenRaw } from '../../datasource/util';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import { Lockfile, PoetrySchemaToml } from './schema';
import type { PoetryFile, PoetrySource } from './types';
@ -101,7 +103,7 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
return [];
}
if (!pyprojectFile.tool?.poetry) {
logger.debug(`{$fileName} contains no poetry section`);
logger.debug(`${fileName} contains no poetry section`);
return [];
}
@ -115,20 +117,42 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
return sourceArray;
}
function getMatchingHostRule(url: string | undefined): HostRule {
async function getMatchingHostRule(url: string | undefined): Promise<HostRule> {
const scopedMatch = find({ hostType: PypiDatasource.id, url });
return is.nonEmptyObject(scopedMatch) ? scopedMatch : find({ url });
const hostRule = is.nonEmptyObject(scopedMatch) ? scopedMatch : find({ url });
if (hostRule) {
return hostRule;
}
const parsedUrl = parseUrl(url);
if (!parsedUrl) {
logger.once.debug(`Failed to parse URL ${url}`);
return {};
}
if (parsedUrl.hostname.endsWith('.pkg.dev')) {
const accessToken = await getGoogleAuthTokenRaw();
if (accessToken) {
return {
username: 'oauth2accesstoken',
password: accessToken,
};
}
logger.once.debug(`Could not get Google access token (url=${url})`);
}
return {};
}
function getSourceCredentialVars(
async function getSourceCredentialVars(
pyprojectContent: string,
packageFileName: string,
): NodeJS.ProcessEnv {
): Promise<NodeJS.ProcessEnv> {
const poetrySources = getPoetrySources(pyprojectContent, packageFileName);
const envVars: NodeJS.ProcessEnv = {};
for (const source of poetrySources) {
const matchingHostRule = getMatchingHostRule(source.url);
const matchingHostRule = await getMatchingHostRule(source.url);
const formattedSourceName = source.name
.replace(regEx(/(\.|-)+/g), '_')
.toUpperCase();
@ -192,7 +216,10 @@ export async function updateArtifacts({
config.constraints?.poetry ??
getPoetryRequirement(newPackageFileContent, existingLockFileContent);
const extraEnv = {
...getSourceCredentialVars(newPackageFileContent, packageFileName),
...(await getSourceCredentialVars(
newPackageFileContent,
packageFileName,
)),
...getGitEnvironmentVariables(['poetry']),
PIP_CACHE_DIR: await ensureCacheDir('pip'),
};