@qawolf/ci-sdk 1.1.1 → 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,7 +38,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
38
38
 
39
39
  // package.json
40
40
  var package_default = {
41
- version: "1.1.1"};
41
+ version: "1.2.0"};
42
42
 
43
43
  // src/lib/sdk/defaults/userAgent.ts
44
44
  var defaultUserAgent = `ci-sdk/${package_default.version}`;
@@ -358,6 +358,29 @@ async function generateSignedUrlForTempTeamStorage(deps, apiConfig, config) {
358
358
  }
359
359
  __name(generateSignedUrlForTempTeamStorage, "generateSignedUrlForTempTeamStorage");
360
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
+
361
384
  // src/lib/api/ci-greenlight.ts
362
385
  async function fetchCiGreenlightStatus({ apiKey, serviceBase, userAgent }, { runId }, { fetch: localFetch, log }) {
363
386
  try {
@@ -413,56 +436,61 @@ async function fetchCiGreenlightStatus({ apiKey, serviceBase, userAgent }, { run
413
436
  }
414
437
  __name(fetchCiGreenlightStatus, "fetchCiGreenlightStatus");
415
438
 
416
- // src/lib/sdk/domain/poll-ci-greenlight.ts
417
- var safeDefaults = {
418
- abortOnSuperseded: false,
419
- maxRetries: 10,
420
- pollTimeout: 120 * 60 * 1e3,
421
- retryInterval: 10 * 1e3,
422
- runInProgressInterval: 30 * 1e3,
423
- runUnderReviewInterval: 60 * 1e3
424
- };
425
- async function pollCiGreenlightStatus(deps, apiConfig, { onRunStageChanged = /* @__PURE__ */ __name(() => void 0, "onRunStageChanged"), runId, ...pollConfig }) {
426
- let retries = 0;
439
+ // src/lib/sdk/domain/pollCiGreenlight/iterator.ts
440
+ async function* makePollCiGreenlightStatusIterator(deps, apiConfig, { runId, ...pollConfig }) {
427
441
  const { log } = deps;
428
- const { abortOnSuperseded = safeDefaults.abortOnSuperseded, maxRetries = safeDefaults.maxRetries, pollTimeout = safeDefaults.pollTimeout, retryInterval = safeDefaults.retryInterval, runInProgressInterval = safeDefaults.runInProgressInterval, runUnderReviewInterval = safeDefaults.runUnderReviewInterval } = pollConfig;
429
- let currentStatus = void 0;
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;
430
445
  const startedAt = Date.now();
431
- let hasTimedOut = false;
432
- while (!(hasTimedOut = Date.now() - startedAt > pollTimeout)) {
446
+ while (Date.now() - startedAt <= pollTimeout) {
433
447
  const responseState = await fetchCiGreenlightStatus(apiConfig, {
434
448
  runId
435
449
  }, deps);
450
+ const elapsedMs = Date.now() - startedAt;
436
451
  if (!responseState.ok && !responseState.canRetry) {
437
- log.error(`\u{1F6AB} Unrecoverable error (status ${responseState.httpStatus}) from CI greenlight, aborting.`);
438
- return {
452
+ yield {
439
453
  abortReason: "4XX-client-error",
454
+ elapsedMs,
440
455
  httpStatus: responseState.httpStatus,
441
- outcome: "aborted"
456
+ isAborted: true
442
457
  };
458
+ return;
443
459
  }
444
460
  if (!responseState.ok) {
445
461
  retries += 1;
462
+ const { errorType, httpStatus } = responseState;
446
463
  if (retries > maxRetries) {
447
- log.error(`\u{1F6AB} Unrecoverable error${responseState.httpStatus ? ` (status ${responseState.httpStatus})` : ""} from CI greenlight, aborting after ${maxRetries} attempts.`);
448
- if (responseState.errorType === "network") {
449
- return {
450
- abortReason: "network-error",
451
- httpStatus: void 0,
452
- outcome: "aborted"
453
- };
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}`);
454
491
  }
455
- let abortReason = "XXX-other-http-code";
456
- if (responseState.errorType === "not-found") abortReason = "404-run-not-found";
457
- else if (responseState.errorType === "server") abortReason = "5XX-server-error";
458
- else ;
459
- return {
460
- abortReason,
461
- httpStatus: responseState.httpStatus,
462
- outcome: "aborted"
463
- };
492
+ return;
464
493
  }
465
- const { errorType, httpStatus } = responseState;
466
494
  const retryLabel = `${httpStatus ? ` (status ${httpStatus})` : ""}, will try again... (${retries}/${maxRetries})`;
467
495
  if (errorType === "network") log.warn(`\u274C Network error${retryLabel}`);
468
496
  else if (errorType === "not-found") log.info(`\u23F3 Run not found${retryLabel}`);
@@ -472,89 +500,231 @@ async function pollCiGreenlightStatus(deps, apiConfig, { onRunStageChanged = /*
472
500
  }
473
501
  retries = 0;
474
502
  const { greenlightStatus } = responseState;
475
- const previousStatus = currentStatus;
476
- currentStatus = greenlightStatus;
477
- if (!previousStatus || previousStatus.runStage !== greenlightStatus.runStage) {
478
- try {
479
- await onRunStageChanged(greenlightStatus, previousStatus);
480
- } catch (e) {
481
- log.error(`Error in onRunStageChanged callback`, e);
482
- }
483
- }
484
- const urlInfo = `See ${greenlightStatus.relevantRunUrl}`;
485
- const { greenlight, relevantRunId, relevantRunUrl, relevantRunWithBugsUrl, rootRunId, rootRunUrl, runStage } = greenlightStatus;
486
- if (rootRunId !== relevantRunId) {
487
- const supersedingDetails = `
488
- - Root: ${rootRunUrl}
489
- - Superseding: ${relevantRunUrl}`;
490
- if (abortOnSuperseded) {
491
- log.warn(`\u274C Aborting due to superseded run (option 'abortOnSuperseded' is enabled).${supersedingDetails}`);
492
- return {
493
- abortReason: "superseded-run",
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,
494
519
  httpStatus: void 0,
495
- outcome: "aborted"
520
+ isAborted: true
496
521
  };
522
+ return;
497
523
  }
498
- 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}`);
499
534
  }
500
- if (runStage === "completed") {
501
- const { blockingBugsCount, blockingBugUrls = [], nonBlockingBugsCount, nonBlockingBugUrls = [] } = greenlightStatus;
502
- 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) => `
503
557
  \u2022 ${url}`).join("");
504
- const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
558
+ const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
505
559
  \u2022 ${url}`).join("");
506
- const bugsListInText = `${blockingBugUrls.length > 0 ? `
560
+ const bugsListInText = `${blockingBugUrls.length > 0 ? `
507
561
  BLOCKING BUGS:${blockingBugList}` : ""}${nonBlockingBugUrls.length > 0 ? `
508
562
  NON BLOCKING BUGS:${nonBlockingBugList}` : ""}`;
509
- if (greenlight) {
510
- log.info(`\u2705 Run passed and no blocking bugs found${nonBlockingBugsCount > 0 ? ` and ${nonBlockingBugsCount} non-blocking bug${pluralize(nonBlockingBugsCount)} found` : ""}.
511
- ${urlInfo}`);
512
- return {
513
- greenlightStatus,
514
- outcome: "success"
515
- };
516
- } else {
517
- 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)}` : ""}.
518
564
  See ${relevantRunWithBugsUrl}
519
565
  You may review the priority of certain bugs to mark these non-blocking and retry the job.
520
- ` + bugsListInText);
521
- return {
522
- greenlightStatus,
523
- outcome: "failed"
524
- };
525
- }
526
- }
527
- if (runStage === "canceled") {
528
- log.info(`\u{1F6AB} Run was canceled. Aborting... Please contact support.
529
- ${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`);
636
+ return {
637
+ abortReason,
638
+ httpStatus: void 0,
639
+ outcome: "aborted"
640
+ };
641
+ case "run-canceled":
642
+ log.info(`Run was canceled`);
530
643
  return {
531
- abortReason: "run-canceled",
644
+ abortReason,
532
645
  httpStatus: void 0,
533
646
  outcome: "aborted"
534
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
+ }
535
678
  }
536
- if (hasTimedOut) {
537
- 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);
538
685
  return {
539
- abortReason: "poll-timed-out",
686
+ abortReason: "superseded-run",
540
687
  httpStatus: void 0,
541
688
  outcome: "aborted"
542
689
  };
543
690
  }
544
- if (runStage === "initializing") {
545
- log.info(`\u23F3 Run is in progress... ${urlInfo}`);
546
- await sleep(runInProgressInterval);
547
- } else if (runStage === "underReview") {
548
- log.info(`\u{1F575}\uFE0F Run is under review... ${urlInfo}`);
549
- await sleep(runUnderReviewInterval);
550
- } 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
+ }
551
726
  }
552
- log.warn(`\u274C Poll timed out after ${pollTimeout / 1e3}s. Aborting...`);
553
- return {
554
- abortReason: "poll-timed-out",
555
- httpStatus: void 0,
556
- outcome: "aborted"
557
- };
727
+ throw Error("Iterator ended unexpectedly");
558
728
  }
559
729
  __name(pollCiGreenlightStatus, "pollCiGreenlightStatus");
560
730
 
@@ -580,6 +750,7 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
580
750
  attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
581
751
  generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(null, deps, apiConfig),
582
752
  generateSignedUrlForTempTeamStorage: generateSignedUrlForTempTeamStorage.bind(null, deps, apiConfig),
753
+ makePollCiGreenlightStatusIterator: makePollCiGreenlightStatusIterator.bind(null, deps, apiConfig),
583
754
  pollCiGreenlightStatus: pollCiGreenlightStatus.bind(null, deps, apiConfig)
584
755
  };
585
756
  }