@qawolf/ci-sdk 3.1.0 → 3.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
@@ -13,6 +13,7 @@ to determine the status of your CI/CD step/job/action.
13
13
 
14
14
  - [API Keys and Workspaces](#api-keys)
15
15
  - [Report a Deployment](#report-deployment)
16
+ - [Wait for a Verdict](#wait-for-verdict)
16
17
  - [Notify Deployment (deprecated)](#notify-deployment)
17
18
  - [Notify Preview Deployment (Pull Request / Merge Request Testing)](#notify-preview)
18
19
  - [Poll for CI Greenlight Status (deprecated)](#ci-greenlight)
@@ -249,7 +250,69 @@ The response carries the deployment only, not the resulting runs. Waiting for a
249
250
 
250
251
  `detectProviderDeploymentId()` composes the run, attempt and job, so two jobs in one workflow do not report into the same deployment. Compose the id yourself only if you would rather not call it, and include the job.
251
252
 
252
- Four things move: `deploymentType` becomes `environment.name`; `branch` / `sha` become `metadata`; `providerDeploymentId` is new and required (`deduplicationKey` is subsumed by it); `hostingService` disappears, since the code host comes from your integration. There is no `skipped` outcome: whether a trigger matched is not known at report time. `pollCiGreenlightStatus` takes a run id that only `attemptNotifyDeploy` returns, so a pipeline that waits for a verdict stays on the legacy pair until the successor to polling ships.
253
+ Four things move: `deploymentType` becomes `environment.name`; `branch` / `sha` become `metadata`; `providerDeploymentId` is new and required (`deduplicationKey` is subsumed by it); `hostingService` disappears, since the code host comes from your integration. There is no `skipped` outcome: whether a trigger matched is not known at report time. `pollCiGreenlightStatus` takes a run id that only `attemptNotifyDeploy` returns; a pipeline that waits for a verdict moves to [`deployment.waitForVerdict`](#wait-for-verdict), which takes the deployment id this call returns.
254
+
255
+ <a id="wait-for-verdict"></a>
256
+
257
+ ## Wait for a Verdict
258
+
259
+ `deployment.waitForVerdict` blocks until QA Wolf can say whether a deployment's tests passed. Give it the deployment id [the report](#report-deployment) returned; working out which runs the deployment produced happens inside.
260
+
261
+ > ⚠️ Set `timeout` below your CI job's own ceiling. When the job's ceiling fires first, the job is killed with no run link and no explanation. When ours fires first, you get `timed-out` with the stage, the elapsed time, and every run URL we know about.
262
+
263
+ ### Example
264
+
265
+ ```ts
266
+ const { deployment } = makeQaWolfSdk({ apiKey: "qawolf_xxxxx" });
267
+
268
+ const report = await deployment.reportStatus({ /* ... */ status: "success" });
269
+ if (report.outcome !== "reported") process.exit(1);
270
+
271
+ const verdict = await deployment.waitForVerdict({
272
+ deploymentId: report.deployment.id,
273
+ timeout: 45 * 60 * 1000,
274
+ });
275
+
276
+ process.exit(["passed", "not-tested"].includes(verdict.outcome) ? 0 : 1);
277
+ ```
278
+
279
+ ### Result
280
+
281
+ Like every function here, it never throws. Branch on `outcome` alone:
282
+
283
+ | `outcome` | Meaning |
284
+ | ----------------------- | -------------------------------------------------------------------------------------------------------- |
285
+ | `passed` | Every run QA Wolf started passed. Carries `runs`. |
286
+ | `failed` | A run found a bug that is still open and serious enough to block. Carries `runs` and `blockingBugCount`. |
287
+ | `not-tested` | Nothing tested this deployment. Carries `reason` and a `triggers` note per trigger saying why. |
288
+ | `run-canceled` | A run was canceled, so there is no verdict. This is not a pass. Carries `runs`. |
289
+ | `timed-out` | The budget ran out. Carries `elapsedMs`, `lastStage` and any `runs` it had already looked up. |
290
+ | `canceled-by-caller` | The `signal` you passed was aborted. |
291
+ | `unauthorized` | The API key is unknown, or may not read this deployment. Carries `message`. |
292
+ | `not-found` | No such deployment. Carries `message`. |
293
+ | `server-error` | QA Wolf kept failing after every retry. Carries `httpStatus`. |
294
+ | `unexpected-status` | A status this SDK version does not map. Carries `httpStatus` and `message`. |
295
+ | `network-error` | The connection never came back within the retries. |
296
+ | `invalid-response-body` | QA Wolf answered with a body the contract rejects. |
297
+
298
+ `not-tested` is a separate answer from `passed` on purpose. A deployment that no trigger matched was not tested, which is a different fact from tests passing, and you decide what it is worth: exit 0 and keep the log line, or fail the build when you expect a run every time. `run-canceled` likewise means we cannot tell you.
299
+
300
+ The shape is `deploymentVerdictSchema`, published in `@qawolf/api-contracts`, so the same answer parses the same way wherever you read it.
301
+
302
+ ### Options
303
+
304
+ Every field below is optional and carries the default shown.
305
+
306
+ | Option | Default | Meaning |
307
+ | ---------------------- | ------- | ------------------------------------------------------------------------------------- |
308
+ | `timeout` | `2h` | Whole-operation budget. Set it below your CI job's ceiling. |
309
+ | `runAppearanceTimeout` | `5min` | Give up if no trigger has produced a run by then. Runs that did appear are waited on. |
310
+ | `triggerPollInterval` | `3s` | Between polls while a trigger is still producing a run. |
311
+ | `runPollInterval` | `30s` | Between polls while a run executes. |
312
+ | `maxRetries` | `10` | Network and server-error retries before giving up. |
313
+ | `retryInterval` | `10s` | Between those retries. |
314
+ | `onProgress` | — | Called on every stage change, for your build log. |
315
+ | `signal` | — | An `AbortSignal`. Aborting it yields `canceled-by-caller` instead of a rejection. |
253
316
 
254
317
  <a id="notify-deployment"></a>
255
318
 
@@ -735,6 +798,10 @@ This package follows the [SemVer](https://semver.org/) versioning scheme. Additi
735
798
 
736
799
  # Changelog
737
800
 
801
+ ## v3.2.0
802
+
803
+ - New `deployment.waitForVerdict` function, which blocks until QA Wolf can say whether a deployment's tests passed. It takes the deployment id `deployment.reportStatus` returned and resolves that to runs internally, so no run id crosses the boundary. It answers `passed`, `failed`, `not-tested`, `run-canceled`, `timed-out`, `canceled-by-caller` or one of the transport outcomes, and never throws. `not-tested` is deliberately distinct from `passed`, and `run-canceled` means no verdict rather than a pass. The whole-operation budget stays at 2 hours, matching `pollCiGreenlightStatus`. See [Wait for a Verdict](#wait-for-verdict).
804
+
738
805
  ## v3.1.0
739
806
 
740
807
  - New `deployment.reportStatus` function, which reports a deployment through the public `deployment.reportStatus` API — the one your triggers watch. A deployment's first `success` report is what starts a run. Its parameters are the API's own input, taken from the `@qawolf/api-contracts` package, so the SDK cannot drift from the API, and it returns `{ outcome: "reported", deployment }` carrying the deployment's `id`, `status` and `url`. A refusal comes back as one named outcome you can branch on — `invalid-request`, `unauthorized`, `not-found`, `conflict`, `server-error`, `unexpected-status`, `network-error` or `invalid-response-body` — so `if (result.outcome !== "reported")` is the whole check a CI gate needs. See [Report a Deployment](#report-deployment).
package/dist/index.js CHANGED
@@ -98,6 +98,9 @@ var buildFetchWithTimeout = /* @__PURE__ */ __name((fetch, defaultTimeoutMs) =>
98
98
  return fetch(args[0], { ...args[1], signal });
99
99
  }, "buildFetchWithTimeout");
100
100
 
101
+ // src/lib/sdk/defaults/clock.ts
102
+ var defaultClock = { now: /* @__PURE__ */ __name(() => Date.now(), "now"), sleep };
103
+
101
104
  // src/lib/sdk/defaults/fetch.ts
102
105
  var defaultFetch = globalThis.fetch;
103
106
 
@@ -120,7 +123,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
120
123
  // package.json
121
124
  var package_default = {
122
125
  name: "@qawolf/ci-sdk",
123
- version: "3.1.0",
126
+ version: "3.2.0",
124
127
  private: false,
125
128
  description: "A simple SDK for interacting with QAWolf in CI scripts.",
126
129
  keywords: [],
@@ -149,7 +152,7 @@ var package_default = {
149
152
  "tsc:check": "tsc --noEmit"
150
153
  },
151
154
  dependencies: {
152
- "@qawolf/api-contracts": ">=0.68.0",
155
+ "@qawolf/api-contracts": ">=0.71.0",
153
156
  "@qawolf/ci-utils": "^2.0.0",
154
157
  "@sinonjs/fake-timers": "^10.3.0",
155
158
  tslib: "^2.6.2",
@@ -1273,7 +1276,7 @@ async function callPublicContract({ fetch: localFetch, log }, apiConfig, {
1273
1276
  if (!response.ok) return readHttpError(response, contract.name, log);
1274
1277
  bodyText = await response.text();
1275
1278
  } catch {
1276
- log.error(`\u{1F6AB} Network error from ${contract.name}, aborting.`);
1279
+ log.error(`\u{1F6AB} Network error from ${contract.name}.`);
1277
1280
  return { outcome: "network-error" };
1278
1281
  }
1279
1282
  const parsed = contract.output.safeParse(
@@ -1316,12 +1319,430 @@ function mapFailure(result) {
1316
1319
  }
1317
1320
  __name(mapFailure, "mapFailure");
1318
1321
 
1322
+ // src/lib/sdk/domain/waitForVerdict/awaitRuns.ts
1323
+ import { publicContractsV1 as publicContractsV12 } from "@qawolf/api-contracts";
1324
+
1325
+ // src/lib/sdk/domain/waitForVerdict/failures.ts
1326
+ function handleCallFailure(failure, deploymentId) {
1327
+ if (failure.outcome === "network-error") {
1328
+ return {
1329
+ exhaustedVerdict: { deploymentId, outcome: "network-error" },
1330
+ kind: "retry"
1331
+ };
1332
+ }
1333
+ if (failure.outcome === "invalid-response-body") {
1334
+ return {
1335
+ kind: "stop",
1336
+ verdict: { deploymentId, outcome: "invalid-response-body" }
1337
+ };
1338
+ }
1339
+ const { httpStatus, message } = failure;
1340
+ if (httpStatus >= 500) {
1341
+ return {
1342
+ exhaustedVerdict: { deploymentId, httpStatus, outcome: "server-error" },
1343
+ kind: "retry"
1344
+ };
1345
+ }
1346
+ if (httpStatus === 401 || httpStatus === 403) {
1347
+ return {
1348
+ kind: "stop",
1349
+ verdict: { deploymentId, message, outcome: "unauthorized" }
1350
+ };
1351
+ }
1352
+ if (httpStatus === 404) {
1353
+ return {
1354
+ kind: "stop",
1355
+ verdict: { deploymentId, message, outcome: "not-found" }
1356
+ };
1357
+ }
1358
+ return {
1359
+ kind: "stop",
1360
+ verdict: {
1361
+ deploymentId,
1362
+ httpStatus,
1363
+ message,
1364
+ outcome: "unexpected-status"
1365
+ }
1366
+ };
1367
+ }
1368
+ __name(handleCallFailure, "handleCallFailure");
1369
+
1370
+ // src/lib/sdk/domain/waitForVerdict/poll.ts
1371
+ async function pollUntil(ctx, {
1372
+ deadlineAt,
1373
+ onData,
1374
+ onDeadline,
1375
+ request
1376
+ }) {
1377
+ const { clock, log } = ctx.deps;
1378
+ const { deploymentId } = ctx;
1379
+ const { maxRetries, retryInterval, signal } = ctx.config;
1380
+ let retries = 0;
1381
+ const sleepWithinBudget = /* @__PURE__ */ __name((intervalMs) => clock.sleep(Math.max(0, Math.min(intervalMs, deadlineAt - clock.now()))), "sleepWithinBudget");
1382
+ for (; ; ) {
1383
+ if (signal?.aborted)
1384
+ return { deploymentId, outcome: "canceled-by-caller" };
1385
+ if (clock.now() >= deadlineAt) return onDeadline();
1386
+ const result = await request();
1387
+ if (result.outcome === "ok") {
1388
+ retries = 0;
1389
+ const step = await onData(result.data);
1390
+ if (step.kind === "done") return step.result;
1391
+ await sleepWithinBudget(step.intervalMs);
1392
+ continue;
1393
+ }
1394
+ const handling = handleCallFailure(result, deploymentId);
1395
+ if (handling.kind === "stop") return handling.verdict;
1396
+ retries += 1;
1397
+ if (retries > maxRetries) return handling.exhaustedVerdict;
1398
+ log.warn(
1399
+ `\u23F3 QA Wolf did not answer, trying again... (${retries}/${maxRetries})`
1400
+ );
1401
+ await sleepWithinBudget(retryInterval);
1402
+ }
1403
+ }
1404
+ __name(pollUntil, "pollUntil");
1405
+
1406
+ // src/lib/sdk/domain/waitForVerdict/progress.ts
1407
+ function describeStage(stage) {
1408
+ switch (stage.stage) {
1409
+ case "waiting-for-evaluation":
1410
+ return "\u23F3 Waiting for QA Wolf to evaluate this deployment's triggers...";
1411
+ case "waiting-for-run": {
1412
+ const { matchedTriggerCount } = stage;
1413
+ return `\u23F3 ${matchedTriggerCount} trigger${pluralize(matchedTriggerCount)} matched; waiting for a run to start...`;
1414
+ }
1415
+ case "waiting-for-verdict": {
1416
+ const urls = stage.runs.map((run) => run.runUrl).join(" ");
1417
+ return `\u23F3 ${stage.runs.length} run${pluralize(stage.runs.length)} executing... ${urls}`;
1418
+ }
1419
+ default:
1420
+ throw Error(`Unexpected stage: ${stage}`);
1421
+ }
1422
+ }
1423
+ __name(describeStage, "describeStage");
1424
+ async function reportStage(ctx, previousDescription, stage) {
1425
+ const description = describeStage(stage);
1426
+ if (description === previousDescription) return description;
1427
+ ctx.deps.log.info(description);
1428
+ try {
1429
+ await ctx.config.onProgress?.(stage);
1430
+ } catch (error) {
1431
+ ctx.deps.log.error("The onProgress callback failed.", error);
1432
+ }
1433
+ return description;
1434
+ }
1435
+ __name(reportStage, "reportStage");
1436
+
1437
+ // src/lib/sdk/domain/waitForVerdict/readEvaluations.ts
1438
+ var notConsideredMessages = {
1439
+ "not-a-deployment-trigger": "The trigger has no deployment conditions, so no deployment can fire it.",
1440
+ "trigger-paused": "The trigger was paused when the deployment was evaluated."
1441
+ };
1442
+ function readDidNotMatch(conditionSets) {
1443
+ const reasons = conditionSets.flatMap(
1444
+ (set) => set.conditions.flatMap(
1445
+ (condition) => condition.outcome === "did-not-match" ? [condition.reason] : []
1446
+ )
1447
+ );
1448
+ return reasons.length > 0 ? reasons.join(" ") : "The trigger's conditions did not match this deployment.";
1449
+ }
1450
+ __name(readDidNotMatch, "readDidNotMatch");
1451
+ function toTriggerNote(evaluation) {
1452
+ const { triggerName, verdict } = evaluation;
1453
+ if (verdict.outcome === "did-not-match")
1454
+ return { message: readDidNotMatch(verdict.conditionSets), triggerName };
1455
+ if (verdict.outcome === "not-considered") {
1456
+ return {
1457
+ message: notConsideredMessages[verdict.reason],
1458
+ triggerName
1459
+ };
1460
+ }
1461
+ if (verdict.run.status === "did-not-run") {
1462
+ return {
1463
+ message: verdict.run.message,
1464
+ reason: verdict.run.reason,
1465
+ triggerName
1466
+ };
1467
+ }
1468
+ return void 0;
1469
+ }
1470
+ __name(toTriggerNote, "toTriggerNote");
1471
+ function toMatchedRun(evaluation) {
1472
+ const { triggerName, verdict } = evaluation;
1473
+ if (verdict.outcome !== "matched") return void 0;
1474
+ if (verdict.run.status === "ran")
1475
+ return { runId: verdict.run.runId, triggerName };
1476
+ if (verdict.run.status === "did-not-run" && verdict.run.supersededByRunId)
1477
+ return { runId: verdict.run.supersededByRunId, triggerName };
1478
+ return void 0;
1479
+ }
1480
+ __name(toMatchedRun, "toMatchedRun");
1481
+ function isStillProducingARun(evaluation) {
1482
+ const { verdict } = evaluation;
1483
+ return verdict.outcome === "matched" && (verdict.run.status === "starting" || verdict.run.status === "unknown");
1484
+ }
1485
+ __name(isStillProducingARun, "isStillProducingARun");
1486
+ function readEvaluations(evaluations) {
1487
+ if (evaluations.state === "not-evaluated") return { kind: "not-evaluated" };
1488
+ const runs = evaluations.evaluations.flatMap((evaluation) => {
1489
+ const run = toMatchedRun(evaluation);
1490
+ return run ? [run] : [];
1491
+ });
1492
+ const matched = evaluations.evaluations.filter(
1493
+ (evaluation) => evaluation.verdict.outcome === "matched"
1494
+ );
1495
+ if (evaluations.evaluations.some(isStillProducingARun))
1496
+ return { kind: "awaiting-run", matchedTriggerCount: matched.length, runs };
1497
+ if (runs.length > 0) return { kind: "runs", runs };
1498
+ return {
1499
+ kind: "not-tested",
1500
+ reason: matched.length > 0 ? "no-run-created" : "no-trigger-matched",
1501
+ triggers: evaluations.evaluations.flatMap((evaluation) => {
1502
+ const note = toTriggerNote(evaluation);
1503
+ return note ? [note] : [];
1504
+ })
1505
+ };
1506
+ }
1507
+ __name(readEvaluations, "readEvaluations");
1508
+
1509
+ // src/lib/sdk/domain/waitForVerdict/awaitRuns.ts
1510
+ async function awaitRuns(ctx) {
1511
+ const { clock } = ctx.deps;
1512
+ const { deploymentId } = ctx;
1513
+ const { runAppearanceTimeout, timeout, triggerPollInterval } = ctx.config;
1514
+ const overallDeadlineAt = ctx.startedAt + timeout;
1515
+ let knownRuns = [];
1516
+ let lastStage = { stage: "waiting-for-evaluation" };
1517
+ let lastDescription = void 0;
1518
+ return pollUntil(ctx, {
1519
+ deadlineAt: Math.min(
1520
+ overallDeadlineAt,
1521
+ ctx.startedAt + runAppearanceTimeout
1522
+ ),
1523
+ async onData(evaluations) {
1524
+ const reading = readEvaluations(evaluations);
1525
+ if (reading.kind === "runs")
1526
+ return { kind: "done", result: { kind: "runs", runs: reading.runs } };
1527
+ if (reading.kind === "not-tested") {
1528
+ return {
1529
+ kind: "done",
1530
+ result: {
1531
+ deploymentId,
1532
+ outcome: "not-tested",
1533
+ reason: reading.reason,
1534
+ triggers: reading.triggers
1535
+ }
1536
+ };
1537
+ }
1538
+ knownRuns = reading.kind === "awaiting-run" ? reading.runs : knownRuns;
1539
+ lastStage = reading.kind === "awaiting-run" ? {
1540
+ matchedTriggerCount: reading.matchedTriggerCount,
1541
+ stage: "waiting-for-run"
1542
+ } : { stage: "waiting-for-evaluation" };
1543
+ lastDescription = await reportStage(ctx, lastDescription, lastStage);
1544
+ return { intervalMs: triggerPollInterval, kind: "continue" };
1545
+ },
1546
+ onDeadline() {
1547
+ const now = clock.now();
1548
+ if (knownRuns.length > 0 && now < overallDeadlineAt)
1549
+ return { kind: "runs", runs: knownRuns };
1550
+ return {
1551
+ deploymentId,
1552
+ elapsedMs: now - ctx.startedAt,
1553
+ lastStage,
1554
+ outcome: "timed-out",
1555
+ runs: []
1556
+ };
1557
+ },
1558
+ request: /* @__PURE__ */ __name(() => callPublicContract(ctx.deps, ctx.apiConfig, {
1559
+ contract: publicContractsV12.deployment.listTriggerEvaluations,
1560
+ input: { deploymentId }
1561
+ }), "request")
1562
+ });
1563
+ }
1564
+ __name(awaitRuns, "awaitRuns");
1565
+
1566
+ // src/lib/sdk/domain/waitForVerdict/awaitVerdict.ts
1567
+ import { publicContractsV1 as publicContractsV13 } from "@qawolf/api-contracts";
1568
+ var settledStatuses = ["passed", "failed", "canceled"];
1569
+ function isSettled(run) {
1570
+ return settledStatuses.includes(run.status);
1571
+ }
1572
+ __name(isSettled, "isSettled");
1573
+ function aggregateRuns(deploymentId, runs) {
1574
+ if (runs.some((run) => run.status === "failed")) {
1575
+ return {
1576
+ blockingBugCount: runs.reduce(
1577
+ (total, run) => total + run.blockingBugCount,
1578
+ 0
1579
+ ),
1580
+ deploymentId,
1581
+ outcome: "failed",
1582
+ runs
1583
+ };
1584
+ }
1585
+ if (runs.some((run) => run.status === "canceled"))
1586
+ return { deploymentId, outcome: "run-canceled", runs };
1587
+ return { deploymentId, outcome: "passed", runs };
1588
+ }
1589
+ __name(aggregateRuns, "aggregateRuns");
1590
+ function keepOnePerRun(runs) {
1591
+ return runs.filter(
1592
+ (run, index) => runs.findIndex((other) => other.runId === run.runId) === index
1593
+ );
1594
+ }
1595
+ __name(keepOnePerRun, "keepOnePerRun");
1596
+ async function requestRuns(ctx, matched) {
1597
+ const answers = await Promise.all(
1598
+ matched.map(async (run) => ({
1599
+ result: await callPublicContract(ctx.deps, ctx.apiConfig, {
1600
+ contract: publicContractsV13.run.get,
1601
+ // A suite can hold hundreds of flows and the verdict reads none of
1602
+ // them, so ask for the smallest list the contract allows.
1603
+ input: { flowStatuses: ["failed"], runId: run.runId }
1604
+ }),
1605
+ triggerName: run.triggerName
1606
+ }))
1607
+ );
1608
+ const failed = answers.find((answer) => answer.result.outcome !== "ok");
1609
+ if (failed && failed.result.outcome !== "ok") return failed.result;
1610
+ return {
1611
+ data: keepOnePerRun(
1612
+ answers.flatMap(
1613
+ (answer) => answer.result.outcome === "ok" ? [
1614
+ {
1615
+ blockingBugCount: answer.result.data.blockingBugCount,
1616
+ // The canonical id, which can name a run other than the one asked for.
1617
+ runId: answer.result.data.runId,
1618
+ runUrl: answer.result.data.url,
1619
+ status: answer.result.data.status,
1620
+ triggerName: answer.triggerName
1621
+ }
1622
+ ] : []
1623
+ )
1624
+ ),
1625
+ outcome: "ok"
1626
+ };
1627
+ }
1628
+ __name(requestRuns, "requestRuns");
1629
+ async function awaitVerdict(ctx, matched) {
1630
+ const { deploymentId } = ctx;
1631
+ let lastRuns = [];
1632
+ let lastDescription = void 0;
1633
+ return pollUntil(ctx, {
1634
+ deadlineAt: ctx.startedAt + ctx.config.timeout,
1635
+ async onData(runs) {
1636
+ lastRuns = runs;
1637
+ if (runs.every(isSettled))
1638
+ return { kind: "done", result: aggregateRuns(deploymentId, runs) };
1639
+ lastDescription = await reportStage(ctx, lastDescription, {
1640
+ runs,
1641
+ stage: "waiting-for-verdict"
1642
+ });
1643
+ return { intervalMs: ctx.config.runPollInterval, kind: "continue" };
1644
+ },
1645
+ onDeadline: /* @__PURE__ */ __name(() => ({
1646
+ deploymentId,
1647
+ elapsedMs: ctx.deps.clock.now() - ctx.startedAt,
1648
+ lastStage: { runs: lastRuns, stage: "waiting-for-verdict" },
1649
+ outcome: "timed-out",
1650
+ runs: lastRuns
1651
+ }), "onDeadline"),
1652
+ request: /* @__PURE__ */ __name(() => requestRuns(ctx, matched), "request")
1653
+ });
1654
+ }
1655
+ __name(awaitVerdict, "awaitVerdict");
1656
+
1657
+ // src/lib/sdk/domain/waitForVerdict/consts.ts
1658
+ var waitForVerdictDefaults = {
1659
+ maxRetries: 10,
1660
+ retryInterval: 10 * 1e3,
1661
+ runAppearanceTimeout: 5 * 60 * 1e3,
1662
+ runPollInterval: 30 * 1e3,
1663
+ timeout: 120 * 60 * 1e3,
1664
+ triggerPollInterval: 3 * 1e3
1665
+ };
1666
+
1667
+ // src/lib/sdk/domain/waitForVerdict/logVerdict.ts
1668
+ function runUrls(runs) {
1669
+ return runs.map((run) => run.runUrl).join(" ");
1670
+ }
1671
+ __name(runUrls, "runUrls");
1672
+ function logVerdict(log, verdict) {
1673
+ switch (verdict.outcome) {
1674
+ case "canceled-by-caller":
1675
+ return log.warn("\u{1F6AB} The wait was canceled before QA Wolf answered.");
1676
+ case "failed":
1677
+ return log.error(
1678
+ `\u274C QA Wolf found ${verdict.blockingBugCount} blocking bug${pluralize(verdict.blockingBugCount)}. ${runUrls(verdict.runs)}`
1679
+ );
1680
+ case "not-tested": {
1681
+ log.info(
1682
+ verdict.reason === "no-trigger-matched" ? "\u26A0\uFE0F QA Wolf: nothing to test for this deployment." : "\u26A0\uFE0F QA Wolf: a trigger matched this deployment but started no run."
1683
+ );
1684
+ for (const trigger of verdict.triggers)
1685
+ log.info(` ${trigger.triggerName}: ${trigger.message}`);
1686
+ return;
1687
+ }
1688
+ case "passed":
1689
+ return log.info(`\u2705 QA Wolf gate: passed. ${runUrls(verdict.runs)}`);
1690
+ case "run-canceled":
1691
+ return log.error(
1692
+ `\u{1F6AB} The run was canceled, so QA Wolf reached no verdict. ${runUrls(verdict.runs)}`
1693
+ );
1694
+ case "timed-out":
1695
+ return log.error(
1696
+ `\u{1F6AB} Gave up after ${Math.round(verdict.elapsedMs / 1e3)}s waiting for QA Wolf. ${runUrls(verdict.runs)}`
1697
+ );
1698
+ default:
1699
+ return void 0;
1700
+ }
1701
+ }
1702
+ __name(logVerdict, "logVerdict");
1703
+
1704
+ // src/lib/sdk/domain/waitForVerdict/index.ts
1705
+ async function waitForVerdict(deps, apiConfig, {
1706
+ deploymentId,
1707
+ maxRetries = waitForVerdictDefaults.maxRetries,
1708
+ onProgress,
1709
+ retryInterval = waitForVerdictDefaults.retryInterval,
1710
+ runAppearanceTimeout = waitForVerdictDefaults.runAppearanceTimeout,
1711
+ runPollInterval = waitForVerdictDefaults.runPollInterval,
1712
+ signal,
1713
+ timeout = waitForVerdictDefaults.timeout,
1714
+ triggerPollInterval = waitForVerdictDefaults.triggerPollInterval
1715
+ }) {
1716
+ const ctx = {
1717
+ apiConfig,
1718
+ config: {
1719
+ maxRetries,
1720
+ onProgress,
1721
+ retryInterval,
1722
+ runAppearanceTimeout,
1723
+ runPollInterval,
1724
+ signal,
1725
+ timeout,
1726
+ triggerPollInterval
1727
+ },
1728
+ deploymentId,
1729
+ deps,
1730
+ startedAt: deps.clock.now()
1731
+ };
1732
+ const phase = await awaitRuns(ctx);
1733
+ const verdict = "kind" in phase ? await awaitVerdict(ctx, phase.runs) : phase;
1734
+ logVerdict(deps.log, verdict);
1735
+ return verdict;
1736
+ }
1737
+ __name(waitForVerdict, "waitForVerdict");
1738
+
1319
1739
  // src/lib/sdk/index.ts
1320
1740
  function makeQaWolfSdk({
1321
1741
  apiKey,
1322
1742
  serviceBase = defaultServiceBase,
1323
1743
  userAgent = defaultUserAgent
1324
1744
  }, {
1745
+ clock = defaultClock,
1325
1746
  fetch = defaultFetch,
1326
1747
  log = defaultLogDriver
1327
1748
  } = {}) {
@@ -1332,6 +1753,7 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1332
1753
  );
1333
1754
  }
1334
1755
  const deps = {
1756
+ clock,
1335
1757
  // Versions of fetch may have different default timeouts. For consistency, we explicitly set it to 2 minutes here.
1336
1758
  // The increased timeout helps avoid edge cases where we need to search for a commit against multiple
1337
1759
  // code hosting service repositories as a fallback when not finding the repository based on organization/name.
@@ -1346,7 +1768,8 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1346
1768
  */
1347
1769
  attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
1348
1770
  deployment: {
1349
- reportStatus: reportDeployment.bind(null, deps, apiConfig)
1771
+ reportStatus: reportDeployment.bind(null, deps, apiConfig),
1772
+ waitForVerdict: waitForVerdict.bind(null, deps, apiConfig)
1350
1773
  },
1351
1774
  generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(
1352
1775
  null,