@sentinelqa/uploader 0.1.0 → 0.1.2
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/cli.js +142 -72
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -73,6 +73,38 @@ const listFilesRecursive = (dirPath) => {
|
|
|
73
73
|
}
|
|
74
74
|
return results;
|
|
75
75
|
};
|
|
76
|
+
const findLatestMatch = (dirPath, matcher) => {
|
|
77
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
78
|
+
return null;
|
|
79
|
+
if (!fs_1.default.statSync(dirPath).isDirectory())
|
|
80
|
+
return null;
|
|
81
|
+
const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
82
|
+
const files = entries
|
|
83
|
+
.filter((e) => e.isFile() && matcher(e.name))
|
|
84
|
+
.map((e) => path_1.default.join(dirPath, e.name));
|
|
85
|
+
if (files.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
files.sort((a, b) => fs_1.default.statSync(b).mtimeMs - fs_1.default.statSync(a).mtimeMs);
|
|
88
|
+
return files[0];
|
|
89
|
+
};
|
|
90
|
+
const detectPlaywrightJson = () => {
|
|
91
|
+
const envPath = readEnv("PLAYWRIGHT_JSON_PATH");
|
|
92
|
+
if (envPath && fs_1.default.existsSync(envPath) && fs_1.default.statSync(envPath).isFile()) {
|
|
93
|
+
return envPath;
|
|
94
|
+
}
|
|
95
|
+
const defaultPath = DEFAULT_JSON_PATH;
|
|
96
|
+
if (fs_1.default.existsSync(defaultPath) && fs_1.default.statSync(defaultPath).isFile()) {
|
|
97
|
+
return defaultPath;
|
|
98
|
+
}
|
|
99
|
+
const reportPattern = (name) => name.startsWith("report-") && name.endsWith(".json");
|
|
100
|
+
const latestReport = findLatestMatch(DEFAULT_TEST_RESULTS_DIR, reportPattern);
|
|
101
|
+
if (latestReport)
|
|
102
|
+
return latestReport;
|
|
103
|
+
const anyJson = findLatestMatch(DEFAULT_TEST_RESULTS_DIR, (name) => name.endsWith(".json"));
|
|
104
|
+
if (anyJson)
|
|
105
|
+
return anyJson;
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
76
108
|
const normalizeError = (value) => {
|
|
77
109
|
if (!value)
|
|
78
110
|
return null;
|
|
@@ -158,6 +190,26 @@ const extractTestsFromReport = (reportJson) => {
|
|
|
158
190
|
const computeRunStatus = (tests) => {
|
|
159
191
|
return tests.some((t) => t.status === "failed") ? "failed" : "passed";
|
|
160
192
|
};
|
|
193
|
+
const bestEffortComplete = async (appUrl, ingestToken, runId, status, tests) => {
|
|
194
|
+
try {
|
|
195
|
+
await fetch(`${appUrl}/api/runs/${runId}/complete`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
"content-type": "application/json",
|
|
199
|
+
authorization: `Bearer ${ingestToken}`
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
status,
|
|
203
|
+
finishedAt: new Date().toISOString(),
|
|
204
|
+
tests,
|
|
205
|
+
artifacts: []
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Ignore best-effort failures; original error will be surfaced.
|
|
211
|
+
}
|
|
212
|
+
};
|
|
161
213
|
const detectGitLabEnv = () => {
|
|
162
214
|
const isGitLab = readEnv("GITLAB_CI") === "true" || !!readEnv("CI_PROJECT_ID");
|
|
163
215
|
if (!isGitLab)
|
|
@@ -261,9 +313,18 @@ const main = async () => {
|
|
|
261
313
|
const ingestToken = readEnv("SENTINEL_TOKEN") || readEnv("PROJECT_INGEST_TOKEN");
|
|
262
314
|
if (!ingestToken)
|
|
263
315
|
fail("SENTINEL_TOKEN is required.");
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
316
|
+
const cliJsonPath = getArgValue(args, "--playwright-json-path");
|
|
317
|
+
const playwrightJsonPath = cliJsonPath || detectPlaywrightJson();
|
|
318
|
+
if (!playwrightJsonPath) {
|
|
319
|
+
fail([
|
|
320
|
+
"PLAYWRIGHT_JSON_PATH not found.",
|
|
321
|
+
"Checked:",
|
|
322
|
+
`- ${readEnv("PLAYWRIGHT_JSON_PATH") || "(env not set)"}`,
|
|
323
|
+
`- ${DEFAULT_JSON_PATH}`,
|
|
324
|
+
`- ${DEFAULT_TEST_RESULTS_DIR}/report-*.json`,
|
|
325
|
+
`- ${DEFAULT_TEST_RESULTS_DIR}/*.json`
|
|
326
|
+
].join("\n"));
|
|
327
|
+
}
|
|
267
328
|
const playwrightReportDir = getArgValue(args, "--playwright-report-dir") ||
|
|
268
329
|
readEnv("PLAYWRIGHT_REPORT_DIR") ||
|
|
269
330
|
DEFAULT_PLAYWRIGHT_REPORT_DIR;
|
|
@@ -333,77 +394,86 @@ const main = async () => {
|
|
|
333
394
|
return { ...artifact, sizeBytes: stat.size, contentType };
|
|
334
395
|
});
|
|
335
396
|
let finalArtifacts = artifactsWithMeta;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
headers: {
|
|
363
|
-
"content-type": "application/json",
|
|
364
|
-
authorization: `Bearer ${ingestToken}`
|
|
365
|
-
},
|
|
366
|
-
body: JSON.stringify({
|
|
367
|
-
items: finalArtifacts.map((artifact) => ({
|
|
368
|
-
relPath: artifact.objectKey,
|
|
369
|
-
contentType: artifact.contentType,
|
|
370
|
-
sizeBytes: artifact.sizeBytes,
|
|
371
|
-
kind: artifact.type
|
|
372
|
-
}))
|
|
373
|
-
})
|
|
374
|
-
}), "POST /api/uploads/presign");
|
|
375
|
-
if (!presignRes.ok) {
|
|
376
|
-
const body = await presignRes.text();
|
|
377
|
-
fail(`POST /api/uploads/presign failed (${presignRes.status}): ${body}`);
|
|
397
|
+
try {
|
|
398
|
+
if (byoBucket) {
|
|
399
|
+
const s3Region = readEnv("SENTINEL_S3_REGION") ||
|
|
400
|
+
readEnv("AWS_REGION") ||
|
|
401
|
+
readEnv("S3_REGION");
|
|
402
|
+
if (!s3Region)
|
|
403
|
+
fail("SENTINEL_S3_REGION is required for BYO S3.");
|
|
404
|
+
const s3Endpoint = readEnv("SENTINEL_S3_ENDPOINT") || readEnv("S3_ENDPOINT");
|
|
405
|
+
const accessKeyId = readEnv("SENTINEL_S3_ACCESS_KEY_ID") || readEnv("AWS_ACCESS_KEY_ID");
|
|
406
|
+
const secretAccessKey = readEnv("SENTINEL_S3_SECRET_ACCESS_KEY") ||
|
|
407
|
+
readEnv("AWS_SECRET_ACCESS_KEY");
|
|
408
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
409
|
+
fail("SENTINEL_S3_ACCESS_KEY_ID and SENTINEL_S3_SECRET_ACCESS_KEY are required.");
|
|
410
|
+
}
|
|
411
|
+
const s3Client = new client_s3_1.S3Client({
|
|
412
|
+
region: s3Region,
|
|
413
|
+
...(s3Endpoint ? { endpoint: s3Endpoint } : {}),
|
|
414
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
415
|
+
});
|
|
416
|
+
await withRetry(async () => {
|
|
417
|
+
await uploadArtifacts(s3Client, byoBucket, finalArtifacts);
|
|
418
|
+
}, "upload artifacts");
|
|
419
|
+
finalArtifacts = finalArtifacts.map((artifact) => ({
|
|
420
|
+
...artifact,
|
|
421
|
+
bucket: byoBucket
|
|
422
|
+
}));
|
|
378
423
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
}, `upload ${artifact.objectKey}`);
|
|
400
|
-
artifact.objectKey = item.objectKey;
|
|
424
|
+
else {
|
|
425
|
+
const presignRes = await withRetry(() => fetch(`${appUrl}/api/uploads/presign`, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: {
|
|
428
|
+
"content-type": "application/json",
|
|
429
|
+
authorization: `Bearer ${ingestToken}`
|
|
430
|
+
},
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
items: finalArtifacts.map((artifact) => ({
|
|
433
|
+
relPath: artifact.objectKey,
|
|
434
|
+
contentType: artifact.contentType,
|
|
435
|
+
sizeBytes: artifact.sizeBytes,
|
|
436
|
+
kind: artifact.type
|
|
437
|
+
}))
|
|
438
|
+
})
|
|
439
|
+
}), "POST /api/uploads/presign");
|
|
440
|
+
if (!presignRes.ok) {
|
|
441
|
+
const body = await presignRes.text();
|
|
442
|
+
fail(`POST /api/uploads/presign failed (${presignRes.status}): ${body}`);
|
|
401
443
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
444
|
+
const presignData = await presignRes.json();
|
|
445
|
+
const uploadMap = new Map((presignData.items || []).map((item) => [item.relPath, item]));
|
|
446
|
+
await withRetry(async () => {
|
|
447
|
+
for (const artifact of finalArtifacts) {
|
|
448
|
+
const item = uploadMap.get(artifact.objectKey);
|
|
449
|
+
if (!item?.uploadUrl || !item?.objectKey) {
|
|
450
|
+
throw new Error(`Missing upload URL for ${artifact.objectKey}`);
|
|
451
|
+
}
|
|
452
|
+
await withRetry(async () => {
|
|
453
|
+
const body = fs_1.default.createReadStream(artifact.filePath);
|
|
454
|
+
const res = await fetch(item.uploadUrl, {
|
|
455
|
+
method: "PUT",
|
|
456
|
+
headers: {
|
|
457
|
+
"content-type": artifact.contentType
|
|
458
|
+
},
|
|
459
|
+
body
|
|
460
|
+
});
|
|
461
|
+
if (!res.ok) {
|
|
462
|
+
throw new Error(`Upload failed (${res.status}) for ${artifact.objectKey}`);
|
|
463
|
+
}
|
|
464
|
+
}, `upload ${artifact.objectKey}`);
|
|
465
|
+
artifact.objectKey = item.objectKey;
|
|
466
|
+
}
|
|
467
|
+
}, "upload artifacts");
|
|
468
|
+
finalArtifacts = finalArtifacts.map((artifact) => ({
|
|
469
|
+
...artifact,
|
|
470
|
+
bucket: presignData.bucket
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
await bestEffortComplete(appUrl, ingestToken, runId, "failed", tests);
|
|
476
|
+
fail(err?.message || String(err));
|
|
407
477
|
}
|
|
408
478
|
const uploadDurationMs = Date.now() - uploadStart;
|
|
409
479
|
const completeRes = await withRetry(() => fetch(`${appUrl}/api/runs/${runId}/complete`, {
|