@qawolf/ci-sdk 1.3.2 → 2.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/README.md +22 -1
- package/dist/index.cjs +211 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -35
- package/dist/index.d.ts +111 -35
- package/dist/index.js +211 -67
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -113,8 +113,14 @@ const { attemptNotifyDeploy } = makeQaWolfSdk({
|
|
|
113
113
|
|
|
114
114
|
(async () => {
|
|
115
115
|
const result = await attemptNotifyDeploy(deployConfig);
|
|
116
|
+
if (result.outcome === "skipped") {
|
|
117
|
+
// No trigger matched this deployment, a deterministic no-op rather than a
|
|
118
|
+
// failure. Let the job pass (or fail here instead if you would rather
|
|
119
|
+
// catch a trigger misconfiguration).
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
116
122
|
if (result.outcome !== "success") {
|
|
117
|
-
//
|
|
123
|
+
// "failed" or "aborted": fail the job.
|
|
118
124
|
process.exit(1);
|
|
119
125
|
}
|
|
120
126
|
const runId = result.runId;
|
|
@@ -461,6 +467,21 @@ This package follows the [SemVer](https://semver.org/) versioning scheme. Additi
|
|
|
461
467
|
|
|
462
468
|
# Changelog
|
|
463
469
|
|
|
470
|
+
## v2.1.0
|
|
471
|
+
|
|
472
|
+
- `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.
|
|
473
|
+
|
|
474
|
+
## v2.0.0
|
|
475
|
+
|
|
476
|
+
- `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.
|
|
477
|
+
- `pollCiGreenlightStatus`: the `canceled` `CiGreenlightStatus` object may now include a `reason` code and a human-readable `message` explaining why the run was canceled. These are read off the status directly (via the `onRunStageChanged` callback or `makePollCiGreenlightStatusIterator`); the top-level `pollCiGreenlightStatus` result and logs are unchanged.
|
|
478
|
+
|
|
479
|
+
BREAKING CHANGES:
|
|
480
|
+
|
|
481
|
+
- `attemptNotifyDeploy`: a deployment that matched no trigger now returns `outcome: "skipped"` instead of `outcome: "failed"` / `failReason: "no-matched-trigger"`. Callers that switch on `outcome` must handle the new `"skipped"` case; code that treated a no-trigger-match as a failure should decide whether a deterministic no-op should still fail the deploy step. `AttemptDeployFailedReason` no longer includes `"no-matched-trigger"`.
|
|
482
|
+
- `pollCiGreenlightStatus`: on the `initializing` and `canceled` run stages, the fields `relevantRunWithBugsUrl`, `workflowsDisabledAfterRunCount`, `workflowsInRunCount`, and `workflowsUnderInvestigationCount` are now optional, aligning the types with the greenlight API. Code that reads these fields on those stages must handle `undefined`.
|
|
483
|
+
- Removed the deprecated `blockingBugUrls` and `nonBlockingBugUrls` fields and the `CiGreenlightCompletedSuiteDetails` type. Use `reproducedBugs` filtered by `isBlocking`, reading each bug's `applicationUrl`.
|
|
484
|
+
|
|
464
485
|
## v1.3.2
|
|
465
486
|
|
|
466
487
|
- Bump dependencies to address security alerts.
|
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: "1.
|
|
44
|
+
version: "2.1.0"};
|
|
45
45
|
|
|
46
46
|
// src/lib/sdk/defaults/userAgent.ts
|
|
47
47
|
var defaultUserAgent = `ci-sdk/${package_default.version}`;
|
|
@@ -76,11 +76,195 @@ async function logFetchError({ log, methodName, response }) {
|
|
|
76
76
|
}
|
|
77
77
|
__name(logFetchError, "logFetchError");
|
|
78
78
|
|
|
79
|
+
// src/lib/sdk/domain/attemptDeploy/handleSuccessResponse.ts
|
|
80
|
+
async function handleSuccessResponse(data, log) {
|
|
81
|
+
const { environment, results } = data;
|
|
82
|
+
const skippedTriggers = data.skippedTriggers ?? [];
|
|
83
|
+
const skippedDeployment = data.skippedDeployment;
|
|
84
|
+
const isPreviewDeployment = environment && !environment.isStatic;
|
|
85
|
+
if (environment) {
|
|
86
|
+
const logPrefix = environment.isStatic ? "Static environment" : "Preview environment";
|
|
87
|
+
log.info(`${logPrefix}: ${environment.alias} (id: ${environment.id})`);
|
|
88
|
+
}
|
|
89
|
+
if (results.length === 0) {
|
|
90
|
+
const skipMessages = [
|
|
91
|
+
...skippedTriggers.map((trigger) => trigger.message),
|
|
92
|
+
...skippedDeployment ? [
|
|
93
|
+
skippedDeployment.message
|
|
94
|
+
] : []
|
|
95
|
+
];
|
|
96
|
+
const skipDetail = skipMessages.length > 0 ? ` ${skipMessages.join(" ")}` : "";
|
|
97
|
+
if (isPreviewDeployment) {
|
|
98
|
+
log.info(`\u2705 Deployment notification was successful. No run was created for this deployment.${skipDetail}`);
|
|
99
|
+
return {
|
|
100
|
+
environmentId: environment.id,
|
|
101
|
+
outcome: "success",
|
|
102
|
+
runId: void 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (skippedTriggers.length > 0) {
|
|
106
|
+
log.error(`\u{1F6AB} No run was created for this deployment.${skipDetail}`);
|
|
107
|
+
return {
|
|
108
|
+
failReason: "trigger-skipped",
|
|
109
|
+
outcome: "failed",
|
|
110
|
+
skippedTriggers
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
log.warn(skippedDeployment ? `\u26A0\uFE0F No run was created for this deployment.${skipDetail}` : `\u26A0\uFE0F No trigger matched the request. Please make sure there is at least one active test in the QA Wolf environment, or contact support.`);
|
|
114
|
+
return {
|
|
115
|
+
outcome: "skipped",
|
|
116
|
+
skippedDeployment
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!environment) {
|
|
120
|
+
log.error(`\u{1F6AB} No environment matched the request. Please contact support.`);
|
|
121
|
+
return {
|
|
122
|
+
failReason: "no-environment",
|
|
123
|
+
outcome: "failed"
|
|
124
|
+
};
|
|
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
|
+
}
|
|
134
|
+
const failedTrigger = results.find((result) => "failureReason" in result);
|
|
135
|
+
if (failedTrigger) {
|
|
136
|
+
log.error(`\u{1F6AB} Run could not be created. Reason: "${failedTrigger.failureReason}". Please contact support.`);
|
|
137
|
+
return {
|
|
138
|
+
failReason: "run-could-not-be-created",
|
|
139
|
+
outcome: "failed"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const duplicateSuite = results.find((result) => "duplicateSuiteId" in result);
|
|
143
|
+
if (duplicateSuite) {
|
|
144
|
+
log.info(`\u2705 Deployment notification was successful. Previous run ID: ${duplicateSuite.duplicateSuiteId} already exists for this deployment.`);
|
|
145
|
+
return {
|
|
146
|
+
environmentId: environment.id,
|
|
147
|
+
outcome: "success",
|
|
148
|
+
runId: duplicateSuite.duplicateSuiteId
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const successfulTrigger = results.find((result) => "createdSuiteId" in result);
|
|
152
|
+
if (!successfulTrigger) {
|
|
153
|
+
log.error(`\u{1F6AB} Run could not be created. Please contact support.`);
|
|
154
|
+
return {
|
|
155
|
+
failReason: "run-could-not-be-created",
|
|
156
|
+
outcome: "failed"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
log.info(`\u2705 Deployment notification was successful. Run created with ID: ${successfulTrigger.createdSuiteId}`);
|
|
160
|
+
return {
|
|
161
|
+
environmentId: environment.id,
|
|
162
|
+
outcome: "success",
|
|
163
|
+
runId: successfulTrigger.createdSuiteId
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
__name(handleSuccessResponse, "handleSuccessResponse");
|
|
167
|
+
|
|
168
|
+
// src/lib/sdk/domain/attemptDeploy/mapDeploySuccessResponse.ts
|
|
169
|
+
function mapSuiteResult(result) {
|
|
170
|
+
if ("created_suite_id" in result) {
|
|
171
|
+
return {
|
|
172
|
+
createdSuiteId: result.created_suite_id,
|
|
173
|
+
triggerId: result.trigger_id
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if ("failure_reason" in result) {
|
|
177
|
+
return {
|
|
178
|
+
failureReason: result.failure_reason,
|
|
179
|
+
triggerId: result.trigger_id
|
|
180
|
+
};
|
|
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
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
triggerId: result.trigger_id,
|
|
191
|
+
unrecognized: true
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
__name(mapSuiteResult, "mapSuiteResult");
|
|
195
|
+
function mapDeploySuccessResponse(wire) {
|
|
196
|
+
return {
|
|
197
|
+
environment: wire.environment,
|
|
198
|
+
results: wire.results.map(mapSuiteResult),
|
|
199
|
+
skippedDeployment: wire.skipped_deployment && {
|
|
200
|
+
message: wire.skipped_deployment.message,
|
|
201
|
+
reason: wire.skipped_deployment.reason
|
|
202
|
+
},
|
|
203
|
+
skippedTriggers: wire.skipped_triggers?.map((trigger) => ({
|
|
204
|
+
environmentId: trigger.environment_id,
|
|
205
|
+
message: trigger.message,
|
|
206
|
+
reason: trigger.reason,
|
|
207
|
+
triggerId: trigger.trigger_id
|
|
208
|
+
}))
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
__name(mapDeploySuccessResponse, "mapDeploySuccessResponse");
|
|
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
|
+
|
|
79
261
|
// src/lib/sdk/domain/attemptDeploy/index.ts
|
|
80
262
|
async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
81
263
|
const { fetch: localFetch, log } = deps;
|
|
264
|
+
let response;
|
|
265
|
+
let bodyText;
|
|
82
266
|
try {
|
|
83
|
-
|
|
267
|
+
response = await localFetch(new URL("/api/webhooks/deploy_success", apiConfig.serviceBase), {
|
|
84
268
|
body: JSON.stringify({
|
|
85
269
|
branch: config.branch,
|
|
86
270
|
commit_url: config.commitUrl,
|
|
@@ -109,8 +293,7 @@ async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
|
109
293
|
signal: AbortSignal.timeout(5 * 60 * 1e3)
|
|
110
294
|
});
|
|
111
295
|
if (!response.ok) return handleErrorResponse(response, log);
|
|
112
|
-
|
|
113
|
-
return handleSuccessResponse(data, log);
|
|
296
|
+
bodyText = await response.text();
|
|
114
297
|
} catch {
|
|
115
298
|
log.error(`\u{1F6AB} Network error from deploy_success, aborting.`);
|
|
116
299
|
return {
|
|
@@ -119,8 +302,29 @@ async function attemptNotifyDeploy(deps, apiConfig, config) {
|
|
|
119
302
|
outcome: "aborted"
|
|
120
303
|
};
|
|
121
304
|
}
|
|
305
|
+
let rawBody;
|
|
306
|
+
try {
|
|
307
|
+
rawBody = JSON.parse(bodyText);
|
|
308
|
+
} catch {
|
|
309
|
+
return abortInvalidResponseBody(log, "the response body was not JSON");
|
|
310
|
+
}
|
|
311
|
+
const wire = parseDeploySuccessWireResponse(rawBody);
|
|
312
|
+
if (wire === "invalid") {
|
|
313
|
+
return abortInvalidResponseBody(log, "the response body did not match the expected shape");
|
|
314
|
+
}
|
|
315
|
+
const data = mapDeploySuccessResponse(wire);
|
|
316
|
+
return handleSuccessResponse(data, log);
|
|
122
317
|
}
|
|
123
318
|
__name(attemptNotifyDeploy, "attemptNotifyDeploy");
|
|
319
|
+
function abortInvalidResponseBody(log, detail) {
|
|
320
|
+
log.error(`\u{1F6AB} Invalid response from deploy_success (${detail}), aborting.`);
|
|
321
|
+
return {
|
|
322
|
+
abortReason: "invalid-response-body",
|
|
323
|
+
httpStatus: void 0,
|
|
324
|
+
outcome: "aborted"
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
__name(abortInvalidResponseBody, "abortInvalidResponseBody");
|
|
124
328
|
async function handleErrorResponse(response, log) {
|
|
125
329
|
const status = response.status;
|
|
126
330
|
let abortReason;
|
|
@@ -141,68 +345,6 @@ async function handleErrorResponse(response, log) {
|
|
|
141
345
|
};
|
|
142
346
|
}
|
|
143
347
|
__name(handleErrorResponse, "handleErrorResponse");
|
|
144
|
-
async function handleSuccessResponse(data, log) {
|
|
145
|
-
const { environment, results } = data;
|
|
146
|
-
const isPreviewDeployment = environment && !environment.isStatic;
|
|
147
|
-
if (environment) {
|
|
148
|
-
const logPrefix = environment.isStatic ? "Static environment" : "Preview environment";
|
|
149
|
-
log.info(`${logPrefix}: ${environment.alias} (id: ${environment.id})`);
|
|
150
|
-
}
|
|
151
|
-
if (results.length === 0) {
|
|
152
|
-
if (isPreviewDeployment) {
|
|
153
|
-
log.info(`\u2705 Deployment notification was successful. No run was created for this deployment.`);
|
|
154
|
-
return {
|
|
155
|
-
environmentId: environment.id,
|
|
156
|
-
outcome: "success",
|
|
157
|
-
runId: void 0
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
log.error(`\u{1F6AB} No trigger matched the request. Please make sure there is at least one active test in the QA Wolf environment, or contact support.`);
|
|
161
|
-
return {
|
|
162
|
-
failReason: "no-matched-trigger",
|
|
163
|
-
outcome: "failed"
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
if (!environment) {
|
|
167
|
-
log.error(`\u{1F6AB} No environment matched the request. Please contact support.`);
|
|
168
|
-
return {
|
|
169
|
-
failReason: "no-environment",
|
|
170
|
-
outcome: "failed"
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
const failedTrigger = results.find((t) => "failure_reason" in t);
|
|
174
|
-
if (failedTrigger) {
|
|
175
|
-
log.error(`\u{1F6AB} Run could not be created. Reason: "${failedTrigger.failure_reason}". Please contact support.`);
|
|
176
|
-
return {
|
|
177
|
-
failReason: "run-could-not-be-created",
|
|
178
|
-
outcome: "failed"
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const duplicateSuite = results.find((t) => "duplicate_suite_id" in t);
|
|
182
|
-
if (duplicateSuite) {
|
|
183
|
-
log.info(`\u2705 Deployment notification was successful. Previous run ID: ${duplicateSuite.duplicate_suite_id} already exists for this deployment.`);
|
|
184
|
-
return {
|
|
185
|
-
environmentId: environment.id,
|
|
186
|
-
outcome: "success",
|
|
187
|
-
runId: duplicateSuite.duplicate_suite_id
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
const successfulTrigger = results.find((t) => "created_suite_id" in t);
|
|
191
|
-
if (!successfulTrigger) {
|
|
192
|
-
log.error(`\u{1F6AB} No trigger matched the request. Please contact support.`);
|
|
193
|
-
return {
|
|
194
|
-
failReason: "no-matched-trigger",
|
|
195
|
-
outcome: "failed"
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
log.info(`\u2705 Deployment notification was successful. Run created with ID: ${successfulTrigger.created_suite_id}`);
|
|
199
|
-
return {
|
|
200
|
-
environmentId: environment.id,
|
|
201
|
-
outcome: "success",
|
|
202
|
-
runId: successfulTrigger.created_suite_id
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
__name(handleSuccessResponse, "handleSuccessResponse");
|
|
206
348
|
|
|
207
349
|
// src/lib/api/generate-signed-url-for-run-inputs-executables.ts
|
|
208
350
|
async function callGenerateSignedUrlForRunInputsExecutablesStorage({ apiKey, serviceBase, userAgent }, { destinationFilePath }, { fetch: localFetch, log }) {
|
|
@@ -607,7 +749,9 @@ ${urlInfo}`;
|
|
|
607
749
|
}
|
|
608
750
|
__name(buildSuccessLogMessage, "buildSuccessLogMessage");
|
|
609
751
|
function buildFailedLogMessage(status) {
|
|
610
|
-
const { blockingBugsCount,
|
|
752
|
+
const { blockingBugsCount, nonBlockingBugsCount, relevantRunWithBugsUrl, reproducedBugs } = status;
|
|
753
|
+
const blockingBugUrls = reproducedBugs.filter((bug) => bug.isBlocking).map((bug) => bug.applicationUrl);
|
|
754
|
+
const nonBlockingBugUrls = reproducedBugs.filter((bug) => !bug.isBlocking).map((bug) => bug.applicationUrl);
|
|
611
755
|
const blockingBugList = blockingBugUrls.map((url) => `
|
|
612
756
|
\u2022 ${url}`).join("");
|
|
613
757
|
const nonBlockingBugList = nonBlockingBugUrls.map((url) => `
|