@qawolf/ci-sdk 2.8.0 → 3.1.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
@@ -1,7 +1,89 @@
1
- /* Built with @qawolf/platform-builder@1.2.3 */
1
+ /* Built with @qawolf/platform-builder@1.2.7 */
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));
@@ -38,7 +120,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
38
120
  // package.json
39
121
  var package_default = {
40
122
  name: "@qawolf/ci-sdk",
41
- version: "2.8.0",
123
+ version: "3.1.0",
42
124
  private: false,
43
125
  description: "A simple SDK for interacting with QAWolf in CI scripts.",
44
126
  keywords: [],
@@ -53,33 +135,33 @@ var package_default = {
53
135
  exports: {
54
136
  ".": {
55
137
  types: "./dist/index.d.ts",
56
- require: "./dist/index.cjs",
57
- import: "./dist/index.js",
58
- default: "./dist/index.js"
138
+ import: "./dist/index.js"
59
139
  }
60
140
  },
61
141
  scripts: {
62
- build: "platform-builder --config platformBuilder.config.ts --config platformBuilder.cjs.config.ts",
142
+ build: "platform-builder --config platformBuilder.config.ts",
63
143
  "lint:ci": "npm run lint:first-half; FIRST_HALF_EXIT=$?; npm run lint:second-half && exit $FIRST_HALF_EXIT",
64
144
  "lint:dev": "npm run lint:first-half",
65
- "lint:first-half": "eslint . --fix --quiet && oxfmt --write .",
66
- "lint:second-half": "dpdm --exit-code circular:1 --no-tree --no-warning --transform --include='.*' --exclude='/(dist|node_modules)/' '**/*'",
145
+ "lint:first-half": "oxlint . --fix --quiet --report-unused-disable-directives --type-aware && oxfmt --write .",
146
+ "lint:second-half": "rev-dep config run",
67
147
  prepublishOnly: "nx run ci-sdk:build",
68
148
  test: 'NODE_OPTIONS="--experimental-vm-modules" NODE_NO_WARNINGS=1 jest',
69
149
  "tsc:check": "tsc --noEmit"
70
150
  },
71
151
  dependencies: {
72
- "@qawolf/ci-utils": "^1.1.2",
152
+ "@qawolf/api-contracts": ">=0.68.0",
153
+ "@qawolf/ci-utils": "^2.0.0",
73
154
  "@sinonjs/fake-timers": "^10.3.0",
74
- tslib: "^2.6.2"
155
+ tslib: "^2.6.2",
156
+ zod: "^4.1.11"
75
157
  },
76
158
  devDependencies: {
77
- "@qawolf/platform-builder": "^1.2.3",
159
+ "@qawolf/platform-builder": "^1.2.7",
78
160
  tsx: "^4.19.4",
79
161
  typescript: "^5.9.3"
80
162
  },
81
163
  engines: {
82
- node: ">=20"
164
+ node: ">=24.14.1 <25"
83
165
  }
84
166
  };
85
167
 
@@ -1113,6 +1195,127 @@ async function pollCiGreenlightStatus(deps, apiConfig, {
1113
1195
  }
1114
1196
  __name(pollCiGreenlightStatus, "pollCiGreenlightStatus");
1115
1197
 
1198
+ // src/lib/sdk/domain/reportDeployment/index.ts
1199
+ import { publicContractsV1 } from "@qawolf/api-contracts";
1200
+
1201
+ // src/lib/sdk/publicApi/callContract.ts
1202
+ function isRecord2(value) {
1203
+ return typeof value === "object" && value !== null;
1204
+ }
1205
+ __name(isRecord2, "isRecord");
1206
+ function isSuperJsonEnvelope(data) {
1207
+ return "json" in data && Object.keys(data).every((key) => key === "json" || key === "meta");
1208
+ }
1209
+ __name(isSuperJsonEnvelope, "isSuperJsonEnvelope");
1210
+ function unwrapTrpcEnvelope(body) {
1211
+ if (!isRecord2(body) || !isRecord2(body.result)) return void 0;
1212
+ const data = body.result.data;
1213
+ return isRecord2(data) && isSuperJsonEnvelope(data) ? data.json : data;
1214
+ }
1215
+ __name(unwrapTrpcEnvelope, "unwrapTrpcEnvelope");
1216
+ function readTrpcError(body) {
1217
+ const wrapped = isRecord2(body) ? body.error : void 0;
1218
+ const error = isRecord2(wrapped) && isRecord2(wrapped.json) ? wrapped.json : wrapped;
1219
+ if (!isRecord2(error)) return { eventId: void 0, message: void 0 };
1220
+ const data = isRecord2(error.data) ? error.data : void 0;
1221
+ return {
1222
+ eventId: typeof data?.eventId === "string" ? data.eventId : void 0,
1223
+ message: typeof error.message === "string" ? error.message : void 0
1224
+ };
1225
+ }
1226
+ __name(readTrpcError, "readTrpcError");
1227
+ function parseJson(text) {
1228
+ if (text === void 0) return void 0;
1229
+ try {
1230
+ return JSON.parse(text);
1231
+ } catch {
1232
+ return void 0;
1233
+ }
1234
+ }
1235
+ __name(parseJson, "parseJson");
1236
+ async function readHttpError(response, contractName, log) {
1237
+ const bodyText = await response.text().catch(() => void 0);
1238
+ const { eventId, message } = readTrpcError(parseJson(bodyText));
1239
+ const statusText = response.status >= 500 ? "Server Error" : response.statusText || "Unknown Error";
1240
+ const eventIdSuffix = eventId ? ` (Event ID: ${eventId})` : "";
1241
+ const messageSuffix = message ? `: ${message}` : "";
1242
+ log.error(
1243
+ `\u274C [${contractName}] ${response.status} ${statusText}${eventIdSuffix}${messageSuffix}`
1244
+ );
1245
+ return {
1246
+ eventId,
1247
+ httpStatus: response.status,
1248
+ message,
1249
+ outcome: "http-error"
1250
+ };
1251
+ }
1252
+ __name(readHttpError, "readHttpError");
1253
+ async function callPublicContract({ fetch: localFetch, log }, apiConfig, {
1254
+ contract,
1255
+ input
1256
+ }) {
1257
+ const procedurePath = `public.${contract.name}`;
1258
+ let bodyText;
1259
+ try {
1260
+ const response = await localFetch(
1261
+ new URL(`/api/trpc/${procedurePath}`, apiConfig.serviceBase),
1262
+ {
1263
+ // The server's superjson transformer reads the input from `json`.
1264
+ body: JSON.stringify({ json: input }),
1265
+ headers: {
1266
+ Authorization: `Bearer ${apiConfig.apiKey}`,
1267
+ "Content-Type": "application/json",
1268
+ "User-Agent": apiConfig.userAgent
1269
+ },
1270
+ method: "POST"
1271
+ }
1272
+ );
1273
+ if (!response.ok) return readHttpError(response, contract.name, log);
1274
+ bodyText = await response.text();
1275
+ } catch {
1276
+ log.error(`\u{1F6AB} Network error from ${contract.name}, aborting.`);
1277
+ return { outcome: "network-error" };
1278
+ }
1279
+ const parsed = contract.output.safeParse(
1280
+ unwrapTrpcEnvelope(parseJson(bodyText))
1281
+ );
1282
+ if (!parsed.success) {
1283
+ log.error(
1284
+ `\u{1F6AB} Invalid response from ${contract.name} (${parsed.error.issues[0]?.message ?? "the response body did not match the contract"}), aborting.`
1285
+ );
1286
+ return { outcome: "invalid-response-body" };
1287
+ }
1288
+ return { data: parsed.data, outcome: "ok" };
1289
+ }
1290
+ __name(callPublicContract, "callPublicContract");
1291
+
1292
+ // src/lib/sdk/domain/reportDeployment/index.ts
1293
+ async function reportDeployment(deps, apiConfig, params) {
1294
+ const result = await callPublicContract(deps, apiConfig, {
1295
+ contract: publicContractsV1.deployment.reportStatus,
1296
+ input: params
1297
+ });
1298
+ if (result.outcome !== "ok") return mapFailure(result);
1299
+ const { deployment } = result.data;
1300
+ deps.log.info(
1301
+ `\u2705 Deployment ${deployment.id} reported as ${deployment.status}: ${deployment.url}`
1302
+ );
1303
+ return { deployment, outcome: "reported" };
1304
+ }
1305
+ __name(reportDeployment, "reportDeployment");
1306
+ function mapFailure(result) {
1307
+ if (result.outcome !== "http-error") return { outcome: result.outcome };
1308
+ const { httpStatus, message } = result;
1309
+ if (httpStatus === 400) return { message, outcome: "invalid-request" };
1310
+ if (httpStatus === 401 || httpStatus === 403)
1311
+ return { message, outcome: "unauthorized" };
1312
+ if (httpStatus === 404) return { message, outcome: "not-found" };
1313
+ if (httpStatus === 409) return { message, outcome: "conflict" };
1314
+ if (httpStatus >= 500) return { httpStatus, outcome: "server-error" };
1315
+ return { httpStatus, message, outcome: "unexpected-status" };
1316
+ }
1317
+ __name(mapFailure, "mapFailure");
1318
+
1116
1319
  // src/lib/sdk/index.ts
1117
1320
  function makeQaWolfSdk({
1118
1321
  apiKey,
@@ -1137,7 +1340,14 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1137
1340
  };
1138
1341
  const apiConfig = { apiKey, serviceBase, userAgent };
1139
1342
  return {
1343
+ /**
1344
+ * @deprecated Reports through the legacy `deploy_success` webhook, which
1345
+ * triggers do not see. Use `deployment.reportStatus` instead.
1346
+ */
1140
1347
  attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
1348
+ deployment: {
1349
+ reportStatus: reportDeployment.bind(null, deps, apiConfig)
1350
+ },
1141
1351
  generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(
1142
1352
  null,
1143
1353
  deps,
@@ -1150,11 +1360,16 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
1150
1360
  apiConfig
1151
1361
  ),
1152
1362
  notifyTerminatedEphemeralEnvironment: notifyTerminatedEphemeralEnvironment.bind(null, deps, apiConfig),
1363
+ /**
1364
+ * @deprecated Polls a run id that only `attemptNotifyDeploy` returns, so it
1365
+ * keeps the pipeline on the legacy `deploy_success` webhook.
1366
+ */
1153
1367
  pollCiGreenlightStatus: pollCiGreenlightStatus.bind(null, deps, apiConfig)
1154
1368
  };
1155
1369
  }
1156
1370
  __name(makeQaWolfSdk, "makeQaWolfSdk");
1157
1371
  export {
1372
+ detectProviderDeploymentId,
1158
1373
  makeQaWolfSdk
1159
1374
  };
1160
1375
  //# sourceMappingURL=index.js.map