squally-reporter 0.2.2 → 0.3.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 +39 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +282 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,45 @@ reporter: [
|
|
|
50
50
|
That's it - run your tests as usual (`npx playwright test`) and results will
|
|
51
51
|
be sent to Squally after the run finishes.
|
|
52
52
|
|
|
53
|
+
## Screenshot/trace upload
|
|
54
|
+
|
|
55
|
+
If your `playwright.config.ts` captures screenshots or traces, the reporter
|
|
56
|
+
uploads them to Squally so they show up next to the failing test in the
|
|
57
|
+
dashboard - no separate setup beyond the two env vars above.
|
|
58
|
+
|
|
59
|
+
**Only from attempts worth looking at:** screenshots/traces are uploaded
|
|
60
|
+
only for attempts that **failed**, or that **needed a retry** (even if that
|
|
61
|
+
retry then passed) - never for an attempt that passed on its first try,
|
|
62
|
+
regardless of how `screenshot`/`trace` are configured. Nobody looks at a
|
|
63
|
+
screenshot of a test that just passed, and uploading one anyway would only
|
|
64
|
+
eat into your organization's storage for no benefit.
|
|
65
|
+
|
|
66
|
+
**You need to actually enable capture in `playwright.config.ts`** - without
|
|
67
|
+
it, Playwright doesn't produce any attachments, so there's nothing for the
|
|
68
|
+
reporter to find or upload (no error either way, it's a silent no-op):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
export default defineConfig({
|
|
72
|
+
use: {
|
|
73
|
+
screenshot: "only-on-failure",
|
|
74
|
+
trace: "retain-on-failure",
|
|
75
|
+
},
|
|
76
|
+
// ...
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Limits:**
|
|
81
|
+
- 50 MB per individual file. Larger files are skipped (logged as a
|
|
82
|
+
`console.warn`, the test result itself is still reported as usual).
|
|
83
|
+
- A total storage cap per organization, depending on your Squally plan.
|
|
84
|
+
Once reached, further uploads are skipped the same way until you either
|
|
85
|
+
free up space or upgrade.
|
|
86
|
+
|
|
87
|
+
Upload is best-effort and fully non-blocking, same principle as the rest of
|
|
88
|
+
this reporter - a failed or skipped upload never fails your test run or
|
|
89
|
+
changes its exit code, it only logs a `console.warn` explaining what
|
|
90
|
+
happened.
|
|
91
|
+
|
|
53
92
|
## Tested revision (optional)
|
|
54
93
|
|
|
55
94
|
If your test-automation code lives in the same repo as the application it
|
package/dist/index.d.ts
CHANGED
|
@@ -9,9 +9,12 @@ export default class SquallyReporter implements Reporter {
|
|
|
9
9
|
private enabled;
|
|
10
10
|
private startedAt;
|
|
11
11
|
private results;
|
|
12
|
+
private artifacts;
|
|
12
13
|
constructor(options?: SquallyReporterOptions);
|
|
13
14
|
onBegin(_config: FullConfig, _suite: Suite): void;
|
|
14
15
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
15
16
|
onEnd(result: FullResult): Promise<void>;
|
|
17
|
+
private uploadArtifacts;
|
|
18
|
+
private presignAndUpload;
|
|
16
19
|
}
|
|
17
20
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
7
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
10
|
const undici_1 = require("undici");
|
|
9
11
|
// Maps Playwright's more granular statuses down to the three
|
|
@@ -143,6 +145,29 @@ function detectEventType() {
|
|
|
143
145
|
function toPosixPath(p) {
|
|
144
146
|
return p.split(node_path_1.default.sep).join("/");
|
|
145
147
|
}
|
|
148
|
+
// Playwright own assertion errors (expect(...).toHaveText(...) etc.) are
|
|
149
|
+
// colorized with ANSI escape codes in error.message/error.stack - useful in
|
|
150
|
+
// a terminal, but rendered as visible garbage once stored as plain text and
|
|
151
|
+
// shown in Squally UI. Test code that colors its own console output (e.g.
|
|
152
|
+
// via chalk/picocolors) can do the same to stdout/stderr. Stripped here,
|
|
153
|
+
// before anything is ever sent - not server/UI-side - so the raw escape
|
|
154
|
+
// codes never land in the database in the first place, and every later
|
|
155
|
+
// consumer (the existing keyword-based error categorization, the planned
|
|
156
|
+
// AI analysis, a fingerprint cache over normalized error signatures)
|
|
157
|
+
// does not have to strip them again itself.
|
|
158
|
+
//
|
|
159
|
+
// Matches the CSI form ANSI color/style codes actually take: an ESC
|
|
160
|
+
// character (code point 27), then an opening bracket, then any run of
|
|
161
|
+
// digits/semicolons, then one letter (e.g. ESC, bracket, 3, 1, m for red).
|
|
162
|
+
// No new dependency (e.g. strip-ansi) for a one-line regex, same reasoning
|
|
163
|
+
// as toPosixPath/the upload worker pool above. Built via String.fromCharCode
|
|
164
|
+
// rather than a hex escape directly in the source: this file has to stay
|
|
165
|
+
// plain, readable ASCII - a raw ESC byte pasted into source code is
|
|
166
|
+
// invisible in most editors/diffs and easy to mistake for empty content.
|
|
167
|
+
function stripAnsi(value) {
|
|
168
|
+
const ansiPattern = new RegExp(String.fromCharCode(27) + String.fromCharCode(92) + "[[0-9;]*[a-zA-Z]", "g");
|
|
169
|
+
return value.replace(ansiPattern, "");
|
|
170
|
+
}
|
|
146
171
|
// Playwright captures stdout/stderr as two separate arrays (each internally
|
|
147
172
|
// chronological, but not merged with each other), so a true byte-accurate
|
|
148
173
|
// interleaving between the two streams isn't available here - stdout lines
|
|
@@ -152,13 +177,78 @@ function toPosixPath(p) {
|
|
|
152
177
|
function formatConsoleOutput(result) {
|
|
153
178
|
const lines = [];
|
|
154
179
|
for (const chunk of result.stdout) {
|
|
155
|
-
lines.push(`[log] ${chunk.toString().replace(/\s+$/, "")}`);
|
|
180
|
+
lines.push(`[log] ${stripAnsi(chunk.toString()).replace(/\s+$/, "")}`);
|
|
156
181
|
}
|
|
157
182
|
for (const chunk of result.stderr) {
|
|
158
|
-
lines.push(`[error] ${chunk.toString().replace(/\s+$/, "")}`);
|
|
183
|
+
lines.push(`[error] ${stripAnsi(chunk.toString()).replace(/\s+$/, "")}`);
|
|
159
184
|
}
|
|
160
185
|
return lines.length > 0 ? lines.join("\n") : null;
|
|
161
186
|
}
|
|
187
|
+
// Presigned upload URLs are valid for 15 minutes (server-side, see
|
|
188
|
+
// squally-app's lib/r2.ts). Uploading fully sequentially could get close to
|
|
189
|
+
// that window for a suite with a very large number of failing/retried
|
|
190
|
+
// tests, but firing everything at once isn't right either - CI runners
|
|
191
|
+
// often have modest outbound bandwidth/file-descriptor limits, and a
|
|
192
|
+
// thundering herd of large trace uploads competes with the test run itself
|
|
193
|
+
// for those resources. 5 concurrent uploads is a pragmatic middle ground:
|
|
194
|
+
// fast enough that even a few hundred artifacts finish in well under 15
|
|
195
|
+
// minutes, bounded enough not to overwhelm a typical CI runner.
|
|
196
|
+
const UPLOAD_CONCURRENCY = 5;
|
|
197
|
+
// No new dependency for this - a fixed-size worker pool that keeps pulling
|
|
198
|
+
// the next item until the list is exhausted is ~10 lines, not worth pulling
|
|
199
|
+
// in e.g. p-limit for.
|
|
200
|
+
async function runWithConcurrencyLimit(items, limit, worker) {
|
|
201
|
+
let nextIndex = 0;
|
|
202
|
+
async function runNext() {
|
|
203
|
+
const currentIndex = nextIndex++;
|
|
204
|
+
if (currentIndex >= items.length)
|
|
205
|
+
return;
|
|
206
|
+
await worker(items[currentIndex]);
|
|
207
|
+
return runNext();
|
|
208
|
+
}
|
|
209
|
+
const workerCount = Math.min(limit, items.length);
|
|
210
|
+
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
211
|
+
}
|
|
212
|
+
// Only screenshots/traces from attempts worth looking at get uploaded -
|
|
213
|
+
// deliberately independent of how the customer configured
|
|
214
|
+
// screenshot/trace in playwright.config.ts. A "screenshot: 'on'" config
|
|
215
|
+
// would otherwise attach an image to every single passing test too: nobody
|
|
216
|
+
// opens a screenshot of a test that just passed, and every one of those
|
|
217
|
+
// uploads eats into the organization's artifact cap (Free tier: 1 GB) for
|
|
218
|
+
// zero benefit. "failed" already covers Playwright's timedOut/interrupted
|
|
219
|
+
// (mapStatus above collapses those down) - retryCount > 0 additionally
|
|
220
|
+
// covers a passed-on-retry attempt, since seeing what an earlier, failing
|
|
221
|
+
// attempt looked like is still useful even if the test eventually passed.
|
|
222
|
+
function attemptQualifiesForArtifacts(mappedStatus, retryCount) {
|
|
223
|
+
return mappedStatus === "failed" || retryCount > 0;
|
|
224
|
+
}
|
|
225
|
+
// A control character as separator, not an empty string or a slash -
|
|
226
|
+
// testName/filePath can contain arbitrary characters, and this key only
|
|
227
|
+
// needs to be unique, not human-readable (it is purely an internal lookup
|
|
228
|
+
// back to the local CollectedArtifact once the server round-trips these
|
|
229
|
+
// identity fields).
|
|
230
|
+
function artifactKey(testName, filePath, retryCount, type) {
|
|
231
|
+
return [testName, filePath, String(retryCount), type].join(String.fromCharCode(1));
|
|
232
|
+
}
|
|
233
|
+
function skipReasonMessage(reason) {
|
|
234
|
+
if (reason === "file_too_large")
|
|
235
|
+
return "file_too_large (limit 50 MB)";
|
|
236
|
+
if (reason === "cap_exceeded")
|
|
237
|
+
return "cap_exceeded (storage limit reached, upgrade your plan)";
|
|
238
|
+
if (reason === "not_found")
|
|
239
|
+
return "not_found (test result not found on server)";
|
|
240
|
+
return reason;
|
|
241
|
+
}
|
|
242
|
+
function confirmReasonMessage(reason) {
|
|
243
|
+
if (reason === "not_uploaded")
|
|
244
|
+
return "not_uploaded (upload did not reach storage)";
|
|
245
|
+
if (reason === "not_found")
|
|
246
|
+
return "not_found (test result not found on server)";
|
|
247
|
+
return reason;
|
|
248
|
+
}
|
|
249
|
+
function artifactLabel(type) {
|
|
250
|
+
return type === "screenshot" ? "Screenshot" : "Trace";
|
|
251
|
+
}
|
|
162
252
|
class SquallyReporter {
|
|
163
253
|
constructor(options = {}) {
|
|
164
254
|
this.apiUrl = "";
|
|
@@ -169,6 +259,7 @@ class SquallyReporter {
|
|
|
169
259
|
this.enabled = false;
|
|
170
260
|
this.startedAt = new Date();
|
|
171
261
|
this.results = [];
|
|
262
|
+
this.artifacts = [];
|
|
172
263
|
const apiUrl = options.apiUrl ?? process.env.SQUALLY_API_URL;
|
|
173
264
|
const apiKey = options.apiKey ?? process.env.SQUALLY_API_KEY;
|
|
174
265
|
if (!apiUrl || !apiKey) {
|
|
@@ -194,16 +285,52 @@ class SquallyReporter {
|
|
|
194
285
|
if (!this.enabled)
|
|
195
286
|
return;
|
|
196
287
|
const error = result.errors[0];
|
|
288
|
+
const testName = test.titlePath().slice(1).join(" > ");
|
|
289
|
+
const filePath = toPosixPath(node_path_1.default.relative(process.cwd(), test.location.file));
|
|
290
|
+
const status = mapStatus(result.status);
|
|
197
291
|
this.results.push({
|
|
198
|
-
testName
|
|
199
|
-
filePath
|
|
200
|
-
status
|
|
292
|
+
testName,
|
|
293
|
+
filePath,
|
|
294
|
+
status,
|
|
201
295
|
durationMs: result.duration,
|
|
202
296
|
retryCount: result.retry,
|
|
203
|
-
errorMessage: error?.message
|
|
204
|
-
errorStack: error?.stack
|
|
297
|
+
errorMessage: error?.message ? stripAnsi(error.message) : null,
|
|
298
|
+
errorStack: error?.stack ? stripAnsi(error.stack) : null,
|
|
205
299
|
consoleOutput: formatConsoleOutput(result),
|
|
206
300
|
});
|
|
301
|
+
if (!attemptQualifiesForArtifacts(status, result.retry))
|
|
302
|
+
return;
|
|
303
|
+
for (const attachment of result.attachments) {
|
|
304
|
+
if (attachment.name !== "screenshot" && attachment.name !== "trace")
|
|
305
|
+
continue;
|
|
306
|
+
// Attachments can be in-memory (body, no path) when added manually via
|
|
307
|
+
// testInfo.attach() - only file-backed ones (screenshot/trace always
|
|
308
|
+
// are, when Playwright itself captures them) can be uploaded here.
|
|
309
|
+
if (!attachment.path)
|
|
310
|
+
continue;
|
|
311
|
+
// Synchronous, not fs/promises - onTestEnd's return value isn't
|
|
312
|
+
// awaited by Playwright (Reporter['onTestEnd'] is typed as void, not
|
|
313
|
+
// Promise<void>), so an async stat call here would race with
|
|
314
|
+
// onEnd() reading this.artifacts before it resolved.
|
|
315
|
+
let sizeBytes;
|
|
316
|
+
try {
|
|
317
|
+
sizeBytes = (0, node_fs_1.statSync)(attachment.path).size;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// File already gone (e.g. cleaned up by a parallel worker) -
|
|
321
|
+
// nothing to upload, silently skip rather than warn for what's an
|
|
322
|
+
// expected, harmless race in some CI setups.
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
this.artifacts.push({
|
|
326
|
+
testName,
|
|
327
|
+
filePath,
|
|
328
|
+
retryCount: result.retry,
|
|
329
|
+
type: attachment.name,
|
|
330
|
+
localPath: attachment.path,
|
|
331
|
+
sizeBytes,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
207
334
|
}
|
|
208
335
|
async onEnd(result) {
|
|
209
336
|
if (!this.enabled)
|
|
@@ -242,26 +369,166 @@ class SquallyReporter {
|
|
|
242
369
|
// copy when passing our Agent as the dispatcher.
|
|
243
370
|
const dispatcher = new undici_1.Agent({ keepAliveTimeout: 1, keepAliveMaxTimeout: 1 });
|
|
244
371
|
try {
|
|
245
|
-
|
|
372
|
+
let runId = null;
|
|
373
|
+
try {
|
|
374
|
+
const response = await (0, undici_1.fetch)(`${this.apiUrl}/api/ingest`, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json",
|
|
378
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify(payload),
|
|
381
|
+
dispatcher,
|
|
382
|
+
});
|
|
383
|
+
if (!response.ok) {
|
|
384
|
+
// Never throw here - a failing reporter upload should not fail the
|
|
385
|
+
// whole CI run just because the dashboard was unreachable.
|
|
386
|
+
console.error(`[squally-reporter] Ingest failed: ${response.status} ${response.statusText}`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const body = (await response.json());
|
|
390
|
+
runId = body.runId ?? null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
console.error("[squally-reporter] Failed to reach Squally API:", err);
|
|
395
|
+
}
|
|
396
|
+
// Artifact upload is a separate, best-effort step layered on top of
|
|
397
|
+
// ingest, never a reason to fail it: it needs a runId (presign looks
|
|
398
|
+
// TestResults up by runId + identity fields, which only exist once
|
|
399
|
+
// ingest has actually inserted them), and there's nothing to do if
|
|
400
|
+
// this run had no failing/retried attempts with a screenshot/trace
|
|
401
|
+
// attached (attemptQualifiesForArtifacts already filtered that in
|
|
402
|
+
// onTestEnd) - no reason to make an empty presign call.
|
|
403
|
+
if (runId && this.artifacts.length > 0) {
|
|
404
|
+
await this.uploadArtifacts(runId, dispatcher);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
await dispatcher.close();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Top-level safety net - presignAndUpload already handles presign/
|
|
412
|
+
// upload/confirm failures individually (see Aufgabe 5's requirement that
|
|
413
|
+
// none of this ever affects the test suite's exit code), but this catch
|
|
414
|
+
// still exists in case something in there throws in a way that wasn't
|
|
415
|
+
// anticipated. The whole artifact feature must be additive: worst case,
|
|
416
|
+
// some screenshots/traces don't make it to Squally, but the customer's
|
|
417
|
+
// CI run itself is never at risk.
|
|
418
|
+
async uploadArtifacts(runId, dispatcher) {
|
|
419
|
+
try {
|
|
420
|
+
await this.presignAndUpload(runId, dispatcher);
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
console.warn("[squally-reporter] Artifact upload failed unexpectedly:", err);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async presignAndUpload(runId, dispatcher) {
|
|
427
|
+
const presignArtifacts = this.artifacts.map((artifact) => ({
|
|
428
|
+
testName: artifact.testName,
|
|
429
|
+
filePath: artifact.filePath,
|
|
430
|
+
retryCount: artifact.retryCount,
|
|
431
|
+
type: artifact.type,
|
|
432
|
+
sizeBytes: artifact.sizeBytes,
|
|
433
|
+
}));
|
|
434
|
+
let presignResults;
|
|
435
|
+
try {
|
|
436
|
+
const response = await (0, undici_1.fetch)(`${this.apiUrl}/api/artifacts/presign`, {
|
|
246
437
|
method: "POST",
|
|
247
438
|
headers: {
|
|
248
439
|
"Content-Type": "application/json",
|
|
249
440
|
Authorization: `Bearer ${this.apiKey}`,
|
|
250
441
|
},
|
|
251
|
-
body: JSON.stringify(
|
|
442
|
+
body: JSON.stringify({ runId, artifacts: presignArtifacts }),
|
|
252
443
|
dispatcher,
|
|
253
444
|
});
|
|
254
445
|
if (!response.ok) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
console.error(`[squally-reporter] Ingest failed: ${response.status} ${response.statusText}`);
|
|
446
|
+
console.warn(`[squally-reporter] Artifact presign failed: ${response.status} ${response.statusText} - skipping artifact upload for this run`);
|
|
447
|
+
return;
|
|
258
448
|
}
|
|
449
|
+
const body = (await response.json());
|
|
450
|
+
presignResults = body.results;
|
|
259
451
|
}
|
|
260
452
|
catch (err) {
|
|
261
|
-
console.
|
|
453
|
+
console.warn("[squally-reporter] Artifact presign request failed:", err);
|
|
454
|
+
return;
|
|
262
455
|
}
|
|
263
|
-
|
|
264
|
-
|
|
456
|
+
// Maps a presign result back to the local file it came from -
|
|
457
|
+
// testName/filePath/retryCount/type is exactly what was sent, so it's
|
|
458
|
+
// guaranteed to match one of this.artifacts (barring a server bug),
|
|
459
|
+
// but a missing lookup is handled defensively below instead of assumed.
|
|
460
|
+
const artifactsByKey = new Map(this.artifacts.map((artifact) => [
|
|
461
|
+
artifactKey(artifact.testName, artifact.filePath, artifact.retryCount, artifact.type),
|
|
462
|
+
artifact,
|
|
463
|
+
]));
|
|
464
|
+
const uploads = [];
|
|
465
|
+
for (const item of presignResults) {
|
|
466
|
+
const local = artifactsByKey.get(artifactKey(item.testName, item.filePath ?? "", item.retryCount, item.type));
|
|
467
|
+
if (!local)
|
|
468
|
+
continue;
|
|
469
|
+
if (item.skip) {
|
|
470
|
+
console.warn(`[squally-reporter] ${artifactLabel(item.type)} for '${item.testName}' skipped: ${skipReasonMessage(item.reason)}`);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
uploads.push({ artifact: local, uploadUrl: item.uploadUrl });
|
|
474
|
+
}
|
|
475
|
+
if (uploads.length === 0)
|
|
476
|
+
return;
|
|
477
|
+
// See UPLOAD_CONCURRENCY's own comment above for why this is bounded
|
|
478
|
+
// rather than either fully sequential or fully parallel.
|
|
479
|
+
await runWithConcurrencyLimit(uploads, UPLOAD_CONCURRENCY, async ({ artifact, uploadUrl }) => {
|
|
480
|
+
try {
|
|
481
|
+
const fileBody = await (0, promises_1.readFile)(artifact.localPath);
|
|
482
|
+
const response = await (0, undici_1.fetch)(uploadUrl, {
|
|
483
|
+
method: "PUT",
|
|
484
|
+
body: fileBody,
|
|
485
|
+
dispatcher,
|
|
486
|
+
});
|
|
487
|
+
if (!response.ok) {
|
|
488
|
+
console.warn(`[squally-reporter] Upload failed for ${artifact.type} of '${artifact.testName}': ${response.status} ${response.statusText}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
console.warn(`[squally-reporter] Upload failed for ${artifact.type} of '${artifact.testName}':`, err);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// Confirm every artifact that was actually presigned (not the ones
|
|
496
|
+
// presign already skipped - those never got a key, and re-checking
|
|
497
|
+
// them here would just produce a confusing second "not_uploaded"
|
|
498
|
+
// warning for something that was never attempted in the first place).
|
|
499
|
+
// Confirm's own headObject check is the source of truth for whether an
|
|
500
|
+
// upload really landed - deliberately not tracking PUT success/failure
|
|
501
|
+
// above to decide what to confirm, since a PUT that reported success
|
|
502
|
+
// but didn't truly land (or vice versa) should still be caught here.
|
|
503
|
+
const confirmArtifacts = uploads.map(({ artifact }) => ({
|
|
504
|
+
testName: artifact.testName,
|
|
505
|
+
filePath: artifact.filePath,
|
|
506
|
+
retryCount: artifact.retryCount,
|
|
507
|
+
type: artifact.type,
|
|
508
|
+
}));
|
|
509
|
+
try {
|
|
510
|
+
const response = await (0, undici_1.fetch)(`${this.apiUrl}/api/artifacts/confirm`, {
|
|
511
|
+
method: "POST",
|
|
512
|
+
headers: {
|
|
513
|
+
"Content-Type": "application/json",
|
|
514
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
515
|
+
},
|
|
516
|
+
body: JSON.stringify({ runId, artifacts: confirmArtifacts }),
|
|
517
|
+
dispatcher,
|
|
518
|
+
});
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
console.warn(`[squally-reporter] Artifact confirm failed: ${response.status} ${response.statusText}`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const body = (await response.json());
|
|
524
|
+
for (const item of body.results) {
|
|
525
|
+
if (item.confirmed)
|
|
526
|
+
continue;
|
|
527
|
+
console.warn(`[squally-reporter] ${artifactLabel(item.type)} for '${item.testName}' not confirmed: ${confirmReasonMessage(item.reason)}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
console.warn("[squally-reporter] Artifact confirm request failed:", err);
|
|
265
532
|
}
|
|
266
533
|
}
|
|
267
534
|
}
|