-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
index.ts
248 lines (217 loc) · 7.29 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import is from '@sindresorhus/is';
import type { 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 { 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;
const supportedDatasources = ['npm', 'maven', 'pypi'];
export const confidenceLevels: Record<MergeConfidence, number> = {
low: -1,
neutral: 0,
high: 1,
'very high': 2,
};
export function initConfig(): void {
apiBaseUrl = getApiBaseUrl();
token = getApiToken();
}
export function resetConfig(): void {
token = undefined;
apiBaseUrl = undefined;
}
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('/', '%2f');
const url = `${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(): Promise<void> {
initConfig();
if (is.nullOrUndefined(apiBaseUrl) || is.nullOrUndefined(token)) {
logger.trace('merge confidence API usage is disabled');
return;
}
const url = `${apiBaseUrl}api/mc/availability`;
try {
await http.get(url);
} catch (err) {
apiErrorHandler(err);
}
logger.debug('merge confidence API - successfully authenticated');
return;
}
function getApiBaseUrl(): string {
const defaultBaseUrl = 'https://badges.renovateapi.com/';
const baseFromEnv = process.env.RENOVATE_X_MERGE_CONFIDENCE_API_BASE_URL;
if (is.nullOrUndefined(baseFromEnv)) {
logger.trace('using default merge confidence API base URL');
return defaultBaseUrl;
}
try {
const parsedBaseUrl = new URL(baseFromEnv).toString();
logger.trace(
{ baseUrl: parsedBaseUrl },
'using merge confidence API base found in environment variables'
);
return parsedBaseUrl;
} catch (err) {
logger.warn(
{ err, baseFromEnv },
'invalid merge confidence API base URL found in environment variables - using default value instead'
);
return defaultBaseUrl;
}
}
function getApiToken(): string | undefined {
return hostRules.find({
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');
}