mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 15:01:49 +00:00
f0dd862bd5
* refactor(python): share PyPI index credential resolution
pipenv, poetry, uv, pip-compile and the pypi datasource each resolved
index credentials differently, so a host rule that worked for one Python
manager did not necessarily work for the next. Add
`findPypiIndexCredentials()` in `lib/modules/datasource/pypi/host-rules.ts`,
which strips the URL's own `user:password@` part, runs a `pypi`-scoped
`hostRules.find()`, and only falls back to Google Artifact Registry when
the matched rule has no username and no password. The repeated
`.pkg.dev` test moves into a single `isGoogleArtifactRegistry()`
predicate next to `getGoogleAuthHostRule()`.
Behaviour changes:
- pipenv now supports Google Artifact Registry; previously it only ever
consulted host rules.
Spec: manager/pipenv/artifacts > "passes Google Artifact Registry
credential environment vars"
- pip-compile now supports Google Artifact Registry.
Spec: manager/pip-compile/common > getRegistryCredVarsFromPackageFiles()
> "supports Google Artifact Registry"
- pip-compile now matches `hostType: pypi` rules; its find call was
unscoped, so such rules were skipped.
Spec: manager/pip-compile/common > getRegistryCredVarsFromPackageFiles()
> "ignores a rule scoped to another hostType"
- pip-compile now matches a `matchHost` that contains a path; it reduced
every registry URL to its origin first, and `matchesHost()` compares
with `href.startsWith()`, so a path-scoped rule could never match.
Spec: manager/pip-compile/common > getRegistryCredVarsFromPackageFiles()
> "matches a rule scoped to a path"
- poetry and uv now strip `${USER}:${PASS}@` style placeholders from the
index URL before matching, so a `matchHost` can match those URLs too.
Spec: datasource/pypi/host-rules > "strips credentials from the URL
before matching"
- poetry now falls back to Google Artifact Registry when the matched rule
carries no username or password; any non-empty rule used to suppress
the fallback. Its dead unscoped `find({ url })` fallback is gone: a
`pypi`-scoped find already matches every rule an unscoped one would.
Spec: manager/poetry/artifacts > "falls back to Google auth when the
matching rule has no credentials"
- the pypi datasource now prefers host rule credentials over the Google
token for `*.pkg.dev`; it took the Google token unconditionally, and
the resulting `authorization` header made `applyHostRule()` skip the
configured credentials.
Spec: datasource/pypi/index > getReleases > "prefers host rule
credentials over Google Auth"
`getRegistryCredEnvVars()` also no longer logs the whole matched host
rule as the log merge object.
* test(pipenv): inline the Google Artifact Registry Pipfile
The fixture was used by a single spec case; inline it as requested in the
review of #46127.
* test(datasource/pypi): cover partial Google Artifact Registry credentials
A host rule that carries only a username or only a password still produces a
Basic header, but nothing exercised those two `?? ''` fallbacks, so the merge
queue failed the global branch threshold.
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import { logger } from '~test/util.ts';
|
|
import {
|
|
isCrossOriginPaginationAllowed,
|
|
isGoogleArtifactRegistry,
|
|
resolvePaginationUrl,
|
|
} from './util.ts';
|
|
|
|
describe('modules/datasource/util', () => {
|
|
describe('isCrossOriginPaginationAllowed', () => {
|
|
it('returns false when the flag is unset', () => {
|
|
expect(isCrossOriginPaginationAllowed('docker')).toBe(false);
|
|
expect(isCrossOriginPaginationAllowed('nuget')).toBe(false);
|
|
});
|
|
|
|
it('returns true only for the opted-in datasource', () => {
|
|
vi.stubEnv('RENOVATE_X_DOCKER_PAGINATION_ALLOW_CROSS_ORIGIN', 'true');
|
|
expect(isCrossOriginPaginationAllowed('docker')).toBe(true);
|
|
// per-datasource: docker's flag must not opt nuget in
|
|
expect(isCrossOriginPaginationAllowed('nuget')).toBe(false);
|
|
});
|
|
|
|
it('reads a separate flag for nuget', () => {
|
|
vi.stubEnv('RENOVATE_X_NUGET_PAGINATION_ALLOW_CROSS_ORIGIN', 'true');
|
|
expect(isCrossOriginPaginationAllowed('nuget')).toBe(true);
|
|
expect(isCrossOriginPaginationAllowed('docker')).toBe(false);
|
|
});
|
|
|
|
it('returns false for a datasource without a flag', () => {
|
|
expect(isCrossOriginPaginationAllowed('npm')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isGoogleArtifactRegistry', () => {
|
|
it.each`
|
|
hostname | expected
|
|
${'someregion-python.pkg.dev'} | ${true}
|
|
${'someregion-docker.pkg.dev'} | ${true}
|
|
${'pkg.dev.example.com'} | ${false}
|
|
${'pypi.org'} | ${false}
|
|
`('$hostname -> $expected', ({ hostname, expected }) => {
|
|
expect(isGoogleArtifactRegistry(hostname)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('resolvePaginationUrl', () => {
|
|
it('resolves a same-origin link when cross-origin is not allowed', () => {
|
|
expect(
|
|
resolvePaginationUrl(
|
|
'https://reg.example.com/v2/foo?n=10',
|
|
'https://reg.example.com/v2/foo?n=10&last=z',
|
|
false,
|
|
),
|
|
).toBe('https://reg.example.com/v2/foo?n=10&last=z');
|
|
});
|
|
|
|
it('drops a cross-origin link when cross-origin is not allowed', () => {
|
|
expect(
|
|
resolvePaginationUrl(
|
|
'https://reg.example.com/v2/foo',
|
|
'https://attacker.example.com/v2/foo',
|
|
false,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('follows a cross-origin link when allowed', () => {
|
|
expect(
|
|
resolvePaginationUrl(
|
|
'https://reg.example.com/v2/foo',
|
|
'https://cdn.example.com/v2/foo?page=2',
|
|
true,
|
|
),
|
|
).toBe('https://cdn.example.com/v2/foo?page=2');
|
|
});
|
|
|
|
it('logs a warning when following a cross-origin link when allowed', () => {
|
|
resolvePaginationUrl(
|
|
'https://reg.example.com/v2/foo',
|
|
'https://cdn.example.com/v2/foo?page=2',
|
|
true,
|
|
);
|
|
|
|
expect(logger.logger.once.warn).toHaveBeenCalledWith(
|
|
{
|
|
baseUrl: 'https://reg.example.com/v2/foo',
|
|
nextUrl: 'https://cdn.example.com/v2/foo?page=2',
|
|
},
|
|
'Following cross-origin pagination link',
|
|
);
|
|
});
|
|
|
|
it('resolves a relative link when cross-origin is allowed', () => {
|
|
expect(
|
|
resolvePaginationUrl(
|
|
'https://reg.example.com/v2/foo',
|
|
'/v2/foo?page=2',
|
|
true,
|
|
),
|
|
).toBe('https://reg.example.com/v2/foo?page=2');
|
|
});
|
|
|
|
it('returns null for an invalid link when cross-origin is allowed', () => {
|
|
expect(
|
|
resolvePaginationUrl('https://reg.example.com/v2/', 'http://', true),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
});
|