@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 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
- 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
- };
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: undefined,
107
- failureMessage: undefined
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: undefined,
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 === undefined) abortReason = "network-error";
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: undefined
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/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;
439
+ // src/lib/sdk/domain/pollCiGreenlight/iterator.ts
440
+ async function* makePollCiGreenlightStatusIterator(deps, apiConfig, { runId, ...pollConfig }) {
469
441
  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;
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
- let hasTimedOut = false;
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
- log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) from CI greenlight, aborting.`);
480
- return {
452
+ yield {
481
453
  abortReason: "4XX-client-error",
454
+ elapsedMs,
482
455
  httpStatus: responseState.httpStatus,
483
- outcome: "aborted"
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
- 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
- };
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
- 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
- };
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 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"
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
- log.info(`Root run was superseded:${supersedingDetails}`);
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
- if (runStage === "completed") {
543
- const { blockingBugsCount, blockingBugUrls = [], nonBlockingBugsCount, nonBlockingBugUrls = [] } = greenlightStatus;
544
- const blockingBugList = blockingBugUrls.map((url) => `
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
- const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
558
+ const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
547
559
  \u2022 ${url}`).join("");
548
- const bugsListInText = `${blockingBugUrls.length > 0 ? `
560
+ const bugsListInText = `${blockingBugUrls.length > 0 ? `
549
561
  BLOCKING BUGS:${blockingBugList}` : ""}${nonBlockingBugUrls.length > 0 ? `
550
562
  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)}` : ""}.
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
- 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}`);
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: "run-canceled",
574
- httpStatus: undefined,
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
- if (hasTimedOut) {
579
- log.warn(`\u274C Poll timed out after ${pollTimeout / 1e3}s. Aborting...`);
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: "poll-timed-out",
582
- httpStatus: undefined,
686
+ abortReason: "superseded-run",
687
+ httpStatus: void 0,
583
688
  outcome: "aborted"
584
689
  };
585
690
  }
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}`);
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
- 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
- };
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
  }