@qawolf/ci-sdk 2.0.0 → 2.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 +6 -0
- package/dist/index.cjs +120 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -42
- package/dist/index.d.ts +72 -42
- package/dist/index.js +120 -13
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -454,8 +454,10 @@ const sdk = makeQaWolfSdk(
|
|
|
454
454
|
This package follows the [SemVer](https://semver.org/) versioning scheme. Additional notes:
|
|
455
455
|
|
|
456
456
|
- We recommend depending on the `^` range operator for this package, as it will not introduce breaking changes and guarantee an up-to-date API usage version.
|
|
457
|
+
|
|
457
458
|
<!-- There is a limitation in NPM preventing us from using relative links and hosting other MD files when the repository hosting code is not
|
|
458
459
|
public, hence we inline the changelog in the README. See https://github.com/npm/feedback/discussions/666 -->
|
|
460
|
+
|
|
459
461
|
- We will provide a changelog for each release, which will be available in the Changelog section below.
|
|
460
462
|
- This package major version will be bumped when an API breaking change is introduced. This won't
|
|
461
463
|
happen too often, and we will reach out to you and give advance notice when it does.
|
|
@@ -467,6 +469,10 @@ This package follows the [SemVer](https://semver.org/) versioning scheme. Additi
|
|
|
467
469
|
|
|
468
470
|
# Changelog
|
|
469
471
|
|
|
472
|
+
## v2.1.0
|
|
473
|
+
|
|
474
|
+
- `attemptNotifyDeploy`: the `deploy_success` response is now validated at runtime instead of being trusted. A response with a non-JSON body, or a body that does not match the expected shape, returns `outcome: "aborted"` with the new `abortReason: "invalid-response-body"`. This is distinct from `"network-error"`, which now covers only transport and body-stream failures rather than also masking unparseable responses. A result the SDK does not recognize (for example a new result kind sent by a newer backend) returns `outcome: "failed"` with the new `failReason: "unknown-result"` instead of being reported as a success. Recognized results are unchanged, and older backends that omit newer fields remain fully supported.
|
|
475
|
+
|
|
470
476
|
## v2.0.0
|
|
471
477
|
|
|
472
478
|
- `attemptNotifyDeploy`: the `DeployStatus` response now carries `skippedTriggers` (triggers that matched the deployment but produced no run, each with a `reason` code and human-readable `message`) and `skippedDeployment` (set when no trigger matched at all). The result now distinguishes these cases. A trigger that matched but could not run (e.g. the environment was still syncing) returns `outcome: "failed"` with `failReason: "trigger-skipped"` and a `skippedTriggers` array, since a precondition was not met and retrying may succeed. A deployment that matched no trigger returns `outcome: "skipped"` with `skippedDeployment` (the server's reason, when it provides one). That result is deterministic given the current configuration, so it is reported as a skip rather than a failure and is logged as a warning, not an error. Preview deployments keep `outcome: "success"` but now log the skip reason. `outcome: "success"` results are unchanged.
|
package/dist/index.cjs
CHANGED
|
@@ -41,7 +41,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
|
|
|
41
41
|
|
|
42
42
|
// package.json
|
|
43
43
|
var package_default = {
|
|
44
|
-
version: "2.
|
|
44
|
+
version: "2.2.0"};
|
|
45
45
|
|
|
46
46
|
// src/lib/sdk/defaults/userAgent.ts
|
|
47
47
|
var defaultUserAgent = `ci-sdk/${package_default.version}`;
|
|
@@ -123,6 +123,14 @@ async function handleSuccessResponse(data, log) {
|
|
|
123
123
|
outcome: "failed"
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
+
const unrecognizedResult = results.find((result) => "unrecognized" in result);
|
|
127
|
+
if (unrecognizedResult) {
|
|
128
|
+
log.error(`\u{1F6AB} The deployment response contained an unrecognized result. Please upgrade @qawolf/ci-sdk.`);
|
|
129
|
+
return {
|
|
130
|
+
failReason: "unknown-result",
|
|
131
|
+
outcome: "failed"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
126
134
|
const failedTrigger = results.find((result) => "failureReason" in result);
|
|
127
135
|
if (failedTrigger) {
|
|
128
136
|
log.error(`\u{1F6AB} Run could not be created. Reason: "${failedTrigger.failureReason}". Please contact support.`);
|
|
@@ -171,10 +179,16 @@ function mapSuiteResult(result) {
|
|
|
171
179
|
triggerId: result.trigger_id
|
|
172
180
|
};
|
|
173
181
|
}
|
|
182
|
+
if ("duplicate_suite_id" in result) {
|
|
183
|
+
return {
|
|
184
|
+
duplicateSuiteId: result.duplicate_suite_id,
|
|
185
|
+
skipReason: result.skip_reason,
|
|
186
|
+
triggerId: result.trigger_id
|
|
187
|
+
};
|
|
188
|
+
}
|
|
174
189
|
return {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
triggerId: result.trigger_id
|
|
190
|
+
triggerId: result.trigger_id,
|
|
191
|
+
unrecognized: true
|
|
178
192
|
};
|
|
179
193
|
}
|
|
180
194
|
__name(mapSuiteResult, "mapSuiteResult");
|
|
@@ -196,11 +210,61 @@ function mapDeploySuccessResponse(wire) {
|
|
|
196
210
|
}
|
|
197
211
|
__name(mapDeploySuccessResponse, "mapDeploySuccessResponse");
|
|
198
212
|
|
|
213
|
+
// src/lib/sdk/domain/attemptDeploy/parseDeploySuccessWireResponse.ts
|
|
214
|
+
function isRecord(value) {
|
|
215
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
__name(isRecord, "isRecord");
|
|
218
|
+
function isWireSuiteResult(value) {
|
|
219
|
+
if (!isRecord(value)) return false;
|
|
220
|
+
if (typeof value.trigger_id !== "string") return false;
|
|
221
|
+
if ("created_suite_id" in value) return typeof value.created_suite_id === "string";
|
|
222
|
+
if ("failure_reason" in value) return typeof value.failure_reason === "string";
|
|
223
|
+
if ("duplicate_suite_id" in value) {
|
|
224
|
+
return typeof value.duplicate_suite_id === "string" && typeof value.skip_reason === "string";
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
__name(isWireSuiteResult, "isWireSuiteResult");
|
|
229
|
+
function isEnvironment(value) {
|
|
230
|
+
if (!isRecord(value)) return false;
|
|
231
|
+
if (typeof value.id !== "string") return false;
|
|
232
|
+
if (typeof value.isStatic !== "boolean") return false;
|
|
233
|
+
if (value.alias !== void 0 && typeof value.alias !== "string") return false;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
__name(isEnvironment, "isEnvironment");
|
|
237
|
+
function isSkippedDeployment(value) {
|
|
238
|
+
if (!isRecord(value)) return false;
|
|
239
|
+
return typeof value.message === "string" && typeof value.reason === "string";
|
|
240
|
+
}
|
|
241
|
+
__name(isSkippedDeployment, "isSkippedDeployment");
|
|
242
|
+
function isSkippedTrigger(value) {
|
|
243
|
+
if (!isRecord(value)) return false;
|
|
244
|
+
return typeof value.environment_id === "string" && typeof value.message === "string" && typeof value.reason === "string" && typeof value.trigger_id === "string";
|
|
245
|
+
}
|
|
246
|
+
__name(isSkippedTrigger, "isSkippedTrigger");
|
|
247
|
+
function isDeploySuccessWireResponse(value) {
|
|
248
|
+
if (!isRecord(value)) return false;
|
|
249
|
+
if (value.environment !== void 0 && !isEnvironment(value.environment)) return false;
|
|
250
|
+
if (!Array.isArray(value.results) || !value.results.every(isWireSuiteResult)) return false;
|
|
251
|
+
if (value.skipped_deployment !== void 0 && !isSkippedDeployment(value.skipped_deployment)) return false;
|
|
252
|
+
if (value.skipped_triggers !== void 0 && (!Array.isArray(value.skipped_triggers) || !value.skipped_triggers.every(isSkippedTrigger))) return false;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
__name(isDeploySuccessWireResponse, "isDeploySuccessWireResponse");
|
|
256
|
+
function parseDeploySuccessWireResponse(value) {
|
|
257
|
+
return isDeploySuccessWireResponse(value) ? value : "invalid";
|
|
258
|
+
}
|
|
259
|
+
__name(parseDeploySuccessWireResponse, "parseDeploySuccessWireResponse");
|
|
260
|
+
|
|
199
261
|
// src/lib/sdk/domain/attemptDeploy/index.ts
|
|
200
262
|
async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
201
263
|
const { fetch: localFetch, log } = deps;
|
|
264
|
+
let response;
|
|
265
|
+
let bodyText;
|
|
202
266
|
try {
|
|
203
|
-
|
|
267
|
+
response = await localFetch(new URL("/api/webhooks/deploy_success", apiConfig.serviceBase), {
|
|
204
268
|
body: JSON.stringify({
|
|
205
269
|
branch: config.branch,
|
|
206
270
|
commit_url: config.commitUrl,
|
|
@@ -216,7 +280,8 @@ async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
|
216
280
|
repository: config.repository
|
|
217
281
|
},
|
|
218
282
|
sha: config.sha,
|
|
219
|
-
variables: config.variables
|
|
283
|
+
variables: config.variables,
|
|
284
|
+
workspaceId: config.workspaceId
|
|
220
285
|
}),
|
|
221
286
|
headers: {
|
|
222
287
|
Authorization: `Bearer ${apiConfig.apiKey}`,
|
|
@@ -229,9 +294,7 @@ async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
|
229
294
|
signal: AbortSignal.timeout(5 * 60 * 1e3)
|
|
230
295
|
});
|
|
231
296
|
if (!response.ok) return handleErrorResponse(response, log);
|
|
232
|
-
|
|
233
|
-
const data = mapDeploySuccessResponse(wire);
|
|
234
|
-
return handleSuccessResponse(data, log);
|
|
297
|
+
bodyText = await response.text();
|
|
235
298
|
} catch {
|
|
236
299
|
log.error(`\u{1F6AB} Network error from deploy_success, aborting.`);
|
|
237
300
|
return {
|
|
@@ -240,8 +303,29 @@ async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
|
240
303
|
outcome: "aborted"
|
|
241
304
|
};
|
|
242
305
|
}
|
|
306
|
+
let rawBody;
|
|
307
|
+
try {
|
|
308
|
+
rawBody = JSON.parse(bodyText);
|
|
309
|
+
} catch {
|
|
310
|
+
return abortInvalidResponseBody(log, "the response body was not JSON");
|
|
311
|
+
}
|
|
312
|
+
const wire = parseDeploySuccessWireResponse(rawBody);
|
|
313
|
+
if (wire === "invalid") {
|
|
314
|
+
return abortInvalidResponseBody(log, "the response body did not match the expected shape");
|
|
315
|
+
}
|
|
316
|
+
const data = mapDeploySuccessResponse(wire);
|
|
317
|
+
return handleSuccessResponse(data, log);
|
|
243
318
|
}
|
|
244
319
|
__name(attemptNotifyDeploy, "attemptNotifyDeploy");
|
|
320
|
+
function abortInvalidResponseBody(log, detail) {
|
|
321
|
+
log.error(`\u{1F6AB} Invalid response from deploy_success (${detail}), aborting.`);
|
|
322
|
+
return {
|
|
323
|
+
abortReason: "invalid-response-body",
|
|
324
|
+
httpStatus: void 0,
|
|
325
|
+
outcome: "aborted"
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
__name(abortInvalidResponseBody, "abortInvalidResponseBody");
|
|
245
329
|
async function handleErrorResponse(response, log) {
|
|
246
330
|
const status = response.status;
|
|
247
331
|
let abortReason;
|
|
@@ -263,10 +347,28 @@ async function handleErrorResponse(response, log) {
|
|
|
263
347
|
}
|
|
264
348
|
__name(handleErrorResponse, "handleErrorResponse");
|
|
265
349
|
|
|
350
|
+
// src/lib/api/signed-url.ts
|
|
351
|
+
var signedUrlPaths = {
|
|
352
|
+
runInputsExecutables: "/api/v0/run-inputs-executables-signed-urls",
|
|
353
|
+
teamStorage: "/api/v0/team-storage-signed-url"
|
|
354
|
+
};
|
|
355
|
+
function buildSignedUrl({ destinationFilePath, endpoint, serviceBase, workspaceId }) {
|
|
356
|
+
const url = new URL(signedUrlPaths[endpoint], serviceBase);
|
|
357
|
+
if (destinationFilePath !== void 0) url.searchParams.set("file", destinationFilePath);
|
|
358
|
+
if (workspaceId !== void 0) url.searchParams.set("workspaceId", workspaceId);
|
|
359
|
+
return url;
|
|
360
|
+
}
|
|
361
|
+
__name(buildSignedUrl, "buildSignedUrl");
|
|
362
|
+
|
|
266
363
|
// src/lib/api/generate-signed-url-for-run-inputs-executables.ts
|
|
267
|
-
async function callGenerateSignedUrlForRunInputsExecutablesStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath }, { fetch: localFetch, log }) {
|
|
364
|
+
async function callGenerateSignedUrlForRunInputsExecutablesStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath, workspaceId }, { fetch: localFetch, log }) {
|
|
268
365
|
try {
|
|
269
|
-
const response = await localFetch(
|
|
366
|
+
const response = await localFetch(buildSignedUrl({
|
|
367
|
+
destinationFilePath,
|
|
368
|
+
endpoint: "runInputsExecutables",
|
|
369
|
+
serviceBase,
|
|
370
|
+
workspaceId
|
|
371
|
+
}), {
|
|
270
372
|
headers: {
|
|
271
373
|
Authorization: `Bearer ${apiKey}`,
|
|
272
374
|
"Content-Type": "application/json",
|
|
@@ -344,9 +446,14 @@ async function generateSignedUrlForRunInputsExecutablesStorage(deps, apiConfig,
|
|
|
344
446
|
__name(generateSignedUrlForRunInputsExecutablesStorage, "generateSignedUrlForRunInputsExecutablesStorage");
|
|
345
447
|
|
|
346
448
|
// src/lib/api/generate-signed-url-for-team-storage.ts
|
|
347
|
-
async function callGenerateSignedUrlForTempTeamStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath }, { fetch: localFetch, log }) {
|
|
449
|
+
async function callGenerateSignedUrlForTempTeamStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath, workspaceId }, { fetch: localFetch, log }) {
|
|
348
450
|
try {
|
|
349
|
-
const response = await localFetch(
|
|
451
|
+
const response = await localFetch(buildSignedUrl({
|
|
452
|
+
destinationFilePath,
|
|
453
|
+
endpoint: "teamStorage",
|
|
454
|
+
serviceBase,
|
|
455
|
+
workspaceId
|
|
456
|
+
}), {
|
|
350
457
|
headers: {
|
|
351
458
|
Authorization: `Bearer ${apiKey}`,
|
|
352
459
|
"Content-Type": "application/json",
|