@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/README.md +338 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +644 -4
- package/dist/index.js.map +4 -4
- package/dist/lib/sdk/defaults/clock.d.ts +2 -0
- package/dist/lib/sdk/dependencies.d.ts +9 -0
- package/dist/lib/sdk/domain/attemptDeploy/index.d.ts +4 -0
- package/dist/lib/sdk/domain/detectProviderDeploymentId/index.d.ts +49 -0
- package/dist/lib/sdk/domain/pollCiGreenlight/index.d.ts +4 -0
- package/dist/lib/sdk/domain/reportDeployment/index.d.ts +4 -0
- package/dist/lib/sdk/domain/reportDeployment/types.d.ts +50 -0
- package/dist/lib/sdk/domain/waitForVerdict/awaitRuns.d.ts +14 -0
- package/dist/lib/sdk/domain/waitForVerdict/awaitVerdict.d.ts +12 -0
- package/dist/lib/sdk/domain/waitForVerdict/consts.d.ts +13 -0
- package/dist/lib/sdk/domain/waitForVerdict/failures.d.ts +14 -0
- package/dist/lib/sdk/domain/waitForVerdict/index.d.ts +12 -0
- package/dist/lib/sdk/domain/waitForVerdict/logVerdict.d.ts +3 -0
- package/dist/lib/sdk/domain/waitForVerdict/poll.d.ts +20 -0
- package/dist/lib/sdk/domain/waitForVerdict/progress.d.ts +7 -0
- package/dist/lib/sdk/domain/waitForVerdict/readEvaluations.d.ts +20 -0
- package/dist/lib/sdk/domain/waitForVerdict/testHarness.d.ts +53 -0
- package/dist/lib/sdk/domain/waitForVerdict/types.d.ts +48 -0
- package/dist/lib/sdk/index.d.ts +130 -1
- package/dist/lib/sdk/publicApi/callContract.d.ts +39 -0
- package/package.json +6 -4
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.
|
|
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": "
|
|
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.
|
|
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
|