mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-02 10:33:39 +00:00
795d083264
* feat(datasource): add APK datasource support * remove missing manager link for now * lint docs * update tests * replace binary fixtures * fix coverage * Update lib/modules/datasource/apk/readme.md Co-authored-by: Jamie Tanna <github@jamietanna.co.uk> * Apply suggestion from @viceice Co-authored-by: Michael Kriese <michael.kriese@gmx.de> * use tar instead of tar-stream * use codeBlock * add ignore back * make method private * use an async extract instead of list * add usage example * stream apkindex download and parse line-by-line * refactor(datasource): address review comments on APK datasource Co-authored-by: Cursor <cursoragent@cursor.com> * chore: sync pnpm-lock.yaml after merge Co-authored-by: Cursor <cursoragent@cursor.com> * test: remove ignored lines As they seem to be covered, anyway. * fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource * fixup! fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource * lint: use `node:` prefix * Update lib/modules/datasource/apk/index.ts Co-authored-by: Michael Kriese <michael.kriese@gmx.de> * refactor(datasource/apk): use asTimestamp millis instead of new Date Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(datasource/apk): rename uncached method to _getPackages Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(datasource/apk): encode registry URL parts as query parameters Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): reject invalid registry URLs, skip caching empty indexes Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): throw on unparseable registry URL and drop unset sourceUrl support Co-authored-by: Cursor <cursoragent@cursor.com> * docs(datasource/apk): show how registry URL parameters build the index URLs Co-authored-by: Cursor <cursoragent@cursor.com> * Apply suggestions from code review Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com> * fix(datasource/apk): keep credentials and port in component URLs Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): extract APKINDEX by basename and key packages by name Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): report a version served by two components only once Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Jamie Tanna <github@jamietanna.co.uk> Co-authored-by: Michael Kriese <michael.kriese@gmx.de> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jamie Tanna <jamie.tanna@mend.io> Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
import { isNotNullOrUndefined } from '../../../util/array.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { joinUrlParts, parseUrl } from '../../../util/url.ts';
|
|
|
|
const REQUIRED_PARAMS = ['arch'];
|
|
const OPTIONAL_PARAMS = ['branch', 'components'];
|
|
const KNOWN_PARAMS = [...REQUIRED_PARAMS, ...OPTIONAL_PARAMS];
|
|
|
|
// Parameter values become path segments verbatim, so anything which could escape
|
|
// or reshape the resulting URL is rejected
|
|
const validSegment = regEx(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/);
|
|
|
|
/**
|
|
* Constructs the `APKINDEX` directory URLs from the given registry URL.
|
|
*
|
|
* @param registryUrl - The base URL of the registry, with the path segments encoded as query parameters.
|
|
* @returns One URL per component.
|
|
* @throws Will throw an error if the registry URL cannot be parsed, or if the query
|
|
* parameters are missing, unknown or malformed.
|
|
*
|
|
* @example
|
|
* // Returns ['https://dl-cdn.alpinelinux.org/alpine/v3.19/main/x86_64',
|
|
* // 'https://dl-cdn.alpinelinux.org/alpine/v3.19/community/x86_64']
|
|
* constructComponentUrls('https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64');
|
|
*/
|
|
export function constructComponentUrls(registryUrl: string): string[] {
|
|
try {
|
|
const url = parseUrl(registryUrl);
|
|
if (!url) {
|
|
throw new Error('Cannot parse URL');
|
|
}
|
|
|
|
validateParams(url);
|
|
|
|
const arch = validateSegment('arch', url.searchParams.get('arch')!);
|
|
const branch = getBranch(url);
|
|
const components = getComponents(url);
|
|
|
|
// The parameters describe the path, so they are stripped before the segments
|
|
// are appended. Unknown parameters are rejected above, so nothing else remains.
|
|
for (const param of KNOWN_PARAMS) {
|
|
url.searchParams.delete(param);
|
|
}
|
|
url.hash = '';
|
|
const baseUrl = url.toString();
|
|
|
|
return components.map((component) =>
|
|
joinUrlParts(
|
|
baseUrl,
|
|
...[branch, component, arch].filter(isNotNullOrUndefined),
|
|
),
|
|
);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Invalid apk repo URL: ${registryUrl} - see documentation: ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that the required parameters are present, and that no unknown parameters are given.
|
|
*
|
|
* Unknown parameters are rejected so that a typo such as `component=main` fails loudly
|
|
* instead of being silently treated as a repository without components.
|
|
*
|
|
* @param url - The URL to validate.
|
|
* @throws Will throw an error if a required parameter is missing or an unknown parameter is present.
|
|
*/
|
|
function validateParams(url: URL): void {
|
|
for (const param of REQUIRED_PARAMS) {
|
|
if (!url.searchParams.has(param)) {
|
|
throw new Error(`Missing required query parameter '${param}'`);
|
|
}
|
|
}
|
|
|
|
for (const param of url.searchParams.keys()) {
|
|
if (!KNOWN_PARAMS.includes(param)) {
|
|
throw new Error(`Unknown query parameter '${param}'`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates a single parameter value which is used as a path segment.
|
|
*
|
|
* @param param - The name of the query parameter, used for the error message.
|
|
* @param value - The value to validate.
|
|
* @returns The validated value.
|
|
* @throws Will throw an error if the value is not usable as a path segment.
|
|
*/
|
|
function validateSegment(param: string, value: string): string {
|
|
if (!validSegment.test(value)) {
|
|
throw new Error(`Invalid '${param}' query parameter: '${value}'`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the branch from the URL.
|
|
*
|
|
* Repositories such as Wolfi have no branch in their path, so `null` is returned
|
|
* when `branch` is absent.
|
|
*
|
|
* @param url - The URL to retrieve the branch from.
|
|
* @returns The branch, or `null` when there is none.
|
|
*/
|
|
function getBranch(url: URL): string | null {
|
|
const branch = url.searchParams.get('branch');
|
|
|
|
return branch === null ? null : validateSegment('branch', branch);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the components from the URL.
|
|
*
|
|
* Repositories such as Wolfi serve their index directly below the repository
|
|
* root, so a single component-less URL is returned when `components` is absent.
|
|
*
|
|
* @param url - The URL to retrieve the components from.
|
|
* @returns The list of components, or `[undefined]` when there are none.
|
|
*/
|
|
function getComponents(url: URL): (string | undefined)[] {
|
|
const components = url.searchParams
|
|
.get('components')
|
|
?.split(',')
|
|
.map((component) => component.trim())
|
|
.filter((component) => component !== '')
|
|
.map((component) => validateSegment('components', component));
|
|
|
|
return components?.length ? components : [undefined];
|
|
}
|