renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
382 lines (381 loc) • 19.8 kB
JavaScript
import "../../../../constants/error-messages.js";
import { regEx } from "../../../../util/regex.js";
import { logger } from "../../../../logger/index.js";
import { mergeChildConfig } from "../../../../config/utils.js";
import { ExternalHostError } from "../../../../types/errors/external-host-error.js";
import "../../../../modules/versioning/docker/index.js";
import { get } from "../../../../modules/versioning/index.js";
import { Result } from "../../../../util/result.js";
import { assignKeys } from "../../../../util/assign-keys.js";
import { getElapsedDays } from "../../../../util/date.js";
import { getDatasourceFor, getDefaultVersioning, isGetPkgReleasesConfig } from "../../../../modules/datasource/common.js";
import { applyDatasourceFilters, getDigest, getRawPkgReleases, supportsDigests } from "../../../../modules/datasource/index.js";
import { getRangeStrategy } from "../../../../modules/manager/index.js";
import "../../../../config/index.js";
import { applyPackageRules } from "../../../../util/package-rules/index.js";
import { postprocessRelease } from "../../../../modules/datasource/postprocess-release.js";
import { calculateAbandonment } from "./abandonment.js";
import { getBucket } from "./bucket.js";
import { getCurrentVersion } from "./current.js";
import { filterVersions } from "./filter.js";
import { filterInternalChecks } from "./filter-checks.js";
import { generateUpdate } from "./generate.js";
import { getRollbackUpdate } from "./rollback.js";
import { calculateMostRecentTimestamp } from "./timestamps.js";
import { addReplacementUpdateIfValid, isReplacementRulesConfigured } from "./utils.js";
import { isNonEmptyString, isString, isUndefined } from "@sindresorhus/is";
//#region lib/workers/repository/process/lookup/index.ts
async function getTimestamp(config, versions, version, versioningApi) {
const currentRelease = versions.find((v) => versioningApi.isValid(v.version) && versioningApi.equals(v.version, version));
if (!currentRelease) return null;
if (currentRelease.releaseTimestamp) return currentRelease.releaseTimestamp;
return (await postprocessRelease(config, currentRelease))?.releaseTimestamp;
}
async function lookupUpdates(inconfig) {
let config = { ...inconfig };
config.versioning ??= getDefaultVersioning(config.datasource);
const versioningApi = get(config.versioning);
let dependency = null;
const res = {
versioning: config.versioning,
updates: [],
warnings: []
};
try {
logger.trace({
dependency: config.packageName,
currentValue: config.currentValue
}, "lookupUpdates");
if (config.currentValue && !isString(config.currentValue)) {
// v8 ignore else -- TODO: add test #40625
if (config.currentValue) logger.debug(`Invalid currentValue for ${config.packageName}: ${JSON.stringify(config.currentValue)} (${typeof config.currentValue})`);
res.skipReason = "invalid-value";
return Result.ok(res);
}
if (!isGetPkgReleasesConfig(config) || !getDatasourceFor(config.datasource)) {
res.skipReason = "invalid-config";
return Result.ok(res);
}
let compareValue = config.currentValue;
if (isString(config.currentValue) && isString(config.versionCompatibility)) {
const regexMatch = regEx(config.versionCompatibility).exec(config.currentValue);
if (regexMatch?.groups) {
logger.debug({
versionCompatibility: config.versionCompatibility,
currentValue: config.currentValue,
packageName: config.packageName,
groups: regexMatch.groups
}, "version compatibility regex match");
config.currentCompatibility = regexMatch.groups.compatibility;
compareValue = regexMatch.groups.version;
} else logger.debug({
versionCompatibility: config.versionCompatibility,
currentValue: config.currentValue,
packageName: config.packageName
}, "version compatibility regex mismatch");
}
const isValid = isString(compareValue) && versioningApi.isValid(compareValue);
const unconstrainedValue = !!config.lockedVersion && isUndefined(config.currentValue);
if (isValid || unconstrainedValue) {
if (!config.updatePinnedDependencies && versioningApi.isSingleVersion(compareValue)) {
res.skipReason = "is-pinned";
return Result.ok(res);
}
const { val: releaseResult, err: lookupError } = await getRawPkgReleases(config).transform((res) => calculateMostRecentTimestamp(versioningApi, res)).transform((res) => calculateAbandonment(res, config)).transform((res) => applyDatasourceFilters(res, config)).unwrap();
if (lookupError instanceof Error) throw lookupError;
if (lookupError) {
const warning = {
topic: config.packageName,
message: `Failed to look up ${config.datasource} package ${config.packageName}: ${lookupError}`
};
logger.debug({
dependency: config.packageName,
packageFile: config.packageFile
}, warning.message);
res.warnings.push(warning);
return Result.ok(res);
}
dependency = releaseResult;
if (dependency.deprecationMessage) logger.debug(`Found deprecationMessage for ${config.datasource} package ${config.packageName}`);
assignKeys(res, dependency, [
"deprecationMessage",
"sourceUrl",
"registryUrl",
"sourceDirectory",
"homepage",
"changelogUrl",
"dependencyUrl",
"lookupName",
"packageScope",
"mostRecentTimestamp",
"isAbandoned",
"respectLatest"
]);
const latestVersion = dependency.tags?.latest;
let allVersions = dependency.releases.filter((release) => versioningApi.isVersion(release.version));
const allReleaseVersions = new Set(allVersions.map((r) => r.version));
// istanbul ignore if
if (allVersions.length === 0) {
logger.info({
dependency: config.packageName,
result: dependency
}, `Found no results from datasource that look like a version`);
if (!config.currentDigest) return Result.ok(res);
}
config = await applyPackageRules({
...config,
sourceUrl: res.sourceUrl
}, "source-url");
if (config.followTag) {
const taggedVersion = dependency.tags?.[config.followTag];
if (!taggedVersion) {
res.warnings.push({
topic: config.packageName,
message: `Can't find version with tag ${config.followTag} for ${config.datasource} package ${config.packageName}`
});
return Result.ok(res);
}
allVersions = allVersions.filter((v) => v.version === taggedVersion || v.version === compareValue && versioningApi.isGreaterThan(taggedVersion, compareValue));
}
const inRangeOnlyStrategy = config.rangeStrategy === "in-range-only";
const allSatisfyingVersions = (inRangeOnlyStrategy || config.rollbackPrs) && !unconstrainedValue ? allVersions.filter((v) => versioningApi.matches(v.version, compareValue)) : allVersions;
if (!allSatisfyingVersions.length) logger.debug(`Found no satisfying versions with '${config.versioning}' versioning`);
if (config.rollbackPrs && !allSatisfyingVersions.length) {
const rollback = getRollbackUpdate(config, allVersions, versioningApi);
// istanbul ignore if
if (!rollback) {
res.warnings.push({
topic: config.packageName,
message: `Can't find version matching ${compareValue} for ${config.datasource} package ${config.packageName}`
});
return Result.ok(res);
}
res.updates.push(rollback);
}
let rangeStrategy = getRangeStrategy(config);
// istanbul ignore next
if (config.isVulnerabilityAlert && rangeStrategy === "update-lockfile" && !config.lockedVersion) rangeStrategy = "bump";
if (config.isVulnerabilityAlert && !config.currentValue && config.lockedVersion) rangeStrategy = "update-lockfile";
const nonDeprecatedVersions = dependency.releases.filter((release) => !release.isDeprecated).map((release) => release.version);
let currentVersion;
if (rangeStrategy === "update-lockfile") currentVersion = config.lockedVersion;
else if (compareValue && versioningApi.isSingleVersion(compareValue) && allVersions.find((v) => v.version === compareValue)) currentVersion = compareValue;
currentVersion ??= getCurrentVersion(compareValue, config.lockedVersion, versioningApi, rangeStrategy, latestVersion, nonDeprecatedVersions) ?? getCurrentVersion(compareValue, config.lockedVersion, versioningApi, rangeStrategy, latestVersion, allVersions.map((v) => v.version));
if (!currentVersion) {
// v8 ignore else -- TODO: add test #40625
if (!config.lockedVersion) {
logger.debug(`No currentVersion or lockedVersion found for ${config.packageName}`);
res.skipReason = "invalid-value";
}
return Result.ok(res);
}
res.currentVersion = currentVersion;
const versionForTimestamp = config.lockedVersion ?? currentVersion;
const currentVersionTimestamp = await getTimestamp(config, allVersions, versionForTimestamp, versioningApi);
if (isNonEmptyString(currentVersionTimestamp)) {
res.currentVersionTimestamp = currentVersionTimestamp;
res.currentVersionAgeInDays = getElapsedDays(currentVersionTimestamp);
if (config.packageRules?.some((rule) => isNonEmptyString(rule.matchCurrentAge))) config = await applyPackageRules({
...config,
currentVersionTimestamp
}, "current-timestamp");
}
if (compareValue && currentVersion && rangeStrategy === "pin" && !versioningApi.isSingleVersion(compareValue)) {
const newValue = versioningApi.getPinnedValue?.(currentVersion) ?? currentVersion;
res.updates.push({
updateType: "pin",
isPin: true,
newValue,
newVersion: currentVersion,
newMajor: versioningApi.getMajor(currentVersion)
});
}
if (rangeStrategy === "pin") rangeStrategy = "replace";
// istanbul ignore if
if (!versioningApi.isVersion(currentVersion)) {
res.skipReason = "invalid-version";
return Result.ok(res);
}
let filteredReleases = filterVersions(config, currentVersion, latestVersion, inRangeOnlyStrategy ? allSatisfyingVersions : allVersions, versioningApi).filter((v) => unconstrainedValue || versioningApi.isCompatible(v.version, compareValue));
let shrinkedViaVulnerability = false;
if (config.isVulnerabilityAlert) {
if (config.vulnerabilityFixVersion) {
res.vulnerabilityFixVersion = config.vulnerabilityFixVersion;
res.vulnerabilityFixStrategy = config.vulnerabilityFixStrategy;
if (versioningApi.isValid(config.vulnerabilityFixVersion)) {
let fixedFilteredReleases;
if (versioningApi.isVersion(config.vulnerabilityFixVersion)) fixedFilteredReleases = filteredReleases.filter((release) => !versioningApi.isGreaterThan(config.vulnerabilityFixVersion, release.version));
else fixedFilteredReleases = filteredReleases.filter((release) => versioningApi.matches(release.version, config.vulnerabilityFixVersion));
if (fixedFilteredReleases.length === 0 && filteredReleases.length) logger.warn({
releases: filteredReleases,
vulnerabilityFixVersion: config.vulnerabilityFixVersion,
packageName: config.packageName
}, "No releases satisfy vulnerabilityFixVersion");
filteredReleases = fixedFilteredReleases;
} else logger.warn({
vulnerabilityFixVersion: config.vulnerabilityFixVersion,
packageName: config.packageName
}, "vulnerabilityFixVersion is not valid");
}
if (config.vulnerabilityFixStrategy === "highest") logger.once.debug(`Using vulnerabilityFixStrategy=highest for ${config.packageName}`);
else {
logger.once.debug(`Using vulnerabilityFixStrategy=lowest for ${config.packageName}`);
filteredReleases = filteredReleases.slice(0, 1);
shrinkedViaVulnerability = true;
}
}
const buckets = {};
for (const release of filteredReleases) {
const bucket = getBucket(config, currentVersion, release.version, versioningApi);
// v8 ignore else -- TODO: add test #40625
if (isString(bucket)) if (buckets[bucket]) buckets[bucket].push(release);
else buckets[bucket] = [release];
}
const depResultConfig = mergeChildConfig(config, res);
for (const [bucket, releases] of Object.entries(buckets)) {
const { release, pendingChecks, pendingReleases } = await filterInternalChecks(depResultConfig, versioningApi, bucket, releases.sort((r1, r2) => versioningApi.sortVersions(r1.version, r2.version)));
// istanbul ignore next
if (!release) return Result.ok(res);
const newVersion = release.version;
const update = await generateUpdate(config, compareValue, versioningApi, rangeStrategy, config.lockedVersion ?? currentVersion, bucket, release, allReleaseVersions);
if (config.manager === "gomod" && compareValue?.startsWith("v0.0.0-") && update.newValue?.startsWith("v0.0.0-") && config.currentDigest !== update.newDigest) update.updateType = "digest";
if (pendingChecks) update.pendingChecks = pendingChecks;
if (pendingReleases.length) update.pendingVersions = pendingReleases.map((r) => r.version);
if (!update.newValue || update.newValue === compareValue) {
if (!config.lockedVersion) continue;
// istanbul ignore if
if (rangeStrategy === "bump") {
logger.trace({
packageName: config.packageName,
currentValue: config.currentValue,
lockedVersion: config.lockedVersion,
newVersion
}, "Skipping bump because newValue is the same");
continue;
}
res.isSingleVersion = true;
}
res.isSingleVersion ??= isString(update.newValue) && versioningApi.isSingleVersion(update.newValue);
// istanbul ignore if
if (config.versioning === "docker" && update.updateType !== "rollback" && update.newValue && versioningApi.isVersion(update.newValue) && compareValue && versioningApi.isVersion(compareValue) && versioningApi.isGreaterThan(compareValue, update.newValue)) logger.warn({
packageName: config.packageName,
currentValue: config.currentValue,
compareValue,
currentVersion: config.currentVersion,
update,
allVersionsLength: allVersions.length,
filteredReleaseVersions: filteredReleases.map((r) => r.version),
shrinkedViaVulnerability
}, "Unexpected downgrade detected: skipping");
else res.updates.push(update);
}
} else if (compareValue) {
logger.debug(`Dependency ${config.packageName} has unsupported/unversioned value ${compareValue} (versioning=${config.versioning})`);
if (!config.pinDigests && !config.currentDigest) {
logger.debug(`Skipping ${config.packageName} because no currentDigest or pinDigests`);
res.skipReason = "invalid-value";
} else delete res.skipReason;
} else res.skipReason = "invalid-value";
if (isReplacementRulesConfigured(config)) addReplacementUpdateIfValid(res.updates, config);
else if (dependency?.replacementName && dependency.replacementVersion) res.updates.push({
updateType: "replacement",
newName: dependency.replacementName,
newValue: dependency.replacementVersion
});
if (config.lockedVersion) {
res.currentVersion = config.lockedVersion;
res.fixedVersion = config.lockedVersion;
} else if (compareValue && versioningApi.isSingleVersion(compareValue)) res.fixedVersion = compareValue.replace(regEx(/^=+/), "");
if (isString(config.currentValue) && isString(compareValue) && isString(config.versionCompatibility)) for (const update of res.updates) {
logger.debug({ update });
// v8 ignore else -- TODO: add test #40625
if (isString(config.currentValue) && isString(update.newValue)) update.newValue = config.currentValue.replace(compareValue, update.newValue);
}
if (supportsDigests(config.datasource)) {
if (config.currentDigest) {
if (!config.digestOneAndOnly || !res.updates.length) res.updates.push({
updateType: "digest",
newValue: config.currentValue
});
} else if (config.pinDigests) {
// v8 ignore else -- TODO: add test #40625
if (!res.updates.some((update) => update.updateType === "pin")) res.updates.push({
isPinDigest: true,
updateType: "pinDigest",
newValue: config.currentValue
});
}
if (versioningApi.valueToVersion) {
res.currentVersion = versioningApi.valueToVersion(res.currentVersion);
for (const update of res.updates) update.newVersion = versioningApi.valueToVersion(update.newVersion);
}
if (res.registryUrl) config.registryUrls = [res.registryUrl];
for (const update of res.updates) {
if (config.pinDigests === true || config.currentDigest) {
const getDigestConfig = {
...config,
registryUrl: update.registryUrl ?? res.registryUrl,
lookupName: res.lookupName
};
if (update.updateType !== "replacement") delete getDigestConfig.replacementName;
if (update.updateType === "replacement" && update.newName !== config.packageName) {
delete getDigestConfig.lookupName;
delete getDigestConfig.currentDigest;
getDigestConfig.replacementName = update.newName;
}
if (update.updateType !== "replacement" || update.newName === config.packageName) update.newDigest ??= dependency?.releases.find((r) => r.version === update.newValue)?.newDigest;
update.newDigest ??= await getDigest(getDigestConfig, update.newValue);
if (update.newDigest === null) {
logger.debug({
packageName: config.packageName,
currentValue: config.currentValue,
datasource: config.datasource,
newValue: update.newValue,
bucket: update.bucket
}, "Could not determine new digest for update.");
if (config.currentDigest) res.warnings.push({
message: `Could not determine new digest for update (${config.datasource} package ${config.packageName})`,
topic: config.packageName
});
}
} else delete update.newDigest;
if (update.newVersion) {
const registryUrl = dependency?.releases?.find((release) => release.version === update.newVersion)?.registryUrl;
if (registryUrl && registryUrl !== res.registryUrl) update.registryUrl = registryUrl;
}
}
}
if (res.updates.length) delete res.skipReason;
res.updates = res.updates.filter((update) => update.newValue !== null || config.currentValue === null).filter((update) => update.newDigest !== null).filter((update) => isString(update.newName) && update.newName !== config.packageName || update.isReplacement === true || update.newValue !== config.currentValue || update.isLockfileUpdate === true || update.newDigest && !update.newDigest.startsWith(config.currentDigest));
if (config.rangeStrategy === "in-range-only") res.updates = res.updates.filter((update) => update.newValue === config.currentValue);
if (config.rollbackPrs && config.followTag) res.updates = res.updates.filter((update) => update.updateType !== "rollback" || res.updates.length === 1);
const release = res.updates.length > 0 ? dependency?.releases.find((r) => r.version === res.updates[0].newValue) ?? dependency?.releases.find((r) => r.version === res.updates[0].newVersion) : null;
if (release?.changelogContent) {
res.changelogContent = release.changelogContent;
res.changelogUrl = release.changelogUrl;
}
} catch (err) /* istanbul ignore next */ {
if (err instanceof ExternalHostError) return Result.err(err);
if (err instanceof Error && err.message === "config-validation") return Result.err(err);
logger.error({
currentDigest: config.currentDigest,
currentValue: config.currentValue,
datasource: config.datasource,
packageName: config.packageName,
digestOneAndOnly: config.digestOneAndOnly,
followTag: config.followTag,
lockedVersion: config.lockedVersion,
packageFile: config.packageFile,
pinDigests: config.pinDigests,
rollbackPrs: config.rollbackPrs,
isVulnerabilityAlert: config.isVulnerabilityAlert,
updatePinnedDependencies: config.updatePinnedDependencies,
err
}, "lookupUpdates error");
res.skipReason = "internal-error";
}
return Result.ok(res);
}
//#endregion
export { lookupUpdates };
//# sourceMappingURL=index.js.map