@qawolf/ci-sdk 3.0.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/dist/index.js CHANGED
@@ -2,6 +2,88 @@
2
2
  var __defProp = Object.defineProperty;
3
3
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
4
 
5
+ // src/lib/sdk/domain/detectProviderDeploymentId/index.ts
6
+ var ciSystems = [
7
+ {
8
+ ci: "github",
9
+ isActive: /* @__PURE__ */ __name((env) => env.GITHUB_ACTIONS === "true", "isActive"),
10
+ // GITHUB_RUN_ID is stable across re-runs and GITHUB_RUN_ATTEMPT increments,
11
+ // and both are shared by every job in the workflow, so GITHUB_JOB separates
12
+ // jobs that deploy in parallel. GITHUB_JOB names the job definition, which
13
+ // every leg of a matrix shares, so those legs need a discriminator.
14
+ variables: ["GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT", "GITHUB_JOB"]
15
+ },
16
+ {
17
+ ci: "gitlab",
18
+ isActive: /* @__PURE__ */ __name((env) => env.GITLAB_CI === "true", "isActive"),
19
+ // Retrying a job keeps the pipeline id and mints a new job id.
20
+ variables: ["CI_PIPELINE_ID", "CI_JOB_ID"]
21
+ },
22
+ {
23
+ ci: "circleci",
24
+ isActive: /* @__PURE__ */ __name((env) => env.CIRCLECI === "true", "isActive"),
25
+ // A rerun keeps the workflow id and allocates a new build number. A job
26
+ // run with `parallelism` shares both across its containers, so those need
27
+ // a discriminator.
28
+ variables: ["CIRCLE_WORKFLOW_ID", "CIRCLE_BUILD_NUM"]
29
+ },
30
+ {
31
+ ci: "buildkite",
32
+ isActive: /* @__PURE__ */ __name((env) => env.BUILDKITE === "true", "isActive"),
33
+ // BUILDKITE_BUILD_ID covers the whole build, so BUILDKITE_JOB_ID separates
34
+ // the jobs within it and BUILDKITE_RETRY_COUNT separates a job's retries.
35
+ variables: [
36
+ "BUILDKITE_BUILD_ID",
37
+ "BUILDKITE_JOB_ID",
38
+ "BUILDKITE_RETRY_COUNT"
39
+ ]
40
+ },
41
+ {
42
+ ci: "jenkins",
43
+ // JENKINS_URL is unset on some agent configurations.
44
+ isActive: /* @__PURE__ */ __name((env) => env.JENKINS_URL !== void 0 || env.JENKINS_HOME !== void 0, "isActive"),
45
+ // BUILD_TAG is `jenkins-${JOB_NAME}-${BUILD_NUMBER}`, and Jenkins allocates
46
+ // a fresh BUILD_NUMBER for every externally visible re-run — Rebuild,
47
+ // Naginator, Replay and Restart from Stage — so it needs no attempt
48
+ // counter. An in-pipeline `retry {}` reuses it, but reports nothing new.
49
+ // A declarative `matrix` or `parallel` block runs its branches inside one
50
+ // build and so shares BUILD_TAG; those branches need a discriminator.
51
+ variables: ["BUILD_TAG"]
52
+ },
53
+ {
54
+ ci: "bitbucket",
55
+ isActive: /* @__PURE__ */ __name((env) => env.BITBUCKET_WORKSPACE !== void 0 || env.BITBUCKET_BUILD_NUMBER !== void 0, "isActive"),
56
+ // Rerunning a whole pipeline mints a new build number, but rerunning only
57
+ // the failed steps keeps it and increments the step's run number instead.
58
+ // Parallel steps share both, so BITBUCKET_STEP_UUID separates them.
59
+ variables: [
60
+ "BITBUCKET_BUILD_NUMBER",
61
+ "BITBUCKET_STEP_UUID",
62
+ "BITBUCKET_STEP_RUN_NUMBER"
63
+ ]
64
+ }
65
+ ];
66
+ function detectProviderDeploymentId({
67
+ discriminator,
68
+ env = process.env
69
+ } = {}) {
70
+ const system = ciSystems.find((candidate) => candidate.isActive(env));
71
+ if (!system) return { outcome: "unsupported" };
72
+ const missingVariables = system.variables.filter((name) => !env[name]);
73
+ if (missingVariables.length > 0)
74
+ return { ci: system.ci, missingVariables, outcome: "missing-variables" };
75
+ const composed = system.variables.map((name) => env[name]).join("-");
76
+ return {
77
+ ci: system.ci,
78
+ // A colon separates the discriminator because a job id can itself contain
79
+ // a hyphen, so joining with one would let job "deploy-web" collide with
80
+ // job "deploy" discriminated by "web".
81
+ id: discriminator ? `${composed}:${discriminator}` : composed,
82
+ outcome: "detected"
83
+ };
84
+ }
85
+ __name(detectProviderDeploymentId, "detectProviderDeploymentId");
86
+
5
87
  // src/lib/other.ts
6
88
  function sleep(ms) {
7
89
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -16,6 +98,9 @@ var buildFetchWithTimeout = /* @__PURE__ */ __name((fetch, defaultTimeoutMs) =>
16
98
  return fetch(args[0], { ...args[1], signal });
17
99
  }, "buildFetchWithTimeout");
18
100
 
101
+ // src/lib/sdk/defaults/clock.ts
102
+ var defaultClock = { now: /* @__PURE__ */ __name(() => Date.now(), "now"), sleep };
103
+
19
104
  // src/lib/sdk/defaults/fetch.ts
20
105
  var defaultFetch = globalThis.fetch;
21
106
 
@@ -38,7 +123,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
38
123
  // package.json
39
124
  var package_default = {
40
125
  name: "@qawolf/ci-sdk",
41
- version: "3.0.0",
126
+ version: "3.2.0",
42
127
  private: false,
43
128
  description: "A simple SDK for interacting with QAWolf in CI scripts.",
44
129
  keywords: [],
@@ -61,18 +146,20 @@ var package_default = {
61
146
  "lint:ci": "npm run lint:first-half; FIRST_HALF_EXIT=$?; npm run lint:second-half && exit $FIRST_HALF_EXIT",
62
147
  "lint:dev": "npm run lint:first-half",
63
148
  "lint:first-half": "oxlint . --fix --quiet --report-unused-disable-directives --type-aware && oxfmt --write .",
64
- "lint:second-half": "dpdm --exit-code circular:1 --no-tree --no-warning --transform --include='.*' --exclude='/(dist|node_modules)/' '**/*'",
149
+ "lint:second-half": "rev-dep config run",
65
150
  prepublishOnly: "nx run ci-sdk:build",
66
151
  test: 'NODE_OPTIONS="--experimental-vm-modules" NODE_NO_WARNINGS=1 jest',
67
152
  "tsc:check": "tsc --noEmit"
68
153
  },
69
154
  dependencies: {
155
+ "@qawolf/api-contracts": ">=0.71.0",
70
156
  "@qawolf/ci-utils": "^2.0.0",
71
157
  "@sinonjs/fake-timers": "^10.3.0",
72
- tslib: "^2.6.2"
158
+ tslib: "^2.6.2",
159
+ zod: "^4.1.11"
73
160
  },
74
161
  devDependencies: {
75
- "@qawolf/platform-builder": "^1.2.3",
162
+ "@qawolf/platform-builder": "^1.2.7",
76
163
  tsx: "^4.19.4",
77
164
  typescript: "^5.9.3"
78
165
  },
@@ -1111,12 +1198,551 @@ async function pollCiGreenlightStatus(deps, apiConfig, {
1111
1198
  }
1112
1199
  __name(pollCiGreenlightStatus, "pollCiGreenlightStatus");
1113
1200
 
1201
+ // src/lib/sdk/domain/reportDeployment/index.ts
1202
+ import { publicContractsV1 } from "@qawolf/api-contracts";
1203
+
1204
+ // src/lib/sdk/publicApi/callContract.ts
1205
+ function isRecord2(value) {
1206
+ return typeof value === "object" && value !== null;
1207
+ }
1208
+ __name(isRecord2, "isRecord");
1209
+ function isSuperJsonEnvelope(data) {
1210
+ return "json" in data && Object.keys(data).every((key) => key === "json" || key === "meta");
1211
+ }
1212
+ __name(isSuperJsonEnvelope, "isSuperJsonEnvelope");
1213
+ function unwrapTrpcEnvelope(body) {
1214
+ if (!isRecord2(body) || !isRecord2(body.result)) return void 0;
1215
+ const data = body.result.data;
1216
+ return isRecord2(data) && isSuperJsonEnvelope(data) ? data.json : data;
1217
+ }
1218
+ __name(unwrapTrpcEnvelope, "unwrapTrpcEnvelope");
1219
+ function readTrpcError(body) {
1220
+ const wrapped = isRecord2(body) ? body.error : void 0;
1221
+ const error = isRecord2(wrapped) && isRecord2(wrapped.json) ? wrapped.json : wrapped;
1222
+ if (!isRecord2(error)) return { eventId: void 0, message: void 0 };
1223
+ const data = isRecord2(error.data) ? error.data : void 0;
1224
+ return {
1225
+ eventId: typeof data?.eventId === "string" ? data.eventId : void 0,
1226
+ message: typeof error.message === "string" ? error.message : void 0
1227
+ };
1228
+ }
1229
+ __name(readTrpcError, "readTrpcError");
1230
+ function parseJson(text) {
1231
+ if (text === void 0) return void 0;
1232
+ try {
1233
+ return JSON.parse(text);
1234
+ } catch {
1235
+ return void 0;
1236
+ }
1237
+ }
1238
+ __name(parseJson, "parseJson");
1239
+ async function readHttpError(response, contractName, log) {
1240
+ const bodyText = await response.text().catch(() => void 0);
1241
+ const { eventId, message } = readTrpcError(parseJson(bodyText));
1242
+ const statusText = response.status >= 500 ? "Server Error" : response.statusText || "Unknown Error";
1243
+ const eventIdSuffix = eventId ? ` (Event ID: ${eventId})` : "";
1244
+ const messageSuffix = message ? `: ${message}` : "";
1245
+ log.error(
1246
+ `\u274C [${contractName}] ${response.status} ${statusText}${eventIdSuffix}${messageSuffix}`
1247
+ );
1248
+ return {
1249
+ eventId,
1250
+ httpStatus: response.status,
1251
+ message,
1252
+ outcome: "http-error"
1253
+ };
1254
+ }
1255
+ __name(readHttpError, "readHttpError");
1256
+ async function callPublicContract({ fetch: localFetch, log }, apiConfig, {
1257
+ contract,
1258
+ input
1259
+ }) {
1260
+ const procedurePath = `public.${contract.name}`;
1261
+ let bodyText;
1262
+ try {
1263
+ const response = await localFetch(
1264
+ new URL(`/api/trpc/${procedurePath}`, apiConfig.serviceBase),
1265
+ {
1266
+ // The server's superjson transformer reads the input from `json`.
1267
+ body: JSON.stringify({ json: input }),
1268
+ headers: {
1269
+ Authorization: `Bearer ${apiConfig.apiKey}`,
1270
+ "Content-Type": "application/json",
1271
+ "User-Agent": apiConfig.userAgent
1272
+ },
1273
+ method: "POST"
1274
+ }
1275
+ );
1276
+ if (!response.ok) return readHttpError(response, contract.name, log);
1277
+ bodyText = await response.text();
1278
+ } catch {
1279
+ log.error(`\u{1F6AB} Network error from ${contract.name}.`);
1280
+ return { outcome: "network-error" };
1281
+ }
1282
+ const parsed = contract.output.safeParse(
1283
+ unwrapTrpcEnvelope(parseJson(bodyText))
1284
+ );
1285
+ if (!parsed.success) {
1286
+ log.error(
1287
+ `\u{1F6AB} Invalid response from ${contract.name} (${parsed.error.issues[0]?.message ?? "the response body did not match the contract"}), aborting.`
1288
+ );
1289
+ return { outcome: "invalid-response-body" };
1290
+ }
1291
+ return { data: parsed.data, outcome: "ok" };
1292
+ }
1293
+ __name(callPublicContract, "callPublicContract");
1294
+
1295
+ // src/lib/sdk/domain/reportDeployment/index.ts
1296
+ async function reportDeployment(deps, apiConfig, params) {
1297
+ const result = await callPublicContract(deps, apiConfig, {
1298
+ contract: publicContractsV1.deployment.reportStatus,
1299
+ input: params
1300
+ });
1301
+ if (result.outcome !== "ok") return mapFailure(result);
1302
+ const { deployment } = result.data;
1303
+ deps.log.info(
1304
+ `\u2705 Deployment ${deployment.id} reported as ${deployment.status}: ${deployment.url}`
1305
+ );
1306
+ return { deployment, outcome: "reported" };
1307
+ }
1308
+ __name(reportDeployment, "reportDeployment");
1309
+ function mapFailure(result) {
1310
+ if (result.outcome !== "http-error") return { outcome: result.outcome };
1311
+ const { httpStatus, message } = result;
1312
+ if (httpStatus === 400) return { message, outcome: "invalid-request" };
1313
+ if (httpStatus === 401 || httpStatus === 403)
1314
+ return { message, outcome: "unauthorized" };
1315
+ if (httpStatus === 404) return { message, outcome: "not-found" };
1316
+ if (httpStatus === 409) return { message, outcome: "conflict" };
1317
+ if (httpStatus >= 500) return { httpStatus, outcome: "server-error" };
1318
+ return { httpStatus, message, outcome: "unexpected-status" };
1319
+ }
1320
+ __name(mapFailure, "mapFailure");
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
+
1114
1739
  // src/lib/sdk/index.ts
1115
1740
  function makeQaWolfSdk({
1116
1741
  apiKey,
1117
1742
  serviceBase = defaultServiceBase,
1118
1743
  userAgent = defaultUserAgent
1119
1744
  }, {
1745
+ clock = defaultClock,
1120
1746
  fetch = defaultFetch,
1121
1747
  log = defaultLogDriver
1122
1748
  } = {}) {
@@ -1127,6 +1753,7 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1127
1753
  );
1128
1754
  }
1129
1755
  const deps = {
1756
+ clock,
1130
1757
  // Versions of fetch may have different default timeouts. For consistency, we explicitly set it to 2 minutes here.
1131
1758
  // The increased timeout helps avoid edge cases where we need to search for a commit against multiple
1132
1759
  // code hosting service repositories as a fallback when not finding the repository based on organization/name.
@@ -1135,7 +1762,15 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1135
1762
  };
1136
1763
  const apiConfig = { apiKey, serviceBase, userAgent };
1137
1764
  return {
1765
+ /**
1766
+ * @deprecated Reports through the legacy `deploy_success` webhook, which
1767
+ * triggers do not see. Use `deployment.reportStatus` instead.
1768
+ */
1138
1769
  attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
1770
+ deployment: {
1771
+ reportStatus: reportDeployment.bind(null, deps, apiConfig),
1772
+ waitForVerdict: waitForVerdict.bind(null, deps, apiConfig)
1773
+ },
1139
1774
  generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(
1140
1775
  null,
1141
1776
  deps,
@@ -1148,11 +1783,16 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1148
1783
  apiConfig
1149
1784
  ),
1150
1785
  notifyTerminatedEphemeralEnvironment: notifyTerminatedEphemeralEnvironment.bind(null, deps, apiConfig),
1786
+ /**
1787
+ * @deprecated Polls a run id that only `attemptNotifyDeploy` returns, so it
1788
+ * keeps the pipeline on the legacy `deploy_success` webhook.
1789
+ */
1151
1790
  pollCiGreenlightStatus: pollCiGreenlightStatus.bind(null, deps, apiConfig)
1152
1791
  };
1153
1792
  }
1154
1793
  __name(makeQaWolfSdk, "makeQaWolfSdk");
1155
1794
  export {
1795
+ detectProviderDeploymentId,
1156
1796
  makeQaWolfSdk
1157
1797
  };
1158
1798
  //# sourceMappingURL=index.js.map