@qawolf/ci-sdk 0.23.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -198
- package/dist/index.cjs +632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/{index.d.mts → index.d.cts} +6 -211
- package/dist/index.d.ts +6 -211
- package/dist/index.js +58 -1047
- package/dist/index.js.map +1 -1
- package/package.json +23 -25
- package/dist/index.mjs +0 -1610
- package/dist/index.mjs.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
+
|
|
6
|
+
// src/lib/other.ts
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
__name(sleep, "sleep");
|
|
11
|
+
function pluralize(count) {
|
|
12
|
+
return count === 1 ? "" : "s";
|
|
13
|
+
}
|
|
14
|
+
__name(pluralize, "pluralize");
|
|
15
|
+
var buildFetchWithTimeout = /* @__PURE__ */ __name((fetch, timeout) => async (...args) => fetch(args[0], {
|
|
16
|
+
...args[1],
|
|
17
|
+
signal: AbortSignal.timeout(timeout)
|
|
18
|
+
}), "buildFetchWithTimeout");
|
|
19
|
+
|
|
20
|
+
// src/lib/sdk/defaults/fetch.ts
|
|
21
|
+
var defaultFetch = globalThis.fetch;
|
|
22
|
+
|
|
23
|
+
// src/lib/sdk/defaults/log.ts
|
|
24
|
+
var defaultLogDriver = {
|
|
25
|
+
error(message, error) {
|
|
26
|
+
console.error(message, error);
|
|
27
|
+
},
|
|
28
|
+
info(message) {
|
|
29
|
+
console.log(message);
|
|
30
|
+
},
|
|
31
|
+
warn(message) {
|
|
32
|
+
console.warn(message);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/lib/sdk/defaults/serviceBase.ts
|
|
37
|
+
var defaultServiceBase = "https://app.qawolf.com/";
|
|
38
|
+
|
|
39
|
+
// package.json
|
|
40
|
+
var package_default = {
|
|
41
|
+
name: "@qawolf/ci-sdk",
|
|
42
|
+
version: "1.1.0",
|
|
43
|
+
private: false,
|
|
44
|
+
description: "A simple SDK for interacting with QAWolf in CI scripts.",
|
|
45
|
+
keywords: [],
|
|
46
|
+
license: "MIT",
|
|
47
|
+
author: "Jules Sam. Randolph<jules@qawolf.com>",
|
|
48
|
+
sideEffects: false,
|
|
49
|
+
type: "module",
|
|
50
|
+
exports: {
|
|
51
|
+
".": {
|
|
52
|
+
types: "./dist/index.d.ts",
|
|
53
|
+
require: "./dist/index.cjs",
|
|
54
|
+
import: "./dist/index.js",
|
|
55
|
+
default: "./dist/index.js"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
files: [
|
|
59
|
+
"/dist",
|
|
60
|
+
"/README.md"
|
|
61
|
+
],
|
|
62
|
+
scripts: {
|
|
63
|
+
build: "tsup src/index.ts --format esm,cjs --dts --clean --splitting --treeshake --sourcemap --outDir dist --target es2022",
|
|
64
|
+
lint: "dpdm --exit-code circular:1 --no-tree --no-warning --transform --include='.*' --exclude='/(dist|node_modules)/' '**/*' && eslint . --ext cjs,cts,js,jsx,mjs,ts,tsx --quiet && prettier --check .",
|
|
65
|
+
"lint:fix": "eslint . --ext cjs,cts,js,jsx,mjs,ts,tsx --fix --quiet && prettier --log-level=warn --write .",
|
|
66
|
+
"lint:warnings": "eslint . --ext cjs,cts,js,jsx,mjs,ts,tsx",
|
|
67
|
+
prepublishOnly: "nx run ci-sdk:build",
|
|
68
|
+
test: 'NODE_OPTIONS="--experimental-vm-modules" NODE_NO_WARNINGS=1 jest',
|
|
69
|
+
"tsc:check": "tsc --noEmit"
|
|
70
|
+
},
|
|
71
|
+
dependencies: {
|
|
72
|
+
"@qawolf/ci-utils": "^1.0.0",
|
|
73
|
+
"@sinonjs/fake-timers": "^10.3.0",
|
|
74
|
+
slugify: "^1.6.6",
|
|
75
|
+
tslib: "^2.6.2"
|
|
76
|
+
},
|
|
77
|
+
devDependencies: {
|
|
78
|
+
tsup: "8.3.5"
|
|
79
|
+
},
|
|
80
|
+
engines: {
|
|
81
|
+
node: ">=20"
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/lib/sdk/defaults/userAgent.ts
|
|
86
|
+
var defaultUserAgent = `ci-sdk/${package_default.version}`;
|
|
87
|
+
|
|
88
|
+
// src/lib/api/log.ts
|
|
89
|
+
async function logFetchError({ log, methodName, response }) {
|
|
90
|
+
if (response.ok) return;
|
|
91
|
+
const { eventId, failureMessage } = await (async () => {
|
|
92
|
+
try {
|
|
93
|
+
const body = await response.json().catch(() => response.text());
|
|
94
|
+
if (typeof body === "object" && body !== null) {
|
|
95
|
+
return {
|
|
96
|
+
eventId: body.eventId,
|
|
97
|
+
failureMessage: body.failureMessage
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
eventId: void 0,
|
|
102
|
+
failureMessage: body
|
|
103
|
+
};
|
|
104
|
+
} catch {
|
|
105
|
+
return {
|
|
106
|
+
eventId: undefined,
|
|
107
|
+
failureMessage: undefined
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
const eventIdSuffix = eventId ? ` (Event ID: ${eventId})` : "";
|
|
112
|
+
const messageSuffix = failureMessage ? `: ${failureMessage}` : "";
|
|
113
|
+
const statusText = response.status >= 500 ? "Server Error" : response.statusText || "Unknown Error";
|
|
114
|
+
log.error(`\u274C [${methodName}] ${response.status} ${statusText}${eventIdSuffix}${messageSuffix}`);
|
|
115
|
+
}
|
|
116
|
+
__name(logFetchError, "logFetchError");
|
|
117
|
+
|
|
118
|
+
// src/lib/sdk/domain/attemptDeploy/index.ts
|
|
119
|
+
async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
120
|
+
const { fetch: localFetch, log } = deps;
|
|
121
|
+
try {
|
|
122
|
+
const response = await localFetch(new URL("/api/webhooks/deploy_success", apiConfig.serviceBase), {
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
branch: config.branch,
|
|
125
|
+
commit_url: config.commitUrl,
|
|
126
|
+
deduplication_key: config.deduplicationKey,
|
|
127
|
+
deployment_type: config.deploymentType,
|
|
128
|
+
deployment_url: config.deploymentUrl,
|
|
129
|
+
..."ephemeralEnvironment" in config ? {
|
|
130
|
+
ephemeral_environment: config.ephemeralEnvironment
|
|
131
|
+
} : "hostingService" in config && {
|
|
132
|
+
hosting_service: config.hostingService,
|
|
133
|
+
merge_request_number: config.hostingService === "GitLab" ? config.mergeRequestNumber : void 0,
|
|
134
|
+
pull_request_number: config.hostingService === "GitHub" ? config.pullRequestNumber : void 0,
|
|
135
|
+
repository: config.repository
|
|
136
|
+
},
|
|
137
|
+
sha: config.sha,
|
|
138
|
+
variables: config.variables
|
|
139
|
+
}),
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${apiConfig.apiKey}`,
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
"User-Agent": apiConfig.userAgent
|
|
144
|
+
},
|
|
145
|
+
method: "POST"
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) return handleErrorResponse(response, log);
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
return handleSuccessResponse(data, log);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.error(`\u{1F6AB} Network error from deploy_success, aborting.`);
|
|
152
|
+
return {
|
|
153
|
+
abortReason: "network-error",
|
|
154
|
+
httpStatus: undefined,
|
|
155
|
+
outcome: "aborted"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
__name(attemptNotifyDeploy, "attemptNotifyDeploy");
|
|
160
|
+
async function handleErrorResponse(response, log) {
|
|
161
|
+
const status = response.status;
|
|
162
|
+
let abortReason;
|
|
163
|
+
if (status === undefined) abortReason = "network-error";
|
|
164
|
+
else if (status === 401) abortReason = "401-unauthorized";
|
|
165
|
+
else if (status === 403) abortReason = "403-forbidden";
|
|
166
|
+
else if (status >= 500) abortReason = "5XX-server-error";
|
|
167
|
+
else abortReason = "4XX-client-error";
|
|
168
|
+
await logFetchError({
|
|
169
|
+
log,
|
|
170
|
+
methodName: "attemptNotifyDeploy",
|
|
171
|
+
response
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
abortReason,
|
|
175
|
+
httpStatus: status,
|
|
176
|
+
outcome: "aborted"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
__name(handleErrorResponse, "handleErrorResponse");
|
|
180
|
+
async function handleSuccessResponse(data, log) {
|
|
181
|
+
const { environment, results } = data;
|
|
182
|
+
const isPreviewDeployment = environment && !environment.isStatic;
|
|
183
|
+
if (environment) {
|
|
184
|
+
const logPrefix = environment.isStatic ? "Static environment" : "Preview environment";
|
|
185
|
+
log.info(`${logPrefix}: ${environment.alias} (id: ${environment.id})`);
|
|
186
|
+
}
|
|
187
|
+
if (results.length === 0) {
|
|
188
|
+
if (isPreviewDeployment) {
|
|
189
|
+
log.info(`\u2705 Deployment notification was successful. No run was created for this deployment.`);
|
|
190
|
+
return {
|
|
191
|
+
environmentId: environment.id,
|
|
192
|
+
outcome: "success",
|
|
193
|
+
runId: undefined
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
log.error(`\u{1F6AB} No trigger matched the request. Please contact support.`);
|
|
197
|
+
return {
|
|
198
|
+
failReason: "no-matched-trigger",
|
|
199
|
+
outcome: "failed"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (!environment) {
|
|
203
|
+
log.error(`\u{1F6AB} No environment matched the request. Please contact support.`);
|
|
204
|
+
return {
|
|
205
|
+
failReason: "no-environment",
|
|
206
|
+
outcome: "failed"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const failedTrigger = results.find((t) => "failure_reason" in t);
|
|
210
|
+
if (failedTrigger) {
|
|
211
|
+
log.error(`\u{1F6AB} Run could not be created. Reason: "${failedTrigger.failure_reason}". Please contact support.`);
|
|
212
|
+
return {
|
|
213
|
+
failReason: "run-could-not-be-created",
|
|
214
|
+
outcome: "failed"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const duplicateSuite = results.find((t) => "duplicate_suite_id" in t);
|
|
218
|
+
if (duplicateSuite) {
|
|
219
|
+
log.info(`\u2705 Deployment notification was successful. Previous run ID: ${duplicateSuite.duplicate_suite_id} already exists for this deployment.`);
|
|
220
|
+
return {
|
|
221
|
+
environmentId: environment.id,
|
|
222
|
+
outcome: "success",
|
|
223
|
+
runId: duplicateSuite.duplicate_suite_id
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const successfulTrigger = results.find((t) => "created_suite_id" in t);
|
|
227
|
+
if (!successfulTrigger) {
|
|
228
|
+
log.error(`\u{1F6AB} No trigger matched the request. Please contact support.`);
|
|
229
|
+
return {
|
|
230
|
+
failReason: "no-matched-trigger",
|
|
231
|
+
outcome: "failed"
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
log.info(`\u2705 Deployment notification was successful. Run created with ID: ${successfulTrigger.created_suite_id}`);
|
|
235
|
+
return {
|
|
236
|
+
environmentId: environment.id,
|
|
237
|
+
outcome: "success",
|
|
238
|
+
runId: successfulTrigger.created_suite_id
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
__name(handleSuccessResponse, "handleSuccessResponse");
|
|
242
|
+
|
|
243
|
+
// src/lib/api/generate-signed-url-for-run-inputs-executables.ts
|
|
244
|
+
async function callGenerateSignedUrlForRunInputsExecutablesStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath }, { fetch: localFetch, log }) {
|
|
245
|
+
try {
|
|
246
|
+
const response = await localFetch(new URL(`${serviceBase}/api/v0/run-inputs-executables-signed-urls?file=${encodeURIComponent(destinationFilePath)}`), {
|
|
247
|
+
headers: {
|
|
248
|
+
Authorization: `Bearer ${apiKey}`,
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
"User-Agent": userAgent
|
|
251
|
+
},
|
|
252
|
+
method: "GET"
|
|
253
|
+
});
|
|
254
|
+
if (response.status === 200) {
|
|
255
|
+
const json = await response.json();
|
|
256
|
+
return {
|
|
257
|
+
fileLocation: json.fileLocation,
|
|
258
|
+
httpStatus: response.status,
|
|
259
|
+
playgroundFileLocation: json.playgroundFileLocation,
|
|
260
|
+
signedUrl: json.signedUrl,
|
|
261
|
+
success: true
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
await logFetchError({
|
|
265
|
+
log,
|
|
266
|
+
methodName: "callGenerateSignedUrlForRunInputsExecutablesStorage",
|
|
267
|
+
response
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
errorMessage: await response.text(),
|
|
271
|
+
httpStatus: response.status,
|
|
272
|
+
success: false
|
|
273
|
+
};
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return {
|
|
276
|
+
errorMessage: "Network error, aborting request to generate signed URL",
|
|
277
|
+
httpStatus: 0,
|
|
278
|
+
success: false
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
__name(callGenerateSignedUrlForRunInputsExecutablesStorage, "callGenerateSignedUrlForRunInputsExecutablesStorage");
|
|
283
|
+
|
|
284
|
+
// src/lib/sdk/domain/generateSignedUrls/generate-signed-url-for-run-inputs-executables.ts
|
|
285
|
+
async function generateSignedUrlForRunInputsExecutablesStorage(deps, apiConfig, config) {
|
|
286
|
+
const { log } = deps;
|
|
287
|
+
if (!config.destinationFilePath) {
|
|
288
|
+
log.error(`\u{1F6AB} destinationFilePath is a required config parameter`);
|
|
289
|
+
return {
|
|
290
|
+
abortReason: "sdk-argument-validation-failed",
|
|
291
|
+
success: false
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const responseState = await callGenerateSignedUrlForRunInputsExecutablesStorage(apiConfig, config, deps);
|
|
295
|
+
if (!responseState.success) {
|
|
296
|
+
let abortReason = "XXX-other-http-code";
|
|
297
|
+
if (responseState.httpStatus >= 500) {
|
|
298
|
+
abortReason = "5XX-server-error";
|
|
299
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) when generating signed upload url: aborting.`);
|
|
300
|
+
} else {
|
|
301
|
+
if (responseState.httpStatus === 400) abortReason = "400-invalid-request";
|
|
302
|
+
else if (responseState.httpStatus === 401) abortReason = "401-invalid-credentials";
|
|
303
|
+
else if (responseState.httpStatus === 403) abortReason = "403-forbidden";
|
|
304
|
+
else if (responseState.httpStatus === 0) abortReason = "client-network-error";
|
|
305
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) when generating signed upload url: ${responseState.errorMessage ? `${responseState.errorMessage}.` : ""} aborting.`);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
abortReason,
|
|
309
|
+
httpStatus: responseState.httpStatus,
|
|
310
|
+
success: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
log.info(`\u2705 Generated Signed Upload URL: ${responseState.signedUrl}.`);
|
|
314
|
+
return {
|
|
315
|
+
fileLocation: responseState.fileLocation,
|
|
316
|
+
playgroundFileLocation: responseState.playgroundFileLocation,
|
|
317
|
+
success: true,
|
|
318
|
+
uploadUrl: responseState.signedUrl
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
__name(generateSignedUrlForRunInputsExecutablesStorage, "generateSignedUrlForRunInputsExecutablesStorage");
|
|
322
|
+
|
|
323
|
+
// src/lib/api/generate-signed-url-for-team-storage.ts
|
|
324
|
+
async function callGenerateSignedUrlForTempTeamStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath }, { fetch: localFetch, log }) {
|
|
325
|
+
try {
|
|
326
|
+
const response = await localFetch(new URL(`${serviceBase}/api/v0/team-storage-signed-url?file=${destinationFilePath}`), {
|
|
327
|
+
headers: {
|
|
328
|
+
Authorization: `Bearer ${apiKey}`,
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
"User-Agent": userAgent
|
|
331
|
+
},
|
|
332
|
+
method: "GET"
|
|
333
|
+
});
|
|
334
|
+
if (response.status === 200) {
|
|
335
|
+
const json = await response.json();
|
|
336
|
+
return {
|
|
337
|
+
fileLocation: json.fileLocation,
|
|
338
|
+
httpStatus: response.status,
|
|
339
|
+
playgroundFileLocation: json.playgroundFileLocation,
|
|
340
|
+
signedUrl: json.signedUrl,
|
|
341
|
+
success: true
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
await logFetchError({
|
|
345
|
+
log,
|
|
346
|
+
methodName: "callGenerateSignedUrlForTempTeamStorage",
|
|
347
|
+
response
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
errorMessage: await response.text(),
|
|
351
|
+
httpStatus: response.status,
|
|
352
|
+
success: false
|
|
353
|
+
};
|
|
354
|
+
} catch (e) {
|
|
355
|
+
return {
|
|
356
|
+
errorMessage: "Network error, aborting request to generate signed URL",
|
|
357
|
+
httpStatus: 0,
|
|
358
|
+
success: false
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
__name(callGenerateSignedUrlForTempTeamStorage, "callGenerateSignedUrlForTempTeamStorage");
|
|
363
|
+
|
|
364
|
+
// src/lib/sdk/domain/generateSignedUrls/generate-signed-url-for-team-storage.ts
|
|
365
|
+
async function generateSignedUrlForTempTeamStorage(deps, apiConfig, config) {
|
|
366
|
+
const { log } = deps;
|
|
367
|
+
if (!config.destinationFilePath) {
|
|
368
|
+
log.error(`\u{1F6AB} destinationFilePath is a required config parameter`);
|
|
369
|
+
return {
|
|
370
|
+
abortReason: "sdk-argument-validation-failed",
|
|
371
|
+
success: false
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const responseState = await callGenerateSignedUrlForTempTeamStorage(apiConfig, config, deps);
|
|
375
|
+
if (!responseState.success) {
|
|
376
|
+
let abortReason = "XXX-other-http-code";
|
|
377
|
+
if (responseState.httpStatus >= 500) {
|
|
378
|
+
abortReason = "5XX-server-error";
|
|
379
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) when generating signed upload url: aborting.`);
|
|
380
|
+
} else {
|
|
381
|
+
if (responseState.httpStatus === 400) abortReason = "400-invalid-request";
|
|
382
|
+
else if (responseState.httpStatus === 401) abortReason = "401-invalid-credentials";
|
|
383
|
+
else if (responseState.httpStatus === 403) abortReason = "403-forbidden";
|
|
384
|
+
else if (responseState.httpStatus === 0) abortReason = "client-network-error";
|
|
385
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) when generating signed upload url: ${responseState.errorMessage ? `${responseState.errorMessage}.` : ""} aborting.`);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
abortReason,
|
|
389
|
+
httpStatus: responseState.httpStatus,
|
|
390
|
+
success: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
log.info(`\u2705 Generated Signed Upload URL: ${responseState.signedUrl}.`);
|
|
394
|
+
return {
|
|
395
|
+
fileLocation: responseState.fileLocation,
|
|
396
|
+
playgroundFileLocation: responseState.playgroundFileLocation,
|
|
397
|
+
success: true,
|
|
398
|
+
uploadUrl: responseState.signedUrl
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
__name(generateSignedUrlForTempTeamStorage, "generateSignedUrlForTempTeamStorage");
|
|
402
|
+
|
|
403
|
+
// src/lib/api/ci-greenlight.ts
|
|
404
|
+
async function fetchCiGreenlightStatus({ apiKey, serviceBase, userAgent }, { runId }, { fetch: localFetch, log }) {
|
|
405
|
+
try {
|
|
406
|
+
const resp = await localFetch(new URL(`/api/v0/ci-greenlight/${encodeURIComponent(runId)}`, serviceBase), {
|
|
407
|
+
headers: {
|
|
408
|
+
Authorization: `Bearer ${apiKey}`,
|
|
409
|
+
"User-Agent": userAgent
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
if (resp.ok) {
|
|
413
|
+
const greenlightStatus = await resp.json();
|
|
414
|
+
return {
|
|
415
|
+
greenlightStatus,
|
|
416
|
+
httpStatus: resp.status,
|
|
417
|
+
ok: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
await logFetchError({
|
|
421
|
+
log,
|
|
422
|
+
methodName: "fetchCiGreenlightStatus",
|
|
423
|
+
response: resp
|
|
424
|
+
});
|
|
425
|
+
if (resp.status === 404) {
|
|
426
|
+
return {
|
|
427
|
+
canRetry: true,
|
|
428
|
+
errorType: "not-found",
|
|
429
|
+
httpStatus: resp.status,
|
|
430
|
+
ok: false
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (resp.status >= 500) {
|
|
434
|
+
return {
|
|
435
|
+
canRetry: true,
|
|
436
|
+
errorType: "server",
|
|
437
|
+
httpStatus: resp.status,
|
|
438
|
+
ok: false
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
canRetry: false,
|
|
443
|
+
errorType: "client",
|
|
444
|
+
httpStatus: resp.status,
|
|
445
|
+
ok: false
|
|
446
|
+
};
|
|
447
|
+
} catch (e) {
|
|
448
|
+
return {
|
|
449
|
+
canRetry: true,
|
|
450
|
+
errorType: "network",
|
|
451
|
+
httpStatus: 0,
|
|
452
|
+
ok: false
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
__name(fetchCiGreenlightStatus, "fetchCiGreenlightStatus");
|
|
457
|
+
|
|
458
|
+
// src/lib/sdk/domain/poll-ci-greenlight.ts
|
|
459
|
+
var safeDefaults = {
|
|
460
|
+
abortOnSuperseded: false,
|
|
461
|
+
maxRetries: 10,
|
|
462
|
+
pollTimeout: 120 * 60 * 1e3,
|
|
463
|
+
retryInterval: 10 * 1e3,
|
|
464
|
+
runInProgressInterval: 30 * 1e3,
|
|
465
|
+
runUnderReviewInterval: 60 * 1e3
|
|
466
|
+
};
|
|
467
|
+
async function pollCiGreenlightStatus(deps, apiConfig, { onRunStageChanged = /* @__PURE__ */ __name(() => undefined, "onRunStageChanged"), runId, ...pollConfig }) {
|
|
468
|
+
let retries = 0;
|
|
469
|
+
const { log } = deps;
|
|
470
|
+
const { abortOnSuperseded = safeDefaults.abortOnSuperseded, maxRetries = safeDefaults.maxRetries, pollTimeout = safeDefaults.pollTimeout, retryInterval = safeDefaults.retryInterval, runInProgressInterval = safeDefaults.runInProgressInterval, runUnderReviewInterval = safeDefaults.runUnderReviewInterval } = pollConfig;
|
|
471
|
+
let currentStatus = undefined;
|
|
472
|
+
const startedAt = Date.now();
|
|
473
|
+
let hasTimedOut = false;
|
|
474
|
+
while (!(hasTimedOut = Date.now() - startedAt > pollTimeout)) {
|
|
475
|
+
const responseState = await fetchCiGreenlightStatus(apiConfig, {
|
|
476
|
+
runId
|
|
477
|
+
}, deps);
|
|
478
|
+
if (!responseState.ok && !responseState.canRetry) {
|
|
479
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) from CI greenlight, aborting.`);
|
|
480
|
+
return {
|
|
481
|
+
abortReason: "4XX-client-error",
|
|
482
|
+
httpStatus: responseState.httpStatus,
|
|
483
|
+
outcome: "aborted"
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (!responseState.ok) {
|
|
487
|
+
retries += 1;
|
|
488
|
+
if (retries > maxRetries) {
|
|
489
|
+
log.error(`\u{1F6AB} Unrecoverable error${responseState.httpStatus ? ` (status ${responseState.httpStatus})` : ""} from CI greenlight, aborting after ${maxRetries} attempts.`);
|
|
490
|
+
if (responseState.errorType === "network") {
|
|
491
|
+
return {
|
|
492
|
+
abortReason: "network-error",
|
|
493
|
+
httpStatus: undefined,
|
|
494
|
+
outcome: "aborted"
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
let abortReason = "XXX-other-http-code";
|
|
498
|
+
if (responseState.errorType === "not-found") abortReason = "404-run-not-found";
|
|
499
|
+
else if (responseState.errorType === "server") abortReason = "5XX-server-error";
|
|
500
|
+
else ;
|
|
501
|
+
return {
|
|
502
|
+
abortReason,
|
|
503
|
+
httpStatus: responseState.httpStatus,
|
|
504
|
+
outcome: "aborted"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const { errorType, httpStatus } = responseState;
|
|
508
|
+
const retryLabel = `${httpStatus ? ` (status ${httpStatus})` : ""}, will try again... (${retries}/${maxRetries})`;
|
|
509
|
+
if (errorType === "network") log.warn(`\u274C Network error${retryLabel}`);
|
|
510
|
+
else if (errorType === "not-found") log.info(`\u23F3 Run not found${retryLabel}`);
|
|
511
|
+
else if (errorType === "server") log.warn(`\u274C Server error${retryLabel}`);
|
|
512
|
+
await sleep(retryInterval);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
retries = 0;
|
|
516
|
+
const { greenlightStatus } = responseState;
|
|
517
|
+
const previousStatus = currentStatus;
|
|
518
|
+
currentStatus = greenlightStatus;
|
|
519
|
+
if (!previousStatus || previousStatus.runStage !== greenlightStatus.runStage) {
|
|
520
|
+
try {
|
|
521
|
+
await onRunStageChanged(greenlightStatus, previousStatus);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
log.error(`Error in onRunStageChanged callback`, e);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const urlInfo = `See ${greenlightStatus.relevantRunUrl}`;
|
|
527
|
+
const { greenlight, relevantRunId, relevantRunUrl, relevantRunWithBugsUrl, rootRunId, rootRunUrl, runStage } = greenlightStatus;
|
|
528
|
+
if (rootRunId !== relevantRunId) {
|
|
529
|
+
const supersedingDetails = `
|
|
530
|
+
- Root: ${rootRunUrl}
|
|
531
|
+
- Superseding: ${relevantRunUrl}`;
|
|
532
|
+
if (abortOnSuperseded) {
|
|
533
|
+
log.warn(`\u274C Aborting due to superseded run (option 'abortOnSuperseded' is enabled).${supersedingDetails}`);
|
|
534
|
+
return {
|
|
535
|
+
abortReason: "superseded-run",
|
|
536
|
+
httpStatus: undefined,
|
|
537
|
+
outcome: "aborted"
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
log.info(`Root run was superseded:${supersedingDetails}`);
|
|
541
|
+
}
|
|
542
|
+
if (runStage === "completed") {
|
|
543
|
+
const { blockingBugsCount, blockingBugUrls = [], nonBlockingBugsCount, nonBlockingBugUrls = [] } = greenlightStatus;
|
|
544
|
+
const blockingBugList = blockingBugUrls.map((url) => `
|
|
545
|
+
\u2022 ${url}`).join("");
|
|
546
|
+
const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
|
|
547
|
+
\u2022 ${url}`).join("");
|
|
548
|
+
const bugsListInText = `${blockingBugUrls.length > 0 ? `
|
|
549
|
+
BLOCKING BUGS:${blockingBugList}` : ""}${nonBlockingBugUrls.length > 0 ? `
|
|
550
|
+
NON BLOCKING BUGS:${nonBlockingBugList}` : ""}`;
|
|
551
|
+
if (greenlight) {
|
|
552
|
+
log.info(`\u2705 Run passed and no blocking bugs found${nonBlockingBugsCount > 0 ? ` and ${nonBlockingBugsCount} non-blocking bug${pluralize(nonBlockingBugsCount)} found` : ""}.
|
|
553
|
+
${urlInfo}`);
|
|
554
|
+
return {
|
|
555
|
+
greenlightStatus,
|
|
556
|
+
outcome: "success"
|
|
557
|
+
};
|
|
558
|
+
} else {
|
|
559
|
+
log.warn(`\u274C Run failed and ${blockingBugsCount} blocking bug${pluralize(blockingBugsCount)} found${nonBlockingBugsCount ? ` and ${nonBlockingBugsCount} non-blocking bug${pluralize(nonBlockingBugsCount)}` : ""}.
|
|
560
|
+
See ${relevantRunWithBugsUrl}
|
|
561
|
+
You may review the priority of certain bugs to mark these non-blocking and retry the job.
|
|
562
|
+
` + bugsListInText);
|
|
563
|
+
return {
|
|
564
|
+
greenlightStatus,
|
|
565
|
+
outcome: "failed"
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (runStage === "canceled") {
|
|
570
|
+
log.info(`\u{1F6AB} Run was canceled. Aborting... Please contact support.
|
|
571
|
+
${urlInfo}`);
|
|
572
|
+
return {
|
|
573
|
+
abortReason: "run-canceled",
|
|
574
|
+
httpStatus: undefined,
|
|
575
|
+
outcome: "aborted"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (hasTimedOut) {
|
|
579
|
+
log.warn(`\u274C Poll timed out after ${pollTimeout / 1e3}s. Aborting...`);
|
|
580
|
+
return {
|
|
581
|
+
abortReason: "poll-timed-out",
|
|
582
|
+
httpStatus: undefined,
|
|
583
|
+
outcome: "aborted"
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (runStage === "initializing") {
|
|
587
|
+
log.info(`\u23F3 Run is in progress... ${urlInfo}`);
|
|
588
|
+
await sleep(runInProgressInterval);
|
|
589
|
+
} else if (runStage === "underReview") {
|
|
590
|
+
log.info(`\u{1F575}\uFE0F Run is under review... ${urlInfo}`);
|
|
591
|
+
await sleep(runUnderReviewInterval);
|
|
592
|
+
} else throw Error(`Unexpected run stage: ${runStage}`);
|
|
593
|
+
}
|
|
594
|
+
log.warn(`\u274C Poll timed out after ${pollTimeout / 1e3}s. Aborting...`);
|
|
595
|
+
return {
|
|
596
|
+
abortReason: "poll-timed-out",
|
|
597
|
+
httpStatus: undefined,
|
|
598
|
+
outcome: "aborted"
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
__name(pollCiGreenlightStatus, "pollCiGreenlightStatus");
|
|
602
|
+
|
|
603
|
+
// src/lib/sdk/index.ts
|
|
604
|
+
function makeQaWolfSdk({ apiKey, serviceBase = defaultServiceBase, userAgent = defaultUserAgent }, { fetch = defaultFetch, log = defaultLogDriver } = {}) {
|
|
605
|
+
if (typeof fetch !== "function") {
|
|
606
|
+
throw Error(`QA Wolf CI-SDK requires fetch to be defined. Make sure you are using NodeJS 18+, OR pass a fetch polyfill to the makeQaWolfSdk function.
|
|
607
|
+
We recommend 'undici' package for that purpose. See the Requirement section of our README for more guidance.`);
|
|
608
|
+
}
|
|
609
|
+
const deps = {
|
|
610
|
+
// Versions of fetch may have different default timeouts. For consistency, we explicitly set it to 2 minutes here.
|
|
611
|
+
// The increased timeout helps avoid edge cases where we need to search for a commit against multiple
|
|
612
|
+
// code hosting service repositories as a fallback when not finding the repository based on organization/name.
|
|
613
|
+
fetch: buildFetchWithTimeout(fetch, 12e4),
|
|
614
|
+
log
|
|
615
|
+
};
|
|
616
|
+
const apiConfig = {
|
|
617
|
+
apiKey,
|
|
618
|
+
serviceBase,
|
|
619
|
+
userAgent
|
|
620
|
+
};
|
|
621
|
+
return {
|
|
622
|
+
attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
|
|
623
|
+
generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(null, deps, apiConfig),
|
|
624
|
+
generateSignedUrlForTempTeamStorage: generateSignedUrlForTempTeamStorage.bind(null, deps, apiConfig),
|
|
625
|
+
pollCiGreenlightStatus: pollCiGreenlightStatus.bind(null, deps, apiConfig)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
__name(makeQaWolfSdk, "makeQaWolfSdk");
|
|
629
|
+
|
|
630
|
+
exports.makeQaWolfSdk = makeQaWolfSdk;
|
|
631
|
+
//# sourceMappingURL=index.cjs.map
|
|
632
|
+
//# sourceMappingURL=index.cjs.map
|