mirror of https://github.com/renovatebot/renovate
268 lines
7.9 KiB
TypeScript
268 lines
7.9 KiB
TypeScript
import is from '@sindresorhus/is';
|
|
import { supportedDatasources as presetSupportedDatasources } from '../../config/presets/internal/merge-confidence';
|
|
import type { AllConfig, UpdateType } from '../../config/types';
|
|
import { logger } from '../../logger';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error';
|
|
import * as packageCache from '../cache/package';
|
|
import * as hostRules from '../host-rules';
|
|
import { Http } from '../http';
|
|
import { regEx } from '../regex';
|
|
import { ensureTrailingSlash, joinUrlParts } from '../url';
|
|
import { MERGE_CONFIDENCE } from './common';
|
|
import type { MergeConfidence } from './types';
|
|
|
|
const hostType = 'merge-confidence';
|
|
const http = new Http(hostType);
|
|
let token: string | undefined;
|
|
let apiBaseUrl: string | undefined;
|
|
let supportedDatasources: string[] = [];
|
|
|
|
export const confidenceLevels: Record<MergeConfidence, number> = {
|
|
low: -1,
|
|
neutral: 0,
|
|
high: 1,
|
|
'very high': 2,
|
|
};
|
|
|
|
export function initConfig({
|
|
mergeConfidenceEndpoint,
|
|
mergeConfidenceDatasources,
|
|
}: AllConfig): void {
|
|
apiBaseUrl = getApiBaseUrl(mergeConfidenceEndpoint);
|
|
token = getApiToken();
|
|
|
|
supportedDatasources =
|
|
mergeConfidenceDatasources ?? presetSupportedDatasources;
|
|
|
|
if (!is.nullOrUndefined(token)) {
|
|
logger.debug(`Merge confidence token found for ${apiBaseUrl}`);
|
|
}
|
|
}
|
|
|
|
export function resetConfig(): void {
|
|
token = undefined;
|
|
apiBaseUrl = undefined;
|
|
supportedDatasources = [];
|
|
}
|
|
|
|
export function isMergeConfidence(value: string): value is MergeConfidence {
|
|
return MERGE_CONFIDENCE.includes(value as MergeConfidence);
|
|
}
|
|
|
|
export function isActiveConfidenceLevel(confidence: string): boolean {
|
|
return isMergeConfidence(confidence) && confidence !== 'low';
|
|
}
|
|
|
|
export function satisfiesConfidenceLevel(
|
|
confidence: MergeConfidence,
|
|
minimumConfidence: MergeConfidence,
|
|
): boolean {
|
|
return confidenceLevels[confidence] >= confidenceLevels[minimumConfidence];
|
|
}
|
|
|
|
const updateTypeConfidenceMapping: Record<UpdateType, MergeConfidence | null> =
|
|
{
|
|
pin: 'high',
|
|
digest: 'neutral',
|
|
pinDigest: 'high',
|
|
bump: 'neutral',
|
|
lockFileMaintenance: 'neutral',
|
|
lockfileUpdate: 'neutral',
|
|
rollback: 'neutral',
|
|
replacement: 'neutral',
|
|
major: null,
|
|
minor: null,
|
|
patch: null,
|
|
};
|
|
|
|
/**
|
|
* Retrieves the merge confidence of a package update if the merge confidence API is enabled. Otherwise, undefined is returned.
|
|
*
|
|
* @param datasource
|
|
* @param packageName
|
|
* @param currentVersion
|
|
* @param newVersion
|
|
* @param updateType
|
|
*
|
|
* @returns The merge confidence level for the given package release.
|
|
* @throws {ExternalHostError} If a request has been made and an error occurs during the request, such as a timeout, connection reset, authentication failure, or internal server error.
|
|
*/
|
|
export async function getMergeConfidenceLevel(
|
|
datasource: string,
|
|
packageName: string,
|
|
currentVersion: string,
|
|
newVersion: string,
|
|
updateType: UpdateType,
|
|
): Promise<MergeConfidence | undefined> {
|
|
if (is.nullOrUndefined(apiBaseUrl) || is.nullOrUndefined(token)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!supportedDatasources.includes(datasource)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!(currentVersion && newVersion && updateType)) {
|
|
return 'neutral';
|
|
}
|
|
|
|
const mappedConfidence = updateTypeConfidenceMapping[updateType];
|
|
if (mappedConfidence) {
|
|
return mappedConfidence;
|
|
}
|
|
|
|
return await queryApi(datasource, packageName, currentVersion, newVersion);
|
|
}
|
|
|
|
/**
|
|
* Queries the Merge Confidence API with the given package release information.
|
|
*
|
|
* @param datasource
|
|
* @param packageName
|
|
* @param currentVersion
|
|
* @param newVersion
|
|
*
|
|
* @returns The merge confidence level for the given package release.
|
|
* @throws {ExternalHostError} if a timeout or connection reset error, authentication failure, or internal server error occurs during the request.
|
|
*
|
|
* @remarks
|
|
* Results are cached for 60 minutes to reduce the number of API calls.
|
|
*/
|
|
async function queryApi(
|
|
datasource: string,
|
|
packageName: string,
|
|
currentVersion: string,
|
|
newVersion: string,
|
|
): Promise<MergeConfidence> {
|
|
// istanbul ignore if: defensive, already been validated before calling this function
|
|
if (is.nullOrUndefined(apiBaseUrl) || is.nullOrUndefined(token)) {
|
|
return 'neutral';
|
|
}
|
|
|
|
const escapedPackageName = packageName.replace(regEx(/\//g), '%2f');
|
|
const url = joinUrlParts(
|
|
apiBaseUrl,
|
|
'api/mc/json',
|
|
datasource,
|
|
escapedPackageName,
|
|
currentVersion,
|
|
newVersion,
|
|
);
|
|
const cacheKey = `${token}:${url}`;
|
|
const cachedResult = await packageCache.get(hostType, cacheKey);
|
|
|
|
// istanbul ignore if
|
|
if (cachedResult) {
|
|
logger.debug(
|
|
{
|
|
datasource,
|
|
packageName,
|
|
currentVersion,
|
|
newVersion,
|
|
cachedResult,
|
|
},
|
|
'using merge confidence cached result',
|
|
);
|
|
return cachedResult;
|
|
}
|
|
|
|
let confidence: MergeConfidence = 'neutral';
|
|
try {
|
|
const res = (await http.getJson<{ confidence: MergeConfidence }>(url)).body;
|
|
if (isMergeConfidence(res.confidence)) {
|
|
confidence = res.confidence;
|
|
}
|
|
} catch (err) {
|
|
apiErrorHandler(err);
|
|
}
|
|
|
|
await packageCache.set(hostType, cacheKey, confidence, 60);
|
|
return confidence;
|
|
}
|
|
|
|
/**
|
|
* Checks the health of the Merge Confidence API by attempting to authenticate with it.
|
|
*
|
|
* @returns Resolves when the API health check is completed successfully.
|
|
*
|
|
* @throws {ExternalHostError} if a timeout, connection reset error, authentication failure, or internal server error occurs during the request.
|
|
*
|
|
* @remarks
|
|
* This function first checks that the API base URL and an authentication bearer token are defined before attempting to
|
|
* authenticate with the API. If either the base URL or token is not defined, it will immediately return
|
|
* without making a request.
|
|
*/
|
|
export async function initMergeConfidence(config: AllConfig): Promise<void> {
|
|
initConfig(config);
|
|
|
|
if (is.nullOrUndefined(apiBaseUrl) || is.nullOrUndefined(token)) {
|
|
logger.trace('merge confidence API usage is disabled');
|
|
return;
|
|
}
|
|
|
|
const url = joinUrlParts(apiBaseUrl, 'api/mc/availability');
|
|
try {
|
|
await http.get(url);
|
|
} catch (err) {
|
|
apiErrorHandler(err);
|
|
}
|
|
|
|
logger.debug(
|
|
{ supportedDatasources },
|
|
'merge confidence API - successfully authenticated',
|
|
);
|
|
return;
|
|
}
|
|
|
|
function getApiBaseUrl(mergeConfidenceEndpoint: string | undefined): string {
|
|
const defaultBaseUrl = 'https://developer.mend.io/';
|
|
const baseFromEnv = mergeConfidenceEndpoint ?? defaultBaseUrl;
|
|
|
|
try {
|
|
const parsedBaseUrl = new URL(baseFromEnv).toString();
|
|
logger.trace(
|
|
{ baseUrl: parsedBaseUrl },
|
|
'using merge confidence API base found in environment variables',
|
|
);
|
|
return ensureTrailingSlash(parsedBaseUrl);
|
|
} catch (err) {
|
|
logger.warn(
|
|
{ err, baseFromEnv },
|
|
'invalid merge confidence API base URL found in environment variables - using default value instead',
|
|
);
|
|
return defaultBaseUrl;
|
|
}
|
|
}
|
|
|
|
export function getApiToken(): string | undefined {
|
|
return hostRules.find({
|
|
url: apiBaseUrl,
|
|
hostType,
|
|
})?.token;
|
|
}
|
|
|
|
/**
|
|
* Handles errors returned by the Merge Confidence API.
|
|
*
|
|
* @param err - The error object returned by the API.
|
|
* @throws {ExternalHostError} if a timeout or connection reset error, authentication failure, or internal server error occurs during the request.
|
|
*/
|
|
function apiErrorHandler(err: any): void {
|
|
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET') {
|
|
logger.error({ err }, 'merge confidence API request failed - aborting run');
|
|
throw new ExternalHostError(err, hostType);
|
|
}
|
|
|
|
if (err.statusCode === 403) {
|
|
logger.error({ err }, 'merge confidence API token rejected - aborting run');
|
|
throw new ExternalHostError(err, hostType);
|
|
}
|
|
|
|
if (err.statusCode >= 500 && err.statusCode < 600) {
|
|
logger.error({ err }, 'merge confidence API failure: 5xx - aborting run');
|
|
throw new ExternalHostError(err, hostType);
|
|
}
|
|
|
|
logger.warn({ err }, 'error fetching merge confidence data');
|
|
}
|