@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.
Files changed (2) hide show
  1. package/dist/cli.js +130 -73
  2. 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: { sha, branch },
222
- ci: { url }
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
- if (byoBucket) {
378
- const s3Region = readEnv("SENTINEL_S3_REGION") || readEnv("AWS_REGION") || readEnv("S3_REGION");
379
- if (!s3Region)
380
- fail("SENTINEL_S3_REGION is required for BYO S3.");
381
- const s3Endpoint = readEnv("SENTINEL_S3_ENDPOINT") || readEnv("S3_ENDPOINT");
382
- const accessKeyId = readEnv("SENTINEL_S3_ACCESS_KEY_ID") || readEnv("AWS_ACCESS_KEY_ID");
383
- const secretAccessKey = readEnv("SENTINEL_S3_SECRET_ACCESS_KEY") || readEnv("AWS_SECRET_ACCESS_KEY");
384
- if (!accessKeyId || !secretAccessKey) {
385
- fail("SENTINEL_S3_ACCESS_KEY_ID and SENTINEL_S3_SECRET_ACCESS_KEY are required.");
386
- }
387
- const s3Client = new client_s3_1.S3Client({
388
- region: s3Region,
389
- ...(s3Endpoint ? { endpoint: s3Endpoint } : {}),
390
- credentials: { accessKeyId, secretAccessKey }
391
- });
392
- await withRetry(async () => {
393
- await uploadArtifacts(s3Client, byoBucket, finalArtifacts);
394
- }, "upload artifacts");
395
- finalArtifacts = finalArtifacts.map((artifact) => ({
396
- ...artifact,
397
- bucket: byoBucket
398
- }));
399
- }
400
- else {
401
- const presignRes = await withRetry(() => fetch(`${appUrl}/api/uploads/presign`, {
402
- method: "POST",
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
- const presignData = await presignRes.json();
421
- const uploadMap = new Map((presignData.items || []).map((item) => [item.relPath, item]));
422
- await withRetry(async () => {
423
- for (const artifact of finalArtifacts) {
424
- const item = uploadMap.get(artifact.objectKey);
425
- if (!item?.uploadUrl || !item?.objectKey) {
426
- throw new Error(`Missing upload URL for ${artifact.objectKey}`);
427
- }
428
- await withRetry(async () => {
429
- const body = fs_1.default.createReadStream(artifact.filePath);
430
- const res = await fetch(item.uploadUrl, {
431
- method: "PUT",
432
- headers: {
433
- "content-type": artifact.contentType
434
- },
435
- body
436
- });
437
- if (!res.ok) {
438
- throw new Error(`Upload failed (${res.status}) for ${artifact.objectKey}`);
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
- }, "upload artifacts");
444
- finalArtifacts = finalArtifacts.map((artifact) => ({
445
- ...artifact,
446
- bucket: presignData.bucket
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`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentinelqa/uploader",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Sentinel uploader CLI for CI/CD debugging artifacts",
6
6
  "license": "MIT",