mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 17:51:49 +00:00
59852479bf
* test(versioning): cover four uncovered guards - `cargo`: a converted range whose operators are already attached to their versions, so `npm2cargo` leaves the elements alone - `hashicorp`: a widen on a complex range ending in `>=`, which the underlying semver implementation refuses and returns null for - `poetry`: a version list holding an unparseable entry, for both `getSatisfyingVersion` and `minSatisfyingVersion`, plus the same widen refusal, which falls back to the current value - `swift`: an open-ended range whose bound is not a valid semver Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(versioning): drop two guards that cannot be false - `hermit`: `major` is the only mandatory group in the version regex, so the `if (major)` around the first release segment always holds; the sibling parser already pushes it unconditionally - `rpm`: after `c1 === c2` is ruled out, one of `c1 > c2` and `c1 < c2` must hold, so the second comparison becomes a ternary Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(versioning/gradle): cover three uncovered guards - `getMajor` on a version whose leading token is not a number - `getSatisfyingVersion` with a later match that is not greater than the one already held - `minSatisfyingVersion` with a later match that is not smaller than the one already held Two branches in `compare.ts` are still open and are looked at next. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(versioning/ivy): cover a later match that is not the greatest `getSatisfyingVersion` kept every matching version it walked past, so the guard that holds on to the one already found was never taken. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(versioning): drop three guards that cannot be false - `gradle`: `tokenCmp` compares two tokens whose only types are `Number` and `String`, so once both-numbers and both-strings are handled exactly one side is a number; the chain becomes a ternary and the unreachable `return 0` goes - `ivy`: `tokenize` always yields at least one token for a non-empty version, so the length check around the last one always holds - `ivy`: `rangeToStr` only returns null for a null range, which the `length === 1` check above it already rules out Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(versioning/nuget): cover a later match that is not the greatest Both loops in `getSatisfyingVersion` walked version lists that only ever climbed, so the guard that holds on to the candidate already found was never taken -- once for a bracket range, once for a bare version acting as a minimum. Also pin down that a floating range needs a major component: `-*` alone does not parse. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(versioning): drop three more guards that cannot be false - `maven`: the interval picked for auto-extension is only picked when the new value is at or past its upper bound, and that upper bound is the lower one incremented, so the new value never sorts below the lower bound - `maven`: an interval never has both bounds open, because the parser only nulls one side when the other is a version - `nuget`: a floating range always carries a major component, so neither the parser's NaN check nor the serializer's undefined check can fail Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(versioning/pep440): drop three unreachable range paths `updateRangeValue` handles `!=`, a `.*` prefix, `~=`, `==` and `<=` itself, `handleUpperBound` handles `<`, and `parseCurrentRange` rejects `===`. Only `>` and `>=` can reach `handleLowerBound`, so its operator check always holds and it never returns null. Making it total removes the null check at the call site as well. This does drop the `logger.error` that followed, which was guarded by a comment saying it could not happen unless pep440 itself changed. `isLessThanRange` classifies the same fixed operator set, so the last check there is the only one left and its fallback was dead. `divideCompatibleReleaseRange` only ever sees a `~=` version, and pep440 requires at least two release components after that operator, so there is always a component to drop. A new test pins down that `~=7` is not a valid range. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(versioning): cover three more uncovered guards - `elm`: a widen where the new version sits below the lower bound, so the upper bound is kept rather than extended - `hex`: a widen on a complex range ending in `>=`, which the underlying semver implementation refuses and returns null for - `rust-release-channel`: a concrete current version paired with a channel name as the new version, which falls through to the new version unchanged Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(versioning): drop two more guards that cannot be false - `composer`: `getNewValue` falls back to the new version whenever nothing else matched, so it never returns null and the recursive call in the or-range path always has a value to work with - `docker`: a version's prerelease is always a string, so once an empty one on either side is handled, the remaining pair is both non-empty and unequal Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(config,datasource): cover two more uncovered guards - `config/validation`: `hostRules[].allowInternal` nested inside a `packageRules[]` entry, which reports a plain configuration error rather than a security one, because nested host rules are never applied - `datasource/docker`: a non-list manifest whose response carries no `docker-content-digest` header, so the digest falls back to the hash of the manifest body Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(config,datasource): unwind three branches v8 could not count - `config/decrypt` and `datasource/docker/common` each picked a response with an `await` inside a ternary. v8 gives the branch that follows such a ternary a negative hit count, which the reporters read as uncovered even though both paths run. Spelling the choice as an if/else makes the count come out right; behaviour is unchanged. - `datasource/index` and `datasource/npm/npmrc` each ended an exhaustive chain with an `else if` whose implicit else v8 counts but nothing can reach: `registryStrategy` is one of three literals, and `npmrc` starts as an object and is only ever reassigned to one. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(workers/global): cover seven uncovered guards The config parsers rewrite legacy boolean spellings of `dryRun`, `requireConfig` and `platformCommit`, but nothing passed a value that was already current, so the fall-through of each chain went untaken. Cover it from both the CLI and the environment. Also cover a datasource env var that names no auth field, one whose value is undefined, a default config file that cannot be read, and the error `isLimitReached` throws when asked for a concurrent limit without a branch config. That last one had an ignore hint saying it should not happen; the error message documents the contract, so it is now tested and the hint is gone. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(workers/global): drop a guard that cannot be false `getConfig` in the additional-config-file parser returns early unless `RENOVATE_ADDITIONAL_CONFIG_FILE` is set, so by the time a parse error is being classified the variable is always present. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(workers/repository): drop a dead changelog file lookup The gitea, gitlab and forgejo changelog sources each declared `files` as an empty array and then guarded the only assignment to it with `if (!files.length)`, which is always true one line after the declaration. Collapse it to the filter itself. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * ci: ratchet the branch coverage threshold A full run on this branch reports 100% statements (49471/49471), 100% functions (7381/7381), 100% lines (48961/48961) and 99.73% branches (25690/25759), up from 99.53% before this branch's work. The threshold moves to 99.71, keeping the same rounding-down margin as before. Statements, functions and lines are already pinned at 100 and stay there. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(config,datasource): say why the ternary was unwound Both sites now carry a comment explaining that the if/else is deliberate: v8 gives the branch following an `await` inside a ternary a negative hit count, which the coverage reporters read as uncovered. `apiCheckResponse` also gets an explicit `HttpResponse<unknown>` type, which is what the two branches have in common and what `isECRMaxResultsResponse` already expects. Also fix a type error in the new config file spec: the `pathExists` spy needs the same `as never` cast the surrounding tests use. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * refactor(versioning/pep440): use a template literal in handleLowerBound The exclude-style return is the one line this branch rewrote in that function, so it now builds its string with a template literal instead of the `+` operator. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(workers/global): drop the cast from the limits throw test Calling the overload with a non-null assertion on the config argument says the same thing without retyping the whole function. Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
575 lines
18 KiB
TypeScript
575 lines
18 KiB
TypeScript
import { ATTR_CODE_FUNCTION_NAME } from '@opentelemetry/semantic-conventions';
|
|
import {
|
|
isFunction,
|
|
isNonEmptyArray,
|
|
isString,
|
|
isTruthy,
|
|
} from '@sindresorhus/is';
|
|
import { dequal } from 'dequal';
|
|
import { GlobalConfig } from '../../config/global.ts';
|
|
import { HOST_BLOCKED, HOST_DISABLED } from '../../constants/error-messages.ts';
|
|
import { instrument } from '../../instrumentation/index.ts';
|
|
import {
|
|
ATTR_RENOVATE_DATASOURCE,
|
|
ATTR_RENOVATE_PACKAGE_NAME,
|
|
ATTR_RENOVATE_REGISTRY_URL,
|
|
} from '../../instrumentation/types.ts';
|
|
import { logger } from '../../logger/index.ts';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
|
|
import { coerceArray } from '../../util/array.ts';
|
|
import * as memCache from '../../util/cache/memory/index.ts';
|
|
import * as packageCache from '../../util/cache/package/index.ts';
|
|
import type { PackageCacheNamespace } from '../../util/cache/package/types.ts';
|
|
import { clone } from '../../util/clone.ts';
|
|
import { filterMap } from '../../util/filter-map.ts';
|
|
import { AsyncResult, Result } from '../../util/result.ts';
|
|
import { DatasourceCacheStats } from '../../util/stats.ts';
|
|
import { safeStringify } from '../../util/stringify.ts';
|
|
import { trimTrailingSlash } from '../../util/url.ts';
|
|
import * as versioning from '../versioning/index.ts';
|
|
import datasources from './api.ts';
|
|
import {
|
|
applyConstraintsFiltering,
|
|
applyExtractVersion,
|
|
applyVersionCompatibility,
|
|
filterValidVersions,
|
|
getDatasourceFor,
|
|
sortAndRemoveDuplicates,
|
|
} from './common.ts';
|
|
import { addMetaData } from './metadata.ts';
|
|
import { setNpmrc } from './npm/index.ts';
|
|
import { resolveRegistryUrl } from './npm/npmrc.ts';
|
|
import type {
|
|
DatasourceApi,
|
|
DigestConfig,
|
|
GetDigestInputConfig,
|
|
GetPkgReleasesConfig,
|
|
GetReleasesConfig,
|
|
ReleaseResult,
|
|
} from './types.ts';
|
|
|
|
export { isGetPkgReleasesConfig } from './common.ts';
|
|
export * from './types.ts';
|
|
|
|
export function getDatasources(): Map<string, DatasourceApi> {
|
|
return datasources;
|
|
}
|
|
export function getDatasourceList(): string[] {
|
|
return Array.from(datasources.keys());
|
|
}
|
|
|
|
type GetReleasesInternalConfig = GetReleasesConfig & GetPkgReleasesConfig;
|
|
|
|
// TODO: fix error Type
|
|
function logError(datasource: string, packageName: string, err: any): void {
|
|
const { statusCode, code: errCode, url } = err;
|
|
if (statusCode === 404) {
|
|
logger.debug({ datasource, packageName, url }, 'Datasource 404');
|
|
} else if (statusCode === 401 || statusCode === 403) {
|
|
logger.debug({ datasource, packageName, url }, 'Datasource unauthorized');
|
|
} else if (errCode) {
|
|
logger.debug(
|
|
{ datasource, packageName, url, errCode },
|
|
'Datasource connection error',
|
|
);
|
|
} else {
|
|
logger.debug({ datasource, packageName, err }, 'Datasource unknown error');
|
|
}
|
|
}
|
|
|
|
async function getRegistryReleases(
|
|
datasource: DatasourceApi,
|
|
config: GetReleasesConfig,
|
|
registryUrl: string,
|
|
): Promise<ReleaseResult | null> {
|
|
const cacheNamespace: PackageCacheNamespace = `datasource-releases-${datasource.id}`;
|
|
const cacheKey = `${registryUrl}:${config.packageName}`;
|
|
|
|
const cacheEnabled = !!datasource.caching; // tells if `isPrivate` flag is supported in datasource result
|
|
const cacheForced = GlobalConfig.get('cachePrivatePackages'); // tells if caching is forced via admin config
|
|
|
|
if (cacheEnabled || cacheForced) {
|
|
const cachedResult = await packageCache.get<ReleaseResult>(
|
|
cacheNamespace,
|
|
cacheKey,
|
|
);
|
|
|
|
if (cachedResult) {
|
|
logger.trace({ cacheKey }, 'Returning cached datasource response');
|
|
DatasourceCacheStats.hit(datasource.id, registryUrl, config.packageName);
|
|
return cachedResult;
|
|
}
|
|
|
|
DatasourceCacheStats.miss(datasource.id, registryUrl, config.packageName);
|
|
}
|
|
|
|
const res = await instrument(
|
|
'getReleases',
|
|
() => datasource.getReleases({ ...config, registryUrl }),
|
|
{
|
|
attributes: {
|
|
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
|
|
[ATTR_RENOVATE_DATASOURCE]: datasource.id,
|
|
[ATTR_RENOVATE_REGISTRY_URL]: registryUrl,
|
|
[ATTR_RENOVATE_PACKAGE_NAME]: config.packageName,
|
|
},
|
|
},
|
|
);
|
|
if (res?.releases.length) {
|
|
res.registryUrl ??= registryUrl;
|
|
}
|
|
|
|
if (!res) {
|
|
return null;
|
|
}
|
|
|
|
let cache = false;
|
|
if (cacheForced) {
|
|
cache = true;
|
|
} else if (cacheEnabled && !res.isPrivate) {
|
|
cache = true;
|
|
}
|
|
|
|
if (cache) {
|
|
logger.trace({ cacheKey }, 'Caching datasource response');
|
|
await packageCache.set(cacheNamespace, cacheKey, res, 15);
|
|
DatasourceCacheStats.set(datasource.id, registryUrl, config.packageName);
|
|
} else {
|
|
DatasourceCacheStats.skip(datasource.id, registryUrl, config.packageName);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
function firstRegistry(
|
|
config: GetReleasesInternalConfig,
|
|
datasource: DatasourceApi,
|
|
registryUrls: string[],
|
|
): Promise<ReleaseResult | null> {
|
|
if (registryUrls.length > 1) {
|
|
logger.warn(
|
|
{
|
|
datasource: datasource.id,
|
|
packageName: config.packageName,
|
|
registryUrls,
|
|
},
|
|
'Excess registryUrls found for datasource lookup - using first configured only',
|
|
);
|
|
}
|
|
const registryUrl = registryUrls[0];
|
|
return getRegistryReleases(datasource, config, registryUrl);
|
|
}
|
|
|
|
async function huntRegistries(
|
|
config: GetReleasesInternalConfig,
|
|
datasource: DatasourceApi,
|
|
registryUrls: string[],
|
|
): Promise<ReleaseResult | null> {
|
|
let res: ReleaseResult | null = null;
|
|
let caughtError: Error | undefined;
|
|
for (const registryUrl of registryUrls) {
|
|
try {
|
|
res = await getRegistryReleases(datasource, config, registryUrl);
|
|
if (res) {
|
|
break;
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof ExternalHostError) {
|
|
throw err;
|
|
}
|
|
// We'll always save the last-thrown error
|
|
caughtError = err;
|
|
logger.trace({ err }, 'datasource hunt failure');
|
|
}
|
|
}
|
|
if (res) {
|
|
return res;
|
|
}
|
|
if (caughtError) {
|
|
throw caughtError;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function mergeRegistries(
|
|
config: GetReleasesInternalConfig,
|
|
datasource: DatasourceApi,
|
|
registryUrls: string[],
|
|
): Promise<ReleaseResult | null> {
|
|
let combinedRes: ReleaseResult | undefined;
|
|
let lastErr: Error | undefined;
|
|
let externalHostError: ExternalHostError | undefined;
|
|
let singleRegistry = true;
|
|
const releaseVersioning = versioning.get(config.versioning);
|
|
for (const registryUrl of registryUrls) {
|
|
try {
|
|
// Merging must not mutate responses shared by the package cache.
|
|
const res = clone(
|
|
await getRegistryReleases(datasource, config, registryUrl),
|
|
);
|
|
if (!res) {
|
|
continue;
|
|
}
|
|
|
|
if (!combinedRes) {
|
|
// This is the first registry, so we can just use it and continue
|
|
combinedRes = res;
|
|
continue;
|
|
}
|
|
|
|
if (singleRegistry) {
|
|
// This is the second registry
|
|
// We need to move the registryUrl from the package level to the release level
|
|
for (const release of coerceArray(combinedRes.releases)) {
|
|
release.registryUrl ??= combinedRes.registryUrl;
|
|
}
|
|
singleRegistry = false;
|
|
}
|
|
|
|
const releases = coerceArray(res.releases);
|
|
for (const release of releases) {
|
|
// We have more than one registry, so we need to move the registryUrl
|
|
// from the package level to the release level before merging
|
|
release.registryUrl ??= res.registryUrl;
|
|
}
|
|
|
|
combinedRes.releases.push(...releases);
|
|
|
|
// Merge the tags from the two results
|
|
let tags = combinedRes.tags;
|
|
if (tags) {
|
|
// v8 ignore else -- needs merged registries where only one carries tags
|
|
if (res.tags) {
|
|
// Both results had tags, so we need to compare them
|
|
for (const tag of ['release', 'latest']) {
|
|
const existingTag = combinedRes?.tags?.[tag];
|
|
const newTag = res.tags?.[tag];
|
|
// v8 ignore else -- needs a merged registry whose tag is not a version
|
|
if (isString(newTag) && releaseVersioning.isVersion(newTag)) {
|
|
if (
|
|
isString(existingTag) &&
|
|
releaseVersioning.isVersion(existingTag)
|
|
) {
|
|
// We need to compare them
|
|
if (releaseVersioning.isGreaterThan(newTag, existingTag)) {
|
|
// New tag is greater than the existing one
|
|
tags[tag] = newTag;
|
|
}
|
|
} else {
|
|
// Existing tag was not present or not a version
|
|
// so we can just use the new one
|
|
tags[tag] = newTag;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Existing results had no tags, so we can just use the new ones
|
|
tags = res.tags;
|
|
}
|
|
combinedRes = { ...res, ...combinedRes };
|
|
if (tags) {
|
|
combinedRes.tags = tags;
|
|
}
|
|
// Remove the registryUrl from the package level when more than one registry
|
|
delete combinedRes.registryUrl;
|
|
} catch (err) {
|
|
if (err instanceof ExternalHostError) {
|
|
// Don't abort the merge if another registry already returned releases;
|
|
// a single rate-limited registry shouldn't discard results we have
|
|
externalHostError = err;
|
|
logger.debug(
|
|
{ err, registryUrl },
|
|
'datasource merge: external host error from registry; continuing so releases from other registries are not discarded',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
lastErr = err;
|
|
logger.trace({ err }, 'datasource merge failure');
|
|
}
|
|
}
|
|
|
|
if (!combinedRes) {
|
|
if (externalHostError) {
|
|
throw externalHostError;
|
|
}
|
|
|
|
if (lastErr) {
|
|
throw lastErr;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const seenVersions = new Set<string>();
|
|
combinedRes.releases = filterMap(combinedRes.releases, (release) => {
|
|
if (seenVersions.has(release.version)) {
|
|
return null;
|
|
}
|
|
seenVersions.add(release.version);
|
|
return release;
|
|
});
|
|
|
|
return combinedRes;
|
|
}
|
|
|
|
function massageRegistryUrls(registryUrls: string[]): string[] {
|
|
return registryUrls.filter(isTruthy).map(trimTrailingSlash);
|
|
}
|
|
|
|
function resolveRegistryUrls(
|
|
datasource: DatasourceApi,
|
|
defaultRegistryUrls: string[] | undefined,
|
|
registryUrls: string[] | undefined | null,
|
|
additionalRegistryUrls: string[] | undefined,
|
|
): string[] {
|
|
if (!datasource.customRegistrySupport) {
|
|
if (
|
|
isNonEmptyArray(registryUrls) ||
|
|
isNonEmptyArray(defaultRegistryUrls) ||
|
|
isNonEmptyArray(additionalRegistryUrls)
|
|
) {
|
|
logger.warn(
|
|
{
|
|
datasource: datasource.id,
|
|
registryUrls,
|
|
defaultRegistryUrls,
|
|
additionalRegistryUrls,
|
|
},
|
|
'Custom registries are not allowed for this datasource and will be ignored',
|
|
);
|
|
}
|
|
return isFunction(datasource.defaultRegistryUrls)
|
|
? datasource.defaultRegistryUrls()
|
|
: coerceArray(datasource.defaultRegistryUrls);
|
|
}
|
|
const customUrls = registryUrls?.filter(isTruthy);
|
|
let resolvedUrls: string[] = [];
|
|
if (isNonEmptyArray(customUrls)) {
|
|
resolvedUrls = [...customUrls];
|
|
} else if (isNonEmptyArray(defaultRegistryUrls)) {
|
|
resolvedUrls = [...defaultRegistryUrls];
|
|
resolvedUrls = resolvedUrls.concat(coerceArray(additionalRegistryUrls));
|
|
} else if (isFunction(datasource.defaultRegistryUrls)) {
|
|
resolvedUrls = [...datasource.defaultRegistryUrls()];
|
|
resolvedUrls = resolvedUrls.concat(coerceArray(additionalRegistryUrls));
|
|
} else if (isNonEmptyArray(datasource.defaultRegistryUrls)) {
|
|
resolvedUrls = [...datasource.defaultRegistryUrls];
|
|
resolvedUrls = resolvedUrls.concat(coerceArray(additionalRegistryUrls));
|
|
}
|
|
return massageRegistryUrls(resolvedUrls);
|
|
}
|
|
|
|
function applyReplacements(
|
|
config: GetReleasesInternalConfig,
|
|
): Pick<ReleaseResult, 'replacementName' | 'replacementVersion'> | undefined {
|
|
if (config.replacementName && config.replacementVersion) {
|
|
return {
|
|
replacementName: config.replacementName,
|
|
replacementVersion: config.replacementVersion,
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
async function fetchReleases(
|
|
config: GetReleasesInternalConfig,
|
|
): Promise<ReleaseResult | null> {
|
|
const { datasource: datasourceName } = config;
|
|
let { registryUrls } = config;
|
|
// istanbul ignore if: need test
|
|
if (!datasourceName || getDatasourceFor(datasourceName) === undefined) {
|
|
logger.warn({ datasource: datasourceName }, 'Unknown datasource');
|
|
return null;
|
|
}
|
|
if (datasourceName === 'npm') {
|
|
if (isString(config.npmrc)) {
|
|
setNpmrc(config.npmrc);
|
|
}
|
|
// v8 ignore else -- npm lookups here never arrive with explicit registryUrls
|
|
if (!isNonEmptyArray(registryUrls)) {
|
|
registryUrls = [resolveRegistryUrl(config.packageName)];
|
|
}
|
|
}
|
|
const datasource = getDatasourceFor(datasourceName);
|
|
// istanbul ignore if: needs test
|
|
if (!datasource) {
|
|
logger.warn({ datasource: datasourceName }, 'Unknown datasource');
|
|
return null;
|
|
}
|
|
registryUrls = resolveRegistryUrls(
|
|
datasource,
|
|
config.defaultRegistryUrls,
|
|
registryUrls,
|
|
config.additionalRegistryUrls,
|
|
);
|
|
let dep: ReleaseResult | null = null;
|
|
const registryStrategy =
|
|
config.registryStrategy ?? datasource.registryStrategy ?? 'hunt';
|
|
try {
|
|
if (isNonEmptyArray(registryUrls)) {
|
|
if (registryStrategy === 'first') {
|
|
dep = await firstRegistry(config, datasource, registryUrls);
|
|
} else if (registryStrategy === 'hunt') {
|
|
dep = await huntRegistries(config, datasource, registryUrls);
|
|
} else {
|
|
// `merge` is the only remaining strategy
|
|
dep = await mergeRegistries(config, datasource, registryUrls);
|
|
}
|
|
} else {
|
|
dep = await instrument(
|
|
'getReleases',
|
|
() => datasource.getReleases(config),
|
|
{
|
|
attributes: {
|
|
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
|
|
[ATTR_RENOVATE_DATASOURCE]: datasource.id,
|
|
[ATTR_RENOVATE_REGISTRY_URL]: config.registryUrl ?? '',
|
|
[ATTR_RENOVATE_PACKAGE_NAME]: config.packageName,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
} catch (err) {
|
|
if (
|
|
[HOST_BLOCKED, HOST_DISABLED].includes(err.message) ||
|
|
[HOST_BLOCKED, HOST_DISABLED].includes(err.err?.message)
|
|
) {
|
|
return null;
|
|
}
|
|
if (err instanceof ExternalHostError) {
|
|
throw err;
|
|
}
|
|
logError(datasource.id, config.packageName, err);
|
|
}
|
|
if (!dep || dequal(dep, { releases: [] })) {
|
|
return null;
|
|
}
|
|
addMetaData(dep, datasourceName, config.packageName);
|
|
dep = { ...dep, ...applyReplacements(config) };
|
|
return dep;
|
|
}
|
|
|
|
function fetchCachedReleases(
|
|
config: GetReleasesInternalConfig,
|
|
): Promise<ReleaseResult | null> {
|
|
const { datasource, packageName, registryUrls } = config;
|
|
const cacheKey = `datasource-mem:releases:${datasource}:${packageName}:${config.registryStrategy}:${safeStringify(
|
|
[registryUrls, config.defaultRegistryUrls, config.additionalRegistryUrls],
|
|
)}`;
|
|
// By returning a Promise and reusing it, we should only fetch each package at most once
|
|
const cachedResult = memCache.get<Promise<ReleaseResult | null>>(cacheKey);
|
|
// istanbul ignore if
|
|
if (cachedResult !== undefined) {
|
|
return cachedResult;
|
|
}
|
|
const promisedRes = fetchReleases(config);
|
|
memCache.set(cacheKey, promisedRes);
|
|
return promisedRes;
|
|
}
|
|
|
|
export function getRawPkgReleases(
|
|
config: GetPkgReleasesConfig,
|
|
): AsyncResult<
|
|
ReleaseResult,
|
|
Error | 'no-datasource' | 'no-package-name' | 'no-result'
|
|
> {
|
|
if (!config.datasource) {
|
|
logger.warn('No datasource found');
|
|
return AsyncResult.err('no-datasource');
|
|
}
|
|
|
|
const packageName = config.packageName;
|
|
if (!packageName) {
|
|
logger.error({ config }, 'Datasource getReleases without packageName');
|
|
return AsyncResult.err('no-package-name');
|
|
}
|
|
|
|
return Result.wrapNullable(fetchCachedReleases(config), 'no-result' as const)
|
|
.catch((e) => {
|
|
if (e instanceof ExternalHostError) {
|
|
e.hostType = config.datasource;
|
|
e.packageName = packageName;
|
|
}
|
|
return Result.err(e);
|
|
})
|
|
.transform(clone);
|
|
}
|
|
|
|
export function applyDatasourceFilters(
|
|
releaseResult: ReleaseResult,
|
|
config: GetPkgReleasesConfig,
|
|
): ReleaseResult {
|
|
let res = releaseResult;
|
|
res = applyExtractVersion(res, config.extractVersion);
|
|
res = applyVersionCompatibility(
|
|
res,
|
|
config.versionCompatibility,
|
|
config.currentCompatibility,
|
|
);
|
|
res = filterValidVersions(res, config);
|
|
res = sortAndRemoveDuplicates(res, config);
|
|
res = applyConstraintsFiltering(res, config);
|
|
return res;
|
|
}
|
|
|
|
export async function getPkgReleases(
|
|
config: GetPkgReleasesConfig,
|
|
): Promise<ReleaseResult | null> {
|
|
const { val = null, err } = await getRawPkgReleases(config)
|
|
.transform((res) => applyDatasourceFilters(res, config))
|
|
.unwrap();
|
|
|
|
if (err instanceof Error) {
|
|
throw err;
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
export function supportsDigests(datasource: string | undefined): boolean {
|
|
const ds = !!datasource && getDatasourceFor(datasource);
|
|
return !!ds && 'getDigest' in ds;
|
|
}
|
|
|
|
function getDigestConfig(
|
|
datasource: DatasourceApi,
|
|
config: GetDigestInputConfig,
|
|
): DigestConfig {
|
|
const { lookupName, currentValue, currentDigest } = config;
|
|
const packageName = config.replacementName ?? config.packageName;
|
|
// Prefer registryUrl from getReleases() lookup if it has been passed
|
|
const registryUrl =
|
|
config.registryUrl ??
|
|
resolveRegistryUrls(
|
|
datasource,
|
|
config.defaultRegistryUrls,
|
|
config.registryUrls,
|
|
config.additionalRegistryUrls,
|
|
)[0];
|
|
return { lookupName, packageName, registryUrl, currentValue, currentDigest };
|
|
}
|
|
|
|
export function getDigest(
|
|
config: GetDigestInputConfig,
|
|
value?: string,
|
|
): Promise<string | null> {
|
|
const datasource = getDatasourceFor(config.datasource);
|
|
// istanbul ignore if: need test
|
|
if (!datasource || !('getDigest' in datasource)) {
|
|
return Promise.resolve(null);
|
|
}
|
|
const digestConfig = getDigestConfig(datasource, config);
|
|
return datasource.getDigest!(digestConfig, value);
|
|
}
|
|
|
|
export function getDefaultConfig(
|
|
datasource: string,
|
|
): Promise<Record<string, unknown>> {
|
|
const loadedDatasource = getDatasourceFor(datasource);
|
|
return Promise.resolve<Record<string, unknown>>(
|
|
loadedDatasource?.defaultConfig ?? Object.create({}),
|
|
);
|
|
}
|