@qawolf/ci-sdk 1.1.0 → 1.2.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 +126 -0
- package/dist/index.cjs +278 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -2
- package/dist/index.d.ts +67 -2
- package/dist/index.js +278 -149
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ to determine the status of your CI/CD step/job/action.
|
|
|
14
14
|
- [Notify Deployment](#notify-deployment)
|
|
15
15
|
- [Notify Preview Deployment (Pull Request / Merge Request Testing)](#notify-preview)
|
|
16
16
|
- [Poll for CI Greenlight Status](#ci-greenlight)
|
|
17
|
+
- [Advanced: Poll with Custom Control (Iterator)](#ci-greenlight-iterator)
|
|
17
18
|
- [Upload Run Input Artifacts](#upload-artifacts)
|
|
18
19
|
- [Requirements](#requirements)
|
|
19
20
|
- [Versioning](#versioning)
|
|
@@ -180,6 +181,127 @@ const { pollCiGreenlightStatus } = makeQaWolfSdk({
|
|
|
180
181
|
})();
|
|
181
182
|
```
|
|
182
183
|
|
|
184
|
+
<a id="ci-greenlight-iterator"></a>
|
|
185
|
+
|
|
186
|
+
## Advanced: Poll with Custom Control (Iterator)
|
|
187
|
+
|
|
188
|
+
For advanced use cases where you need fine-grained control over the polling lifecycle, use `makePollCiGreenlightStatusIterator`. This async generator gives you full control to implement custom logic such as:
|
|
189
|
+
|
|
190
|
+
- Stop polling early based on time limits (e.g., "wait max 10 minutes in review then proceed")
|
|
191
|
+
- Stop polling based on bug counts (e.g., "proceed if only 3 or fewer blocking bugs")
|
|
192
|
+
- Implement custom logging or monitoring at each poll iteration
|
|
193
|
+
- Access detailed bug data during the "under review" stage
|
|
194
|
+
|
|
195
|
+
> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/1b170576efea411fa785842a71e7c99e).
|
|
196
|
+
|
|
197
|
+
### Example: Early Exit with Custom Logic
|
|
198
|
+
|
|
199
|
+
> ⚠️ **Important**: When using the iterator, ensure you handle all run stages in your switch statement. The `default` case with `satisfies never` provides compile-time safety - if a new stage is added, TypeScript will error. This prevents silently passing your CI job while ignoring error conditions.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
|
|
203
|
+
|
|
204
|
+
const { makePollCiGreenlightStatusIterator } = makeQaWolfSdk({
|
|
205
|
+
apiKey: "qawolf_xxxxx",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
(async () => {
|
|
209
|
+
let underReviewStartTime: number | null = null;
|
|
210
|
+
|
|
211
|
+
const iterator = makePollCiGreenlightStatusIterator({
|
|
212
|
+
runId: "your-run-id",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
for await (const iteration of iterator) {
|
|
216
|
+
// Check if the iterator yielded an abort result
|
|
217
|
+
if (iteration.isAborted) {
|
|
218
|
+
console.error(`Poll aborted: ${iteration.abortReason}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle status iterations
|
|
223
|
+
const { status, stageChanged } = iteration;
|
|
224
|
+
|
|
225
|
+
switch (status.runStage) {
|
|
226
|
+
case "initializing":
|
|
227
|
+
console.log(`Run stage: ${status.runStage}`);
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case "underReview": {
|
|
231
|
+
// Track when we first enter underReview
|
|
232
|
+
if (stageChanged) {
|
|
233
|
+
underReviewStartTime = Date.now();
|
|
234
|
+
console.log(`Run stage: ${status.runStage}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const timeInUnderReview = underReviewStartTime
|
|
238
|
+
? Date.now() - underReviewStartTime
|
|
239
|
+
: 0;
|
|
240
|
+
|
|
241
|
+
// Option 1: Stop after 10 minutes in review
|
|
242
|
+
if (timeInUnderReview > 10 * 60 * 1000) {
|
|
243
|
+
console.log("10 minutes in review, proceeding with deployment");
|
|
244
|
+
// Decide whether to fail CI based on your criteria
|
|
245
|
+
if (status.blockingBugsCount > 5) {
|
|
246
|
+
console.log("Too many blocking bugs, failing CI");
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Option 2: Stop if bug count is acceptable
|
|
253
|
+
if (status.blockingBugsCount <= 3) {
|
|
254
|
+
console.log(
|
|
255
|
+
`Only ${status.blockingBugsCount} blocking bugs, acceptable to proceed`,
|
|
256
|
+
);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case "completed":
|
|
263
|
+
console.log(`Completed with greenlight: ${status.greenlight}`);
|
|
264
|
+
if (!status.greenlight) {
|
|
265
|
+
console.log("Run failed, blocking bugs found");
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
console.log("Run passed successfully");
|
|
269
|
+
return; // Exit the loop
|
|
270
|
+
|
|
271
|
+
case "canceled":
|
|
272
|
+
console.log("Run was canceled");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
|
|
275
|
+
default:
|
|
276
|
+
// Exhaustive check - TypeScript will error if a case is missing
|
|
277
|
+
status.runStage satisfies never;
|
|
278
|
+
throw new Error(`Unexpected run stage: ${status.runStage}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Yielded Iteration Object
|
|
285
|
+
|
|
286
|
+
The iterator yields a discriminated union that can be either a status update or an abort notification:
|
|
287
|
+
|
|
288
|
+
**Status Iteration** (`isAborted: false`):
|
|
289
|
+
|
|
290
|
+
- `isAborted`: `false` - Indicates a normal status update
|
|
291
|
+
- `status`: The current `CiGreenlightStatus` from the API
|
|
292
|
+
- `previousStatus`: The status from the previous iteration (undefined on first iteration)
|
|
293
|
+
- `stageChanged`: Boolean indicating if the run stage changed from the previous iteration
|
|
294
|
+
- `elapsedMs`: Milliseconds elapsed since polling started
|
|
295
|
+
|
|
296
|
+
**Aborted Iteration** (`isAborted: true`):
|
|
297
|
+
|
|
298
|
+
- `isAborted`: `true` - Indicates the polling was aborted
|
|
299
|
+
- `abortReason`: String indicating why polling was aborted (e.g., `"poll-timed-out"`, `"run-canceled"`, `"network-error"`, `"4XX-client-error"`, `"5XX-server-error"`)
|
|
300
|
+
- `httpStatus`: HTTP status code if applicable, otherwise `undefined`
|
|
301
|
+
- `elapsedMs`: Milliseconds elapsed since polling started
|
|
302
|
+
|
|
303
|
+
**Important**: Always check `iteration.isAborted` to determine which type of result you received. When `isAborted === true`, you should handle the abort reason and exit appropriately.
|
|
304
|
+
|
|
183
305
|
<a id="upload-artifacts"></a>
|
|
184
306
|
|
|
185
307
|
## Upload Run Input Artifacts
|
|
@@ -294,6 +416,10 @@ This package follows the [SemVer](https://semver.org/) versioning scheme. Additi
|
|
|
294
416
|
|
|
295
417
|
# Changelog
|
|
296
418
|
|
|
419
|
+
## v1.2.0
|
|
420
|
+
|
|
421
|
+
- New `makePollCiGreenlightStatusIterator` async generator function for advanced polling control with custom early-exit logic
|
|
422
|
+
|
|
297
423
|
## v1.1.0
|
|
298
424
|
|
|
299
425
|
- New `ephemeralEnvironment` parameter for `attemptNotifyDeploy`
|
package/dist/index.cjs
CHANGED
|
@@ -38,49 +38,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
|
|
|
38
38
|
|
|
39
39
|
// package.json
|
|
40
40
|
var package_default = {
|
|
41
|
-
|
|
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
|
-
};
|
|
41
|
+
version: "1.2.0"};
|
|
84
42
|
|
|
85
43
|
// src/lib/sdk/defaults/userAgent.ts
|
|
86
44
|
var defaultUserAgent = `ci-sdk/${package_default.version}`;
|
|
@@ -103,8 +61,8 @@ async function logFetchError({ log, methodName, response }) {
|
|
|
103
61
|
};
|
|
104
62
|
} catch {
|
|
105
63
|
return {
|
|
106
|
-
eventId:
|
|
107
|
-
failureMessage:
|
|
64
|
+
eventId: void 0,
|
|
65
|
+
failureMessage: void 0
|
|
108
66
|
};
|
|
109
67
|
}
|
|
110
68
|
})();
|
|
@@ -151,7 +109,7 @@ async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
|
151
109
|
log.error(`\u{1F6AB} Network error from deploy_success, aborting.`);
|
|
152
110
|
return {
|
|
153
111
|
abortReason: "network-error",
|
|
154
|
-
httpStatus:
|
|
112
|
+
httpStatus: void 0,
|
|
155
113
|
outcome: "aborted"
|
|
156
114
|
};
|
|
157
115
|
}
|
|
@@ -160,7 +118,7 @@ __name(attemptNotifyDeploy, "attemptNotifyDeploy");
|
|
|
160
118
|
async function handleErrorResponse(response, log) {
|
|
161
119
|
const status = response.status;
|
|
162
120
|
let abortReason;
|
|
163
|
-
if (status ===
|
|
121
|
+
if (status === void 0) abortReason = "network-error";
|
|
164
122
|
else if (status === 401) abortReason = "401-unauthorized";
|
|
165
123
|
else if (status === 403) abortReason = "403-forbidden";
|
|
166
124
|
else if (status >= 500) abortReason = "5XX-server-error";
|
|
@@ -190,10 +148,10 @@ async function handleSuccessResponse(data, log) {
|
|
|
190
148
|
return {
|
|
191
149
|
environmentId: environment.id,
|
|
192
150
|
outcome: "success",
|
|
193
|
-
runId:
|
|
151
|
+
runId: void 0
|
|
194
152
|
};
|
|
195
153
|
}
|
|
196
|
-
log.error(`\u{1F6AB} No trigger matched the request. Please contact support.`);
|
|
154
|
+
log.error(`\u{1F6AB} No trigger matched the request. Please make sure there is at least one active test in the QA Wolf environment, or contact support.`);
|
|
197
155
|
return {
|
|
198
156
|
failReason: "no-matched-trigger",
|
|
199
157
|
outcome: "failed"
|
|
@@ -400,6 +358,29 @@ async function generateSignedUrlForTempTeamStorage(deps, apiConfig, config) {
|
|
|
400
358
|
}
|
|
401
359
|
__name(generateSignedUrlForTempTeamStorage, "generateSignedUrlForTempTeamStorage");
|
|
402
360
|
|
|
361
|
+
// src/lib/sdk/domain/pollCiGreenlight/buildCompletedRunResult.ts
|
|
362
|
+
function buildCompletedRunResult(status) {
|
|
363
|
+
if (status.greenlight) return {
|
|
364
|
+
greenlightStatus: status,
|
|
365
|
+
outcome: "success"
|
|
366
|
+
};
|
|
367
|
+
else return {
|
|
368
|
+
greenlightStatus: status,
|
|
369
|
+
outcome: "failed"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
__name(buildCompletedRunResult, "buildCompletedRunResult");
|
|
373
|
+
|
|
374
|
+
// src/lib/sdk/domain/pollCiGreenlight/consts.ts
|
|
375
|
+
var pollCiGreenlightDefaults = {
|
|
376
|
+
abortOnSuperseded: false,
|
|
377
|
+
maxRetries: 10,
|
|
378
|
+
pollTimeout: 120 * 60 * 1e3,
|
|
379
|
+
retryInterval: 10 * 1e3,
|
|
380
|
+
runInProgressInterval: 30 * 1e3,
|
|
381
|
+
runUnderReviewInterval: 60 * 1e3
|
|
382
|
+
};
|
|
383
|
+
|
|
403
384
|
// src/lib/api/ci-greenlight.ts
|
|
404
385
|
async function fetchCiGreenlightStatus({ apiKey, serviceBase, userAgent }, { runId }, { fetch: localFetch, log }) {
|
|
405
386
|
try {
|
|
@@ -455,56 +436,61 @@ async function fetchCiGreenlightStatus({ apiKey, serviceBase, userAgent }, { run
|
|
|
455
436
|
}
|
|
456
437
|
__name(fetchCiGreenlightStatus, "fetchCiGreenlightStatus");
|
|
457
438
|
|
|
458
|
-
// src/lib/sdk/domain/
|
|
459
|
-
|
|
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;
|
|
439
|
+
// src/lib/sdk/domain/pollCiGreenlight/iterator.ts
|
|
440
|
+
async function* makePollCiGreenlightStatusIterator(deps, apiConfig, { runId, ...pollConfig }) {
|
|
469
441
|
const { log } = deps;
|
|
470
|
-
const {
|
|
471
|
-
let
|
|
442
|
+
const { maxRetries = pollCiGreenlightDefaults.maxRetries, pollTimeout = pollCiGreenlightDefaults.pollTimeout, retryInterval = pollCiGreenlightDefaults.retryInterval, runInProgressInterval = pollCiGreenlightDefaults.runInProgressInterval, runUnderReviewInterval = pollCiGreenlightDefaults.runUnderReviewInterval } = pollConfig;
|
|
443
|
+
let retries = 0;
|
|
444
|
+
let previousStatus = void 0;
|
|
472
445
|
const startedAt = Date.now();
|
|
473
|
-
|
|
474
|
-
while (!(hasTimedOut = Date.now() - startedAt > pollTimeout)) {
|
|
446
|
+
while (Date.now() - startedAt <= pollTimeout) {
|
|
475
447
|
const responseState = await fetchCiGreenlightStatus(apiConfig, {
|
|
476
448
|
runId
|
|
477
449
|
}, deps);
|
|
450
|
+
const elapsedMs = Date.now() - startedAt;
|
|
478
451
|
if (!responseState.ok && !responseState.canRetry) {
|
|
479
|
-
|
|
480
|
-
return {
|
|
452
|
+
yield {
|
|
481
453
|
abortReason: "4XX-client-error",
|
|
454
|
+
elapsedMs,
|
|
482
455
|
httpStatus: responseState.httpStatus,
|
|
483
|
-
|
|
456
|
+
isAborted: true
|
|
484
457
|
};
|
|
458
|
+
return;
|
|
485
459
|
}
|
|
486
460
|
if (!responseState.ok) {
|
|
487
461
|
retries += 1;
|
|
462
|
+
const { errorType, httpStatus } = responseState;
|
|
488
463
|
if (retries > maxRetries) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
464
|
+
switch (errorType) {
|
|
465
|
+
case "network":
|
|
466
|
+
yield {
|
|
467
|
+
abortReason: "network-error",
|
|
468
|
+
elapsedMs,
|
|
469
|
+
httpStatus: void 0,
|
|
470
|
+
isAborted: true
|
|
471
|
+
};
|
|
472
|
+
break;
|
|
473
|
+
case "not-found":
|
|
474
|
+
yield {
|
|
475
|
+
abortReason: "404-run-not-found",
|
|
476
|
+
elapsedMs,
|
|
477
|
+
httpStatus: httpStatus ?? 404,
|
|
478
|
+
isAborted: true
|
|
479
|
+
};
|
|
480
|
+
break;
|
|
481
|
+
case "server":
|
|
482
|
+
yield {
|
|
483
|
+
abortReason: "5XX-server-error",
|
|
484
|
+
elapsedMs,
|
|
485
|
+
httpStatus: httpStatus ?? 500,
|
|
486
|
+
isAborted: true
|
|
487
|
+
};
|
|
488
|
+
break;
|
|
489
|
+
default:
|
|
490
|
+
throw Error(`Unexpected error type: ${errorType}`);
|
|
496
491
|
}
|
|
497
|
-
|
|
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
|
-
};
|
|
492
|
+
return;
|
|
506
493
|
}
|
|
507
|
-
const { errorType, httpStatus } = responseState;
|
|
508
494
|
const retryLabel = `${httpStatus ? ` (status ${httpStatus})` : ""}, will try again... (${retries}/${maxRetries})`;
|
|
509
495
|
if (errorType === "network") log.warn(`\u274C Network error${retryLabel}`);
|
|
510
496
|
else if (errorType === "not-found") log.info(`\u23F3 Run not found${retryLabel}`);
|
|
@@ -514,89 +500,231 @@ async function pollCiGreenlightStatus(deps, apiConfig, { onRunStageChanged = /*
|
|
|
514
500
|
}
|
|
515
501
|
retries = 0;
|
|
516
502
|
const { greenlightStatus } = responseState;
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
abortReason: "superseded-run",
|
|
536
|
-
httpStatus: undefined,
|
|
537
|
-
outcome: "aborted"
|
|
503
|
+
const stageChanged = !previousStatus || previousStatus.runStage !== greenlightStatus.runStage;
|
|
504
|
+
const isFailedTerminalStage = greenlightStatus.runStage === "canceled" || greenlightStatus.runStage === "completed" && greenlightStatus.greenlight === false;
|
|
505
|
+
yield {
|
|
506
|
+
elapsedMs,
|
|
507
|
+
isAborted: false,
|
|
508
|
+
isFailedTerminalStage,
|
|
509
|
+
previousStatus,
|
|
510
|
+
stageChanged,
|
|
511
|
+
status: greenlightStatus
|
|
512
|
+
};
|
|
513
|
+
previousStatus = greenlightStatus;
|
|
514
|
+
switch (greenlightStatus.runStage) {
|
|
515
|
+
case "canceled": {
|
|
516
|
+
yield {
|
|
517
|
+
abortReason: "run-canceled",
|
|
518
|
+
elapsedMs,
|
|
519
|
+
httpStatus: void 0,
|
|
520
|
+
isAborted: true
|
|
538
521
|
};
|
|
522
|
+
return;
|
|
539
523
|
}
|
|
540
|
-
|
|
524
|
+
case "completed":
|
|
525
|
+
return;
|
|
526
|
+
case "initializing":
|
|
527
|
+
await sleep(runInProgressInterval);
|
|
528
|
+
break;
|
|
529
|
+
case "underReview":
|
|
530
|
+
await sleep(runUnderReviewInterval);
|
|
531
|
+
break;
|
|
532
|
+
default:
|
|
533
|
+
throw Error(`Unexpected run stage: ${greenlightStatus}`);
|
|
541
534
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
535
|
+
}
|
|
536
|
+
yield {
|
|
537
|
+
abortReason: "poll-timed-out",
|
|
538
|
+
elapsedMs: Date.now() - startedAt,
|
|
539
|
+
httpStatus: void 0,
|
|
540
|
+
isAborted: true
|
|
541
|
+
};
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
__name(makePollCiGreenlightStatusIterator, "makePollCiGreenlightStatusIterator");
|
|
545
|
+
|
|
546
|
+
// src/lib/sdk/domain/pollCiGreenlight/logging.ts
|
|
547
|
+
function buildSuccessLogMessage(status) {
|
|
548
|
+
const { nonBlockingBugsCount, relevantRunUrl } = status;
|
|
549
|
+
const urlInfo = `See ${relevantRunUrl}`;
|
|
550
|
+
return `\u2705 Run passed and no blocking bugs found${nonBlockingBugsCount > 0 ? ` and ${nonBlockingBugsCount} non-blocking bug${pluralize(nonBlockingBugsCount)} found` : ""}.
|
|
551
|
+
${urlInfo}`;
|
|
552
|
+
}
|
|
553
|
+
__name(buildSuccessLogMessage, "buildSuccessLogMessage");
|
|
554
|
+
function buildFailedLogMessage(status) {
|
|
555
|
+
const { blockingBugsCount, blockingBugUrls = [], nonBlockingBugsCount, nonBlockingBugUrls = [], relevantRunWithBugsUrl } = status;
|
|
556
|
+
const blockingBugList = blockingBugUrls.map((url) => `
|
|
545
557
|
\u2022 ${url}`).join("");
|
|
546
|
-
|
|
558
|
+
const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
|
|
547
559
|
\u2022 ${url}`).join("");
|
|
548
|
-
|
|
560
|
+
const bugsListInText = `${blockingBugUrls.length > 0 ? `
|
|
549
561
|
BLOCKING BUGS:${blockingBugList}` : ""}${nonBlockingBugUrls.length > 0 ? `
|
|
550
562
|
NON BLOCKING BUGS:${nonBlockingBugList}` : ""}`;
|
|
551
|
-
|
|
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)}` : ""}.
|
|
563
|
+
return `\u274C Run failed and ${blockingBugsCount} blocking bug${pluralize(blockingBugsCount)} found${nonBlockingBugsCount ? ` and ${nonBlockingBugsCount} non-blocking bug${pluralize(nonBlockingBugsCount)}` : ""}.
|
|
560
564
|
See ${relevantRunWithBugsUrl}
|
|
561
565
|
You may review the priority of certain bugs to mark these non-blocking and retry the job.
|
|
562
|
-
` + bugsListInText
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
566
|
+
` + bugsListInText;
|
|
567
|
+
}
|
|
568
|
+
__name(buildFailedLogMessage, "buildFailedLogMessage");
|
|
569
|
+
function buildSupersededAbortLogMessage(rootRunUrl, relevantRunUrl) {
|
|
570
|
+
const supersedingDetails = `
|
|
571
|
+
- Root: ${rootRunUrl}
|
|
572
|
+
- Superseding: ${relevantRunUrl}`;
|
|
573
|
+
return `\u274C Aborting due to superseded run (option 'abortOnSuperseded' is enabled).${supersedingDetails}`;
|
|
574
|
+
}
|
|
575
|
+
__name(buildSupersededAbortLogMessage, "buildSupersededAbortLogMessage");
|
|
576
|
+
function buildSupersededContinueLogMessage(rootRunUrl, relevantRunUrl) {
|
|
577
|
+
const supersedingDetails = `
|
|
578
|
+
- Root: ${rootRunUrl}
|
|
579
|
+
- Superseding: ${relevantRunUrl}`;
|
|
580
|
+
return `Root run was superseded:${supersedingDetails}`;
|
|
581
|
+
}
|
|
582
|
+
__name(buildSupersededContinueLogMessage, "buildSupersededContinueLogMessage");
|
|
583
|
+
function buildRunStageLogMessage(runStage, relevantRunUrl) {
|
|
584
|
+
const urlInfo = `See ${relevantRunUrl}`;
|
|
585
|
+
switch (runStage) {
|
|
586
|
+
case "canceled":
|
|
587
|
+
return `\u{1F6AB} Run was canceled. Aborting... Please contact support.
|
|
588
|
+
${urlInfo}`;
|
|
589
|
+
case "initializing":
|
|
590
|
+
return `\u23F3 Run is in progress... ${urlInfo}`;
|
|
591
|
+
case "underReview":
|
|
592
|
+
return `\u{1F575}\uFE0F Run is under review... ${urlInfo}`;
|
|
593
|
+
case "completed":
|
|
594
|
+
throw Error("buildRunStageLogMessage should not be called for completed stage");
|
|
595
|
+
default:
|
|
596
|
+
throw Error(`Unexpected run stage: ${runStage}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
__name(buildRunStageLogMessage, "buildRunStageLogMessage");
|
|
600
|
+
|
|
601
|
+
// src/lib/sdk/domain/pollCiGreenlight/mapIteratorAbortToGreenlightStatus.ts
|
|
602
|
+
function mapIteratorAbortToGreenlightStatus(iteration, pollTimeout, deps) {
|
|
603
|
+
const { log } = deps;
|
|
604
|
+
const { abortReason, httpStatus } = iteration;
|
|
605
|
+
switch (abortReason) {
|
|
606
|
+
case "4XX-client-error":
|
|
607
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${httpStatus}) from CI greenlight, aborting.`);
|
|
608
|
+
return {
|
|
609
|
+
abortReason,
|
|
610
|
+
httpStatus,
|
|
611
|
+
outcome: "aborted"
|
|
612
|
+
};
|
|
613
|
+
case "5XX-server-error":
|
|
614
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${httpStatus}) from CI greenlight, aborting.`);
|
|
615
|
+
return {
|
|
616
|
+
abortReason,
|
|
617
|
+
httpStatus,
|
|
618
|
+
outcome: "aborted"
|
|
619
|
+
};
|
|
620
|
+
case "404-run-not-found":
|
|
621
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${httpStatus}) from CI greenlight, aborting.`);
|
|
622
|
+
return {
|
|
623
|
+
abortReason,
|
|
624
|
+
httpStatus,
|
|
625
|
+
outcome: "aborted"
|
|
626
|
+
};
|
|
627
|
+
case "network-error":
|
|
628
|
+
log.error(`\u{1F6AB} Unrecoverable network error from CI greenlight, aborting.`);
|
|
629
|
+
return {
|
|
630
|
+
abortReason,
|
|
631
|
+
httpStatus: void 0,
|
|
632
|
+
outcome: "aborted"
|
|
633
|
+
};
|
|
634
|
+
case "poll-timed-out":
|
|
635
|
+
log.warn(`\u274C Poll timed out after ${pollTimeout / 1e3}s`);
|
|
572
636
|
return {
|
|
573
|
-
abortReason
|
|
574
|
-
httpStatus:
|
|
637
|
+
abortReason,
|
|
638
|
+
httpStatus: void 0,
|
|
575
639
|
outcome: "aborted"
|
|
576
640
|
};
|
|
641
|
+
case "run-canceled":
|
|
642
|
+
log.info(`Run was canceled`);
|
|
643
|
+
return {
|
|
644
|
+
abortReason,
|
|
645
|
+
httpStatus: void 0,
|
|
646
|
+
outcome: "aborted"
|
|
647
|
+
};
|
|
648
|
+
case "XXX-other-http-code":
|
|
649
|
+
log.error(`\u{1F6AB} Unrecoverable error (status ${httpStatus}) from CI greenlight, aborting.`);
|
|
650
|
+
return {
|
|
651
|
+
abortReason,
|
|
652
|
+
httpStatus,
|
|
653
|
+
outcome: "aborted"
|
|
654
|
+
};
|
|
655
|
+
default:
|
|
656
|
+
throw Error(`Unexpected abort reason: ${abortReason}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
__name(mapIteratorAbortToGreenlightStatus, "mapIteratorAbortToGreenlightStatus");
|
|
660
|
+
|
|
661
|
+
// src/lib/sdk/domain/pollCiGreenlight/index.ts
|
|
662
|
+
async function pollCiGreenlightStatus(deps, apiConfig, { onRunStageChanged = /* @__PURE__ */ __name(() => void 0, "onRunStageChanged"), runId, ...pollConfig }) {
|
|
663
|
+
const { log } = deps;
|
|
664
|
+
const { abortOnSuperseded = pollCiGreenlightDefaults.abortOnSuperseded, pollTimeout = pollCiGreenlightDefaults.pollTimeout } = pollConfig;
|
|
665
|
+
const iterator = makePollCiGreenlightStatusIterator(deps, apiConfig, {
|
|
666
|
+
runId,
|
|
667
|
+
...pollConfig
|
|
668
|
+
});
|
|
669
|
+
for await (const lastPoll of iterator) {
|
|
670
|
+
if (lastPoll.isAborted) return mapIteratorAbortToGreenlightStatus(lastPoll, pollTimeout, deps);
|
|
671
|
+
const { previousStatus, stageChanged, status } = lastPoll;
|
|
672
|
+
if (stageChanged) {
|
|
673
|
+
try {
|
|
674
|
+
await onRunStageChanged(status, previousStatus);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
log.error(`Error in onRunStageChanged callback`, e);
|
|
677
|
+
}
|
|
577
678
|
}
|
|
578
|
-
|
|
579
|
-
|
|
679
|
+
const { relevantRunId, relevantRunUrl, rootRunId, rootRunUrl, runStage } = status;
|
|
680
|
+
const isSuperseded = rootRunId !== relevantRunId;
|
|
681
|
+
const shouldAbortOnSuperseded = isSuperseded && abortOnSuperseded;
|
|
682
|
+
if (shouldAbortOnSuperseded) {
|
|
683
|
+
const message = buildSupersededAbortLogMessage(rootRunUrl, relevantRunUrl);
|
|
684
|
+
log.warn(message);
|
|
580
685
|
return {
|
|
581
|
-
abortReason: "
|
|
582
|
-
httpStatus:
|
|
686
|
+
abortReason: "superseded-run",
|
|
687
|
+
httpStatus: void 0,
|
|
583
688
|
outcome: "aborted"
|
|
584
689
|
};
|
|
585
690
|
}
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
691
|
+
if (isSuperseded) {
|
|
692
|
+
const message = buildSupersededContinueLogMessage(rootRunUrl, relevantRunUrl);
|
|
693
|
+
log.info(message);
|
|
694
|
+
}
|
|
695
|
+
switch (runStage) {
|
|
696
|
+
case "canceled":
|
|
697
|
+
case "initializing":
|
|
698
|
+
case "underReview": {
|
|
699
|
+
log.info(buildRunStageLogMessage(runStage, relevantRunUrl));
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "completed": {
|
|
703
|
+
const result = buildCompletedRunResult(status);
|
|
704
|
+
switch (result.outcome) {
|
|
705
|
+
case "aborted": {
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "failed": {
|
|
709
|
+
const message = buildFailedLogMessage(status);
|
|
710
|
+
log.warn(message);
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case "success": {
|
|
714
|
+
const message = buildSuccessLogMessage(status);
|
|
715
|
+
log.info(message);
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
default:
|
|
719
|
+
throw Error(`Unexpected outcome: ${result}`);
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
default:
|
|
724
|
+
throw Error(`Unexpected run stage: ${runStage}`);
|
|
725
|
+
}
|
|
593
726
|
}
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
abortReason: "poll-timed-out",
|
|
597
|
-
httpStatus: undefined,
|
|
598
|
-
outcome: "aborted"
|
|
599
|
-
};
|
|
727
|
+
throw Error("Iterator ended unexpectedly");
|
|
600
728
|
}
|
|
601
729
|
__name(pollCiGreenlightStatus, "pollCiGreenlightStatus");
|
|
602
730
|
|
|
@@ -622,6 +750,7 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
|
|
|
622
750
|
attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
|
|
623
751
|
generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(null, deps, apiConfig),
|
|
624
752
|
generateSignedUrlForTempTeamStorage: generateSignedUrlForTempTeamStorage.bind(null, deps, apiConfig),
|
|
753
|
+
makePollCiGreenlightStatusIterator: makePollCiGreenlightStatusIterator.bind(null, deps, apiConfig),
|
|
625
754
|
pollCiGreenlightStatus: pollCiGreenlightStatus.bind(null, deps, apiConfig)
|
|
626
755
|
};
|
|
627
756
|
}
|