@sentinelqa/uploader 0.1.1 → 0.1.3
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 +130 -73
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -190,6 +190,26 @@ const extractTestsFromReport = (reportJson) => {
|
|
|
190
190
|
const computeRunStatus = (tests) => {
|
|
191
191
|
return tests.some((t) => t.status === "failed") ? "failed" : "passed";
|
|
192
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
|
+
};
|
|
193
213
|
const detectGitLabEnv = () => {
|
|
194
214
|
const isGitLab = readEnv("GITLAB_CI") === "true" || !!readEnv("CI_PROJECT_ID");
|
|
195
215
|
if (!isGitLab)
|
|
@@ -198,10 +218,21 @@ const detectGitLabEnv = () => {
|
|
|
198
218
|
provider: "gitlab",
|
|
199
219
|
commit: {
|
|
200
220
|
sha: readEnv("CI_COMMIT_SHA"),
|
|
201
|
-
branch: readEnv("CI_COMMIT_BRANCH") || readEnv("CI_COMMIT_REF_NAME")
|
|
221
|
+
branch: readEnv("CI_COMMIT_BRANCH") || readEnv("CI_COMMIT_REF_NAME"),
|
|
222
|
+
message: readEnv("CI_COMMIT_MESSAGE"),
|
|
223
|
+
author: readEnv("GITLAB_USER_NAME") || readEnv("GITLAB_USER_LOGIN")
|
|
224
|
+
},
|
|
225
|
+
workflow: {
|
|
226
|
+
id: readEnv("CI_PIPELINE_ID"),
|
|
227
|
+
name: readEnv("CI_PIPELINE_SOURCE")
|
|
228
|
+
},
|
|
229
|
+
job: {
|
|
230
|
+
id: readEnv("CI_JOB_ID"),
|
|
231
|
+
name: readEnv("CI_JOB_NAME")
|
|
202
232
|
},
|
|
203
233
|
ci: {
|
|
204
|
-
url: readEnv("CI_JOB_URL") || readEnv("CI_PIPELINE_URL")
|
|
234
|
+
url: readEnv("CI_JOB_URL") || readEnv("CI_PIPELINE_URL"),
|
|
235
|
+
pipelineUrl: readEnv("CI_PIPELINE_URL")
|
|
205
236
|
}
|
|
206
237
|
};
|
|
207
238
|
};
|
|
@@ -218,8 +249,21 @@ const detectGitHubEnv = () => {
|
|
|
218
249
|
const url = serverUrl && repo && runId ? `${serverUrl}/${repo}/actions/runs/${runId}` : null;
|
|
219
250
|
return {
|
|
220
251
|
provider: "github",
|
|
221
|
-
commit: {
|
|
222
|
-
|
|
252
|
+
commit: {
|
|
253
|
+
sha,
|
|
254
|
+
branch,
|
|
255
|
+
message: readEnv("GITHUB_COMMIT_MESSAGE") || null,
|
|
256
|
+
author: readEnv("GITHUB_ACTOR")
|
|
257
|
+
},
|
|
258
|
+
workflow: {
|
|
259
|
+
id: runId,
|
|
260
|
+
name: readEnv("GITHUB_WORKFLOW")
|
|
261
|
+
},
|
|
262
|
+
job: {
|
|
263
|
+
id: readEnv("GITHUB_RUN_ATTEMPT"),
|
|
264
|
+
name: readEnv("GITHUB_JOB")
|
|
265
|
+
},
|
|
266
|
+
ci: { url, pipelineUrl: url }
|
|
223
267
|
};
|
|
224
268
|
};
|
|
225
269
|
const getArtifacts = (rootDir, typeOverride, runId, s3Prefix) => {
|
|
@@ -328,6 +372,8 @@ const main = async () => {
|
|
|
328
372
|
fail("Commit branch is required.");
|
|
329
373
|
if (!ci.ci.url)
|
|
330
374
|
fail("CI run URL is required.");
|
|
375
|
+
if (!ci.workflow.id)
|
|
376
|
+
fail("Workflow ID is required.");
|
|
331
377
|
const appUrl = readEnv("SENTINEL_URL") || readEnv("APP_URL") || DEFAULT_APP_URL;
|
|
332
378
|
const createRes = await withRetry(() => fetch(`${appUrl}/api/runs`, {
|
|
333
379
|
method: "POST",
|
|
@@ -338,6 +384,8 @@ const main = async () => {
|
|
|
338
384
|
body: JSON.stringify({
|
|
339
385
|
provider: ci.provider,
|
|
340
386
|
commit: ci.commit,
|
|
387
|
+
workflow: ci.workflow,
|
|
388
|
+
job: ci.job,
|
|
341
389
|
ci: ci.ci
|
|
342
390
|
})
|
|
343
391
|
}), "POST /api/runs");
|
|
@@ -374,77 +422,86 @@ const main = async () => {
|
|
|
374
422
|
return { ...artifact, sizeBytes: stat.size, contentType };
|
|
375
423
|
});
|
|
376
424
|
let finalArtifacts = artifactsWithMeta;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
headers: {
|
|
404
|
-
"content-type": "application/json",
|
|
405
|
-
authorization: `Bearer ${ingestToken}`
|
|
406
|
-
},
|
|
407
|
-
body: JSON.stringify({
|
|
408
|
-
items: finalArtifacts.map((artifact) => ({
|
|
409
|
-
relPath: artifact.objectKey,
|
|
410
|
-
contentType: artifact.contentType,
|
|
411
|
-
sizeBytes: artifact.sizeBytes,
|
|
412
|
-
kind: artifact.type
|
|
413
|
-
}))
|
|
414
|
-
})
|
|
415
|
-
}), "POST /api/uploads/presign");
|
|
416
|
-
if (!presignRes.ok) {
|
|
417
|
-
const body = await presignRes.text();
|
|
418
|
-
fail(`POST /api/uploads/presign failed (${presignRes.status}): ${body}`);
|
|
425
|
+
try {
|
|
426
|
+
if (byoBucket) {
|
|
427
|
+
const s3Region = readEnv("SENTINEL_S3_REGION") ||
|
|
428
|
+
readEnv("AWS_REGION") ||
|
|
429
|
+
readEnv("S3_REGION");
|
|
430
|
+
if (!s3Region)
|
|
431
|
+
fail("SENTINEL_S3_REGION is required for BYO S3.");
|
|
432
|
+
const s3Endpoint = readEnv("SENTINEL_S3_ENDPOINT") || readEnv("S3_ENDPOINT");
|
|
433
|
+
const accessKeyId = readEnv("SENTINEL_S3_ACCESS_KEY_ID") || readEnv("AWS_ACCESS_KEY_ID");
|
|
434
|
+
const secretAccessKey = readEnv("SENTINEL_S3_SECRET_ACCESS_KEY") ||
|
|
435
|
+
readEnv("AWS_SECRET_ACCESS_KEY");
|
|
436
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
437
|
+
fail("SENTINEL_S3_ACCESS_KEY_ID and SENTINEL_S3_SECRET_ACCESS_KEY are required.");
|
|
438
|
+
}
|
|
439
|
+
const s3Client = new client_s3_1.S3Client({
|
|
440
|
+
region: s3Region,
|
|
441
|
+
...(s3Endpoint ? { endpoint: s3Endpoint } : {}),
|
|
442
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
443
|
+
});
|
|
444
|
+
await withRetry(async () => {
|
|
445
|
+
await uploadArtifacts(s3Client, byoBucket, finalArtifacts);
|
|
446
|
+
}, "upload artifacts");
|
|
447
|
+
finalArtifacts = finalArtifacts.map((artifact) => ({
|
|
448
|
+
...artifact,
|
|
449
|
+
bucket: byoBucket
|
|
450
|
+
}));
|
|
419
451
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
}, `upload ${artifact.objectKey}`);
|
|
441
|
-
artifact.objectKey = item.objectKey;
|
|
452
|
+
else {
|
|
453
|
+
const presignRes = await withRetry(() => fetch(`${appUrl}/api/uploads/presign`, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: {
|
|
456
|
+
"content-type": "application/json",
|
|
457
|
+
authorization: `Bearer ${ingestToken}`
|
|
458
|
+
},
|
|
459
|
+
body: JSON.stringify({
|
|
460
|
+
items: finalArtifacts.map((artifact) => ({
|
|
461
|
+
relPath: artifact.objectKey,
|
|
462
|
+
contentType: artifact.contentType,
|
|
463
|
+
sizeBytes: artifact.sizeBytes,
|
|
464
|
+
kind: artifact.type
|
|
465
|
+
}))
|
|
466
|
+
})
|
|
467
|
+
}), "POST /api/uploads/presign");
|
|
468
|
+
if (!presignRes.ok) {
|
|
469
|
+
const body = await presignRes.text();
|
|
470
|
+
fail(`POST /api/uploads/presign failed (${presignRes.status}): ${body}`);
|
|
442
471
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
472
|
+
const presignData = await presignRes.json();
|
|
473
|
+
const uploadMap = new Map((presignData.items || []).map((item) => [item.relPath, item]));
|
|
474
|
+
await withRetry(async () => {
|
|
475
|
+
for (const artifact of finalArtifacts) {
|
|
476
|
+
const item = uploadMap.get(artifact.objectKey);
|
|
477
|
+
if (!item?.uploadUrl || !item?.objectKey) {
|
|
478
|
+
throw new Error(`Missing upload URL for ${artifact.objectKey}`);
|
|
479
|
+
}
|
|
480
|
+
await withRetry(async () => {
|
|
481
|
+
const body = fs_1.default.createReadStream(artifact.filePath);
|
|
482
|
+
const res = await fetch(item.uploadUrl, {
|
|
483
|
+
method: "PUT",
|
|
484
|
+
headers: {
|
|
485
|
+
"content-type": artifact.contentType
|
|
486
|
+
},
|
|
487
|
+
body
|
|
488
|
+
});
|
|
489
|
+
if (!res.ok) {
|
|
490
|
+
throw new Error(`Upload failed (${res.status}) for ${artifact.objectKey}`);
|
|
491
|
+
}
|
|
492
|
+
}, `upload ${artifact.objectKey}`);
|
|
493
|
+
artifact.objectKey = item.objectKey;
|
|
494
|
+
}
|
|
495
|
+
}, "upload artifacts");
|
|
496
|
+
finalArtifacts = finalArtifacts.map((artifact) => ({
|
|
497
|
+
...artifact,
|
|
498
|
+
bucket: presignData.bucket
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
await bestEffortComplete(appUrl, ingestToken, runId, "failed", tests);
|
|
504
|
+
fail(err?.message || String(err));
|
|
448
505
|
}
|
|
449
506
|
const uploadDurationMs = Date.now() - uploadStart;
|
|
450
507
|
const completeRes = await withRetry(() => fetch(`${appUrl}/api/runs/${runId}/complete`, {
|