UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

382 lines (381 loc) • 19.8 kB
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