@sentinelqa/uploader 0.1.5 → 0.1.6
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 +18 -5
- package/dist/cli.js +82 -76
- package/dist/httpClient.js +92 -0
- package/dist/output.js +33 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ If you want to run without installing, use:
|
|
|
26
26
|
npx @sentinelqa/uploader playwright
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
### Minimal GitLab CI
|
|
29
|
+
### Minimal GitLab CI (preserves test exit code)
|
|
30
30
|
|
|
31
31
|
```yaml
|
|
32
32
|
e2e:
|
|
@@ -34,8 +34,12 @@ e2e:
|
|
|
34
34
|
script:
|
|
35
35
|
- npm ci
|
|
36
36
|
- npx playwright install --with-deps
|
|
37
|
-
-
|
|
37
|
+
- set +e
|
|
38
|
+
- npx playwright test --reporter=line,json --output=test-results
|
|
39
|
+
- TEST_EXIT=$?
|
|
40
|
+
- export SENTINEL_TEST_EXIT_CODE=$TEST_EXIT
|
|
38
41
|
- npx @sentinelqa/uploader playwright
|
|
42
|
+
- exit $TEST_EXIT
|
|
39
43
|
artifacts:
|
|
40
44
|
when: always
|
|
41
45
|
paths:
|
|
@@ -43,7 +47,7 @@ e2e:
|
|
|
43
47
|
- test-results/
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
### Minimal GitHub Actions
|
|
50
|
+
### Minimal GitHub Actions (preserves test exit code)
|
|
47
51
|
|
|
48
52
|
```yaml
|
|
49
53
|
name: E2E
|
|
@@ -58,14 +62,20 @@ jobs:
|
|
|
58
62
|
node-version: "20"
|
|
59
63
|
- run: npm ci
|
|
60
64
|
- run: npx playwright install --with-deps
|
|
61
|
-
- run:
|
|
62
|
-
|
|
65
|
+
- run: |
|
|
66
|
+
set +e
|
|
67
|
+
npx playwright test --reporter=line,json --output=test-results
|
|
68
|
+
TEST_EXIT=$?
|
|
69
|
+
echo "SENTINEL_TEST_EXIT_CODE=$TEST_EXIT" >> $GITHUB_ENV
|
|
70
|
+
npx @sentinelqa/uploader playwright
|
|
71
|
+
exit $TEST_EXIT
|
|
63
72
|
```
|
|
64
73
|
|
|
65
74
|
## Required Environment Variables
|
|
66
75
|
|
|
67
76
|
- `SENTINEL_TOKEN` (project ingest token)
|
|
68
77
|
- `SENTINEL_URL` (optional; defaults to `https://app.sentinelqa.com`)
|
|
78
|
+
- `SENTINEL_TEST_EXIT_CODE` (optional; used to preserve CI test exit code)
|
|
69
79
|
|
|
70
80
|
## Optional: BYO S3 (Advanced)
|
|
71
81
|
|
|
@@ -92,5 +102,8 @@ Set these to upload directly to your own bucket:
|
|
|
92
102
|
**No CI metadata**
|
|
93
103
|
- The uploader detects GitLab or GitHub. If running locally, set the CI env vars or run in CI.
|
|
94
104
|
|
|
105
|
+
**No failed tests**
|
|
106
|
+
- Ensure `@playwright/test` is installed and the JSON reporter is enabled.
|
|
107
|
+
|
|
95
108
|
**BYO uploads failing**
|
|
96
109
|
- Verify `SENTINEL_S3_*` values and permissions.
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
10
|
+
const httpClient_1 = require("./httpClient");
|
|
11
|
+
const output_1 = require("./output");
|
|
10
12
|
const DEFAULT_APP_URL = "https://app.sentinelqa.com";
|
|
11
13
|
const DEFAULT_JSON_PATH = "playwright-report/report.json";
|
|
12
14
|
const DEFAULT_PLAYWRIGHT_REPORT_DIR = "playwright-report";
|
|
@@ -15,9 +17,19 @@ const readEnv = (key) => {
|
|
|
15
17
|
const value = process.env[key];
|
|
16
18
|
return value && value.trim().length > 0 ? value.trim() : null;
|
|
17
19
|
};
|
|
20
|
+
const getPreservedExitCode = () => {
|
|
21
|
+
const raw = readEnv("SENTINEL_TEST_EXIT_CODE");
|
|
22
|
+
if (!raw)
|
|
23
|
+
return null;
|
|
24
|
+
const num = Number.parseInt(raw, 10);
|
|
25
|
+
if (!Number.isFinite(num) || num <= 0)
|
|
26
|
+
return null;
|
|
27
|
+
return num;
|
|
28
|
+
};
|
|
18
29
|
const fail = (message) => {
|
|
19
30
|
console.error(`Error: ${message}`);
|
|
20
|
-
|
|
31
|
+
const preserved = getPreservedExitCode();
|
|
32
|
+
process.exit(preserved ?? 1);
|
|
21
33
|
};
|
|
22
34
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
35
|
const withRetry = async (fn, label) => {
|
|
@@ -192,22 +204,24 @@ const computeRunStatus = (tests) => {
|
|
|
192
204
|
};
|
|
193
205
|
const bestEffortComplete = async (appUrl, ingestToken, runId, status, tests) => {
|
|
194
206
|
try {
|
|
195
|
-
await
|
|
207
|
+
const res = await (0, httpClient_1.requestJson)(`${appUrl}/api/runs/${runId}/complete`, {
|
|
196
208
|
method: "POST",
|
|
197
209
|
headers: {
|
|
198
|
-
"content-type": "application/json",
|
|
199
210
|
authorization: `Bearer ${ingestToken}`
|
|
200
211
|
},
|
|
201
|
-
body:
|
|
212
|
+
body: {
|
|
202
213
|
status,
|
|
203
214
|
finishedAt: new Date().toISOString(),
|
|
204
215
|
tests,
|
|
205
216
|
artifacts: []
|
|
206
|
-
}
|
|
207
|
-
});
|
|
217
|
+
}
|
|
218
|
+
}, 0);
|
|
219
|
+
if (res.status >= 400)
|
|
220
|
+
return null;
|
|
221
|
+
return res.body;
|
|
208
222
|
}
|
|
209
223
|
catch {
|
|
210
|
-
|
|
224
|
+
return null;
|
|
211
225
|
}
|
|
212
226
|
};
|
|
213
227
|
const detectGitLabEnv = () => {
|
|
@@ -375,25 +389,25 @@ const main = async () => {
|
|
|
375
389
|
if (!ci.workflow.id)
|
|
376
390
|
fail("Workflow ID is required.");
|
|
377
391
|
const appUrl = readEnv("SENTINEL_URL") || readEnv("APP_URL") || DEFAULT_APP_URL;
|
|
378
|
-
const createRes = await
|
|
392
|
+
const createRes = await (0, httpClient_1.requestJson)(`${appUrl}/api/runs`, {
|
|
379
393
|
method: "POST",
|
|
380
394
|
headers: {
|
|
381
|
-
"content-type": "application/json",
|
|
382
395
|
authorization: `Bearer ${ingestToken}`
|
|
383
396
|
},
|
|
384
|
-
body:
|
|
397
|
+
body: {
|
|
385
398
|
provider: ci.provider,
|
|
386
399
|
commit: ci.commit,
|
|
387
400
|
workflow: ci.workflow,
|
|
388
401
|
job: ci.job,
|
|
389
402
|
ci: ci.ci
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
fail(`POST /api/runs failed (${createRes.status}): ${body}`);
|
|
403
|
+
}
|
|
404
|
+
}, 2);
|
|
405
|
+
if (createRes.status >= 400) {
|
|
406
|
+
fail(`POST /api/runs failed (${createRes.status}): ${createRes.raw}`);
|
|
395
407
|
}
|
|
396
|
-
const
|
|
408
|
+
const runId = createRes.body?.runId;
|
|
409
|
+
const internalRunUrl = createRes.body?.internalRunUrl ||
|
|
410
|
+
`${appUrl}/runs/${runId}`;
|
|
397
411
|
if (!runId)
|
|
398
412
|
fail("Missing runId from /api/runs response.");
|
|
399
413
|
const uploadStart = Date.now();
|
|
@@ -450,26 +464,24 @@ const main = async () => {
|
|
|
450
464
|
}));
|
|
451
465
|
}
|
|
452
466
|
else {
|
|
453
|
-
const presignRes = await
|
|
467
|
+
const presignRes = await (0, httpClient_1.requestJson)(`${appUrl}/api/uploads/presign`, {
|
|
454
468
|
method: "POST",
|
|
455
469
|
headers: {
|
|
456
|
-
"content-type": "application/json",
|
|
457
470
|
authorization: `Bearer ${ingestToken}`
|
|
458
471
|
},
|
|
459
|
-
body:
|
|
472
|
+
body: {
|
|
460
473
|
items: finalArtifacts.map((artifact) => ({
|
|
461
474
|
relPath: artifact.objectKey,
|
|
462
475
|
contentType: artifact.contentType,
|
|
463
476
|
sizeBytes: artifact.sizeBytes,
|
|
464
477
|
kind: artifact.type
|
|
465
478
|
}))
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
fail(`POST /api/uploads/presign failed (${presignRes.status}): ${body}`);
|
|
479
|
+
}
|
|
480
|
+
}, 2);
|
|
481
|
+
if (presignRes.status >= 400) {
|
|
482
|
+
fail(`POST /api/uploads/presign failed (${presignRes.status}): ${presignRes.raw}`);
|
|
471
483
|
}
|
|
472
|
-
const presignData =
|
|
484
|
+
const presignData = presignRes.body;
|
|
473
485
|
const uploadMap = new Map((presignData.items || []).map((item) => [item.relPath, item]));
|
|
474
486
|
await withRetry(async () => {
|
|
475
487
|
for (const artifact of finalArtifacts) {
|
|
@@ -478,18 +490,11 @@ const main = async () => {
|
|
|
478
490
|
throw new Error(`Missing upload URL for ${artifact.objectKey}`);
|
|
479
491
|
}
|
|
480
492
|
await withRetry(async () => {
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
},
|
|
487
|
-
body,
|
|
488
|
-
// Required by Node.js fetch when streaming a request body
|
|
489
|
-
duplex: "half"
|
|
490
|
-
});
|
|
491
|
-
if (!res.ok) {
|
|
492
|
-
throw new Error(`Upload failed (${res.status}) for ${artifact.objectKey}`);
|
|
493
|
+
const stat = fs_1.default.statSync(artifact.filePath);
|
|
494
|
+
const stream = fs_1.default.createReadStream(artifact.filePath);
|
|
495
|
+
const result = await (0, httpClient_1.putFile)(item.uploadUrl, stream, artifact.contentType, stat.size);
|
|
496
|
+
if (result.status >= 400) {
|
|
497
|
+
throw new Error(`Upload failed (${result.status}) for ${artifact.objectKey}: ${result.raw}`);
|
|
493
498
|
}
|
|
494
499
|
}, `upload ${artifact.objectKey}`);
|
|
495
500
|
artifact.objectKey = item.objectKey;
|
|
@@ -502,17 +507,39 @@ const main = async () => {
|
|
|
502
507
|
}
|
|
503
508
|
}
|
|
504
509
|
catch (err) {
|
|
505
|
-
await bestEffortComplete(appUrl, ingestToken, runId, "failed", tests);
|
|
510
|
+
const fallback = await bestEffortComplete(appUrl, ingestToken, runId, "failed", tests);
|
|
511
|
+
if (fallback) {
|
|
512
|
+
const output = (0, output_1.formatCiOutput)({
|
|
513
|
+
failedCount: fallback.failedCount ??
|
|
514
|
+
tests.filter((t) => t.status === "failed").length,
|
|
515
|
+
totalCount: fallback.totalCount ?? tests.length,
|
|
516
|
+
internalRunUrl: fallback.internalRunUrl || internalRunUrl,
|
|
517
|
+
internalFirstFailureUrl: fallback.internalFirstFailureUrl || null,
|
|
518
|
+
shareRunUrl: fallback.shareRunUrl || null,
|
|
519
|
+
shareFirstFailureUrl: fallback.shareFirstFailureUrl || null
|
|
520
|
+
}, !process.env.NO_COLOR);
|
|
521
|
+
console.log(output);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
console.log(internalRunUrl);
|
|
525
|
+
}
|
|
526
|
+
console.error("Warning: Sentinel uploader failed to upload artifacts.");
|
|
506
527
|
fail(err?.message || String(err));
|
|
507
528
|
}
|
|
508
529
|
const uploadDurationMs = Date.now() - uploadStart;
|
|
509
|
-
|
|
530
|
+
console.log(JSON.stringify({
|
|
531
|
+
event: "uploader_summary",
|
|
532
|
+
runId,
|
|
533
|
+
tests: tests.length,
|
|
534
|
+
artifacts: artifacts.length,
|
|
535
|
+
uploadDurationMs
|
|
536
|
+
}));
|
|
537
|
+
const completeRes = await (0, httpClient_1.requestJson)(`${appUrl}/api/runs/${runId}/complete`, {
|
|
510
538
|
method: "POST",
|
|
511
539
|
headers: {
|
|
512
|
-
"content-type": "application/json",
|
|
513
540
|
authorization: `Bearer ${ingestToken}`
|
|
514
541
|
},
|
|
515
|
-
body:
|
|
542
|
+
body: {
|
|
516
543
|
status,
|
|
517
544
|
finishedAt: new Date().toISOString(),
|
|
518
545
|
tests,
|
|
@@ -524,42 +551,21 @@ const main = async () => {
|
|
|
524
551
|
contentType: artifact.contentType,
|
|
525
552
|
testKey: null
|
|
526
553
|
}))
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
fail(`POST /api/runs/:runId/complete failed (${completeRes.status}): ${body}`);
|
|
532
|
-
}
|
|
533
|
-
let shareUrl = null;
|
|
534
|
-
try {
|
|
535
|
-
const data = await completeRes.json();
|
|
536
|
-
shareUrl = data.shareUrl || null;
|
|
537
|
-
}
|
|
538
|
-
catch {
|
|
539
|
-
shareUrl = null;
|
|
540
|
-
}
|
|
541
|
-
const internalUrl = `${appUrl}/runs/${runId}`;
|
|
542
|
-
console.log(JSON.stringify({
|
|
543
|
-
event: "uploader_summary",
|
|
544
|
-
runId,
|
|
545
|
-
tests: tests.length,
|
|
546
|
-
artifacts: artifacts.length,
|
|
547
|
-
uploadDurationMs
|
|
548
|
-
}));
|
|
549
|
-
console.log(`Uploaded ${artifacts.length} artifacts and completed run ${runId}.`);
|
|
550
|
-
console.log(internalUrl);
|
|
551
|
-
if (shareUrl)
|
|
552
|
-
console.log(shareUrl);
|
|
553
|
-
if (status === "failed") {
|
|
554
|
-
const total = tests.length;
|
|
555
|
-
const failed = tests.filter((t) => t.status === "failed").length;
|
|
556
|
-
const useColor = !process.env.NO_COLOR;
|
|
557
|
-
const bold = useColor ? "\u001b[1m" : "";
|
|
558
|
-
const red = useColor ? "\u001b[31m" : "";
|
|
559
|
-
const reset = useColor ? "\u001b[0m" : "";
|
|
560
|
-
console.log(`${bold}${red}CI Debug Report${reset}`);
|
|
561
|
-
console.log(`${bold}${failed} failed / ${total} total${reset}`);
|
|
554
|
+
}
|
|
555
|
+
}, 2);
|
|
556
|
+
if (completeRes.status >= 400) {
|
|
557
|
+
fail(`POST /api/runs/:runId/complete failed (${completeRes.status}): ${completeRes.raw}`);
|
|
562
558
|
}
|
|
559
|
+
const response = completeRes.body || {};
|
|
560
|
+
const output = (0, output_1.formatCiOutput)({
|
|
561
|
+
failedCount: response.failedCount ?? tests.filter((t) => t.status === "failed").length,
|
|
562
|
+
totalCount: response.totalCount ?? tests.length,
|
|
563
|
+
internalRunUrl: response.internalRunUrl || internalRunUrl,
|
|
564
|
+
internalFirstFailureUrl: response.internalFirstFailureUrl || null,
|
|
565
|
+
shareRunUrl: response.shareRunUrl || null,
|
|
566
|
+
shareFirstFailureUrl: response.shareFirstFailureUrl || null
|
|
567
|
+
}, !process.env.NO_COLOR);
|
|
568
|
+
console.log(output);
|
|
563
569
|
};
|
|
564
570
|
main().catch((err) => {
|
|
565
571
|
fail(err?.message || String(err));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.putFile = exports.requestJson = void 0;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const https_1 = __importDefault(require("https"));
|
|
9
|
+
const requestText = (url, options) => new Promise((resolve, reject) => {
|
|
10
|
+
const target = new URL(url);
|
|
11
|
+
const client = target.protocol === "https:" ? https_1.default : http_1.default;
|
|
12
|
+
const headers = { ...(options.headers || {}) };
|
|
13
|
+
let body;
|
|
14
|
+
if (options.body !== undefined) {
|
|
15
|
+
body = JSON.stringify(options.body);
|
|
16
|
+
headers["content-type"] = headers["content-type"] || "application/json";
|
|
17
|
+
headers["content-length"] = Buffer.byteLength(body).toString();
|
|
18
|
+
}
|
|
19
|
+
const req = client.request({
|
|
20
|
+
method: options.method,
|
|
21
|
+
hostname: target.hostname,
|
|
22
|
+
port: target.port,
|
|
23
|
+
path: `${target.pathname}${target.search}`,
|
|
24
|
+
headers
|
|
25
|
+
}, (res) => {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
28
|
+
res.on("end", () => {
|
|
29
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
30
|
+
resolve({ status: res.statusCode || 0, raw });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const timeoutMs = options.timeoutMs ?? 15000;
|
|
34
|
+
req.setTimeout(timeoutMs, () => {
|
|
35
|
+
req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
36
|
+
});
|
|
37
|
+
req.on("error", reject);
|
|
38
|
+
if (body)
|
|
39
|
+
req.write(body);
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
const requestJson = async (url, options, retries = 2) => {
|
|
43
|
+
let attempt = 0;
|
|
44
|
+
while (attempt <= retries) {
|
|
45
|
+
try {
|
|
46
|
+
const { status, raw } = await requestText(url, options);
|
|
47
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
48
|
+
if (status >= 500 && attempt < retries) {
|
|
49
|
+
attempt += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
return { status, body: parsed, raw };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (attempt >= retries)
|
|
56
|
+
throw error;
|
|
57
|
+
attempt += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error("Request failed");
|
|
61
|
+
};
|
|
62
|
+
exports.requestJson = requestJson;
|
|
63
|
+
const putFile = (url, fileStream, contentType, contentLength, timeoutMs = 60000) => new Promise((resolve, reject) => {
|
|
64
|
+
const target = new URL(url);
|
|
65
|
+
const client = target.protocol === "https:" ? https_1.default : http_1.default;
|
|
66
|
+
const req = client.request({
|
|
67
|
+
method: "PUT",
|
|
68
|
+
hostname: target.hostname,
|
|
69
|
+
port: target.port,
|
|
70
|
+
path: `${target.pathname}${target.search}`,
|
|
71
|
+
headers: {
|
|
72
|
+
"content-type": contentType,
|
|
73
|
+
"content-length": contentLength.toString()
|
|
74
|
+
}
|
|
75
|
+
}, (res) => {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
78
|
+
res.on("end", () => {
|
|
79
|
+
resolve({
|
|
80
|
+
status: res.statusCode || 0,
|
|
81
|
+
raw: Buffer.concat(chunks).toString("utf8")
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
req.setTimeout(timeoutMs, () => {
|
|
86
|
+
req.destroy(new Error(`Upload timed out after ${timeoutMs}ms`));
|
|
87
|
+
});
|
|
88
|
+
req.on("error", reject);
|
|
89
|
+
fileStream.on("error", reject);
|
|
90
|
+
fileStream.pipe(req);
|
|
91
|
+
});
|
|
92
|
+
exports.putFile = putFile;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCiOutput = void 0;
|
|
4
|
+
const colorize = (value, code, useColor) => {
|
|
5
|
+
if (!useColor)
|
|
6
|
+
return value;
|
|
7
|
+
return `\u001b[${code}m${value}\u001b[0m`;
|
|
8
|
+
};
|
|
9
|
+
const formatCiOutput = (links, useColor = true) => {
|
|
10
|
+
const lines = [];
|
|
11
|
+
const failedLine = `${links.failedCount} failed / ${links.totalCount} total`;
|
|
12
|
+
const header = links.failedCount > 0
|
|
13
|
+
? colorize("❌ CI Debug Report", "1;31", useColor)
|
|
14
|
+
: colorize("✅ CI Debug Report", "1;32", useColor);
|
|
15
|
+
const countLine = colorize(failedLine, "1", useColor);
|
|
16
|
+
lines.push(header);
|
|
17
|
+
lines.push(countLine);
|
|
18
|
+
lines.push("");
|
|
19
|
+
if (links.internalFirstFailureUrl) {
|
|
20
|
+
lines.push("First failure (fastest):");
|
|
21
|
+
lines.push(links.internalFirstFailureUrl);
|
|
22
|
+
lines.push("");
|
|
23
|
+
}
|
|
24
|
+
lines.push("Full run:");
|
|
25
|
+
lines.push(links.internalRunUrl);
|
|
26
|
+
if (links.shareRunUrl) {
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push("Share (expires in 72h):");
|
|
29
|
+
lines.push(links.shareFirstFailureUrl || links.shareRunUrl);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
};
|
|
33
|
+
exports.formatCiOutput = formatCiOutput;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentinelqa/uploader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sentinel uploader CLI for CI/CD debugging artifacts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"main": "dist/cli.js",
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"test": "npm run build && node test/output.test.js",
|
|
35
36
|
"publish:dry": "npm run build && npm pack --dry-run"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|