diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index b4d60f24dfd9ea34e52b9c580082f0cd259efc40..11804223a52e223c6260d57af9ae8f29205691d1 100644 --- a/docs/usage/getting-started/private-packages.md +++ b/docs/usage/getting-started/private-packages.md @@ -325,7 +325,35 @@ For those found, a command similar to the following is run: `dotnet nuget add so ### poetry -For every poetry source, a `hostRules` search is done and then any found credentials are added to env like `POETRY_HTTP_BASIC_X_USERNAME` and `POETRY_HTTP_BASIC_X_PASSWORD`. +For every Poetry source, a `hostRules` search is done and then any found credentials are added to env like `POETRY_HTTP_BASIC_X_USERNAME` and `POETRY_HTTP_BASIC_X_PASSWORD`, where `X` represents the normalized name of the source in `pyproject.toml`. + +```js +module.exports = { + hostRules: [ + { + matchHost: 'pypi.example.com', + hostType: 'pypi', + username: process.env.PYPI_USERNAME, + password: process.env.PYPI_PASSWORD, + }, + ], +}; +``` + +If you're self-hosting Renovate via the [GitLab Runner](../getting-started/running.md#gitlab-runner) and want to access packages from private GitLab registries, you can use the GitLab CI job token for authentication: + +```js +module.exports = { + hostRules: [ + { + matchHost: 'gitlab.example.com', + hostType: 'pypi', + username: 'gitlab-ci-token', + password: process.env.CI_JOB_TOKEN, + }, + ], +}; +``` ## WhiteSource Renovate Hosted App Encryption diff --git a/lib/manager/poetry/__fixtures__/pyproject.10.toml b/lib/manager/poetry/__fixtures__/pyproject.10.toml index 3b2499c44b0379c81f124ddaa75086c85a891aec..14c589fab7736df2c332883be90d1d017f0975e4 100644 --- a/lib/manager/poetry/__fixtures__/pyproject.10.toml +++ b/lib/manager/poetry/__fixtures__/pyproject.10.toml @@ -10,7 +10,7 @@ url = "another.url" url = "missingname.url" [[tool.poetry.source]] -name = "four" +name = "four-oh.four" url = "last.url" [[tool.poetry.source]] diff --git a/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap b/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap index 89b376fad158ddf8a906d86009f9fae9c8df000d..eba67540534e64ad358db06742ce910d28ae4201 100644 --- a/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap +++ b/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap @@ -27,7 +27,7 @@ Array [ "LC_ALL": "en_US", "NO_PROXY": "localhost", "PATH": "/tmp/path", - "POETRY_HTTP_BASIC_FOUR_PASSWORD": "passwordFour", + "POETRY_HTTP_BASIC_FOUR_OH_FOUR_PASSWORD": "passwordFour", "POETRY_HTTP_BASIC_ONE_PASSWORD": "passwordOne", "POETRY_HTTP_BASIC_ONE_USERNAME": "usernameOne", "POETRY_HTTP_BASIC_TWO_USERNAME": "usernameTwo", @@ -39,6 +39,30 @@ Array [ ] `; +exports[`manager/poetry/artifacts prioritizes pypi-scoped credentials 1`] = ` +Array [ + Object { + "cmd": "poetry update --lock --no-interaction dep1", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "POETRY_HTTP_BASIC_ONE_PASSWORD": "scoped-password", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + exports[`manager/poetry/artifacts returns null if unchanged 1`] = ` Array [ Object { diff --git a/lib/manager/poetry/artifacts.spec.ts b/lib/manager/poetry/artifacts.spec.ts index 07e2cc5fff31b69a9acf413eb6d412713fc83625..6dcbb618d8b6607249e4b15d3d49742abf5aa117 100644 --- a/lib/manager/poetry/artifacts.spec.ts +++ b/lib/manager/poetry/artifacts.spec.ts @@ -101,6 +101,7 @@ describe('manager/poetry/artifacts', () => { password: 'passwordOne', }); hostRules.find.mockReturnValueOnce({ username: 'usernameTwo' }); + hostRules.find.mockReturnValueOnce({}); hostRules.find.mockReturnValueOnce({ password: 'passwordFour' }); const updatedDeps = [{ depName: 'dep1' }]; expect( @@ -111,7 +112,31 @@ describe('manager/poetry/artifacts', () => { config, }) ).not.toBeNull(); - expect(hostRules.find.mock.calls).toHaveLength(3); + expect(hostRules.find.mock.calls).toHaveLength(4); + expect(execSnapshots).toMatchSnapshot(); + }); + it('prioritizes pypi-scoped credentials', async () => { + fs.readFile.mockResolvedValueOnce(null); + fs.readFile.mockResolvedValueOnce(Buffer.from('[metadata]\n')); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockResolvedValueOnce(Buffer.from('New poetry.lock')); + hostRules.find.mockImplementation((search) => ({ + password: + search.hostType === 'pypi' ? 'scoped-password' : 'unscoped-password', + })); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: ` + [[tool.poetry.source]] + name = "one" + url = "some.url" + `, + config, + }) + ).not.toBeNull(); expect(execSnapshots).toMatchSnapshot(); }); it('returns updated pyproject.lock', async () => { diff --git a/lib/manager/poetry/artifacts.ts b/lib/manager/poetry/artifacts.ts index 69ad411f812351f723689d6cbb475160096d923f..21e63611e5a46e347df6d8133b7b84084e683c30 100644 --- a/lib/manager/poetry/artifacts.ts +++ b/lib/manager/poetry/artifacts.ts @@ -2,7 +2,9 @@ import { parse } from '@iarna/toml'; import is from '@sindresorhus/is'; import { quote } from 'shlex'; import { TEMPORARY_ERROR } from '../../constants/error-messages'; +import { PypiDatasource } from '../../datasource/pypi'; import { logger } from '../../logger'; +import type { HostRule } from '../../types'; import { exec } from '../../util/exec'; import type { ExecOptions, ToolConstraint } from '../../util/exec/types'; import { @@ -99,6 +101,13 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] { return sourceArray; } +function getMatchingHostRule(source: PoetrySource): HostRule { + const scopedMatch = find({ hostType: PypiDatasource.id, url: source.url }); + return is.nonEmptyObject(scopedMatch) + ? scopedMatch + : find({ url: source.url }); +} + function getSourceCredentialVars( pyprojectContent: string, packageFileName: string @@ -107,8 +116,10 @@ function getSourceCredentialVars( const envVars: Record<string, string> = {}; for (const source of poetrySources) { - const matchingHostRule = find({ url: source.url }); - const formattedSourceName = source.name.toUpperCase(); + const matchingHostRule = getMatchingHostRule(source); + const formattedSourceName = source.name + .replace(regEx(/(\.|-)+/g), '_') + .toUpperCase(); if (matchingHostRule.username) { envVars[`POETRY_HTTP_BASIC_${formattedSourceName}_USERNAME`] = matchingHostRule.username;