@metasession.co/devaudit-cli 0.1.62 → 0.1.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^0.8.2",
36
- "@metasession.co/devaudit-plugin-sdk": "^0.1.62",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.1.63",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -244,6 +244,13 @@ MAX_ATTEMPTS=${UPLOAD_MAX_ATTEMPTS:-5}
244
244
  INITIAL_BACKOFF_SECONDS=${UPLOAD_INITIAL_BACKOFF_SECONDS:-1}
245
245
  UPLOAD_CONNECT_TIMEOUT_SECONDS=${UPLOAD_CONNECT_TIMEOUT_SECONDS:-10}
246
246
  UPLOAD_MAX_TIME_SECONDS=${UPLOAD_MAX_TIME_SECONDS:-120}
247
+ # DevAudit-Installer#189 — files above this size use the presigned R2 URL
248
+ # upload flow (3-step: request URL → PUT to R2 → notify portal) instead of
249
+ # the multipart POST. Avoids the portal's FormData body parser limit.
250
+ # 25MB = 26214400 bytes.
251
+ PRESIGNED_THRESHOLD_BYTES=${PRESIGNED_THRESHOLD:-26214400}
252
+ PRESIGNED_MAX_ATTEMPTS=${PRESIGNED_MAX_ATTEMPTS:-3}
253
+ PRESIGNED_UPLOAD_MAX_TIME_SECONDS=${PRESIGNED_UPLOAD_MAX_TIME_SECONDS:-300}
247
254
 
248
255
  is_unedited_starter_stub() {
249
256
  # Match BOTH banner phrasings the SDLC has shipped (v0.1.36 changed
@@ -253,6 +260,153 @@ is_unedited_starter_stub() {
253
260
  grep -aqE 'STARTER TEMPLATE.+REPLACE BEFORE' "$1"
254
261
  }
255
262
 
263
+ # DevAudit-Installer#189 — Presigned R2 URL upload for large files (>25MB).
264
+ # 3-step flow: (1) request presigned URL from portal, (2) PUT file to R2,
265
+ # (3) notify portal that upload is complete. Each step retries on 429/5xx
266
+ # and connection errors. Returns 0 on success, 1 on failure.
267
+ upload_presigned() {
268
+ local file="$1"
269
+ local filename
270
+ filename=$(basename "$file")
271
+ local file_size
272
+ file_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
273
+
274
+ # Derive a MIME type from the extension (best-effort).
275
+ local mime_type="application/octet-stream"
276
+ case "$filename" in
277
+ *.zip) mime_type="application/zip" ;;
278
+ *.json) mime_type="application/json" ;;
279
+ *.png) mime_type="image/png" ;;
280
+ *.pdf) mime_type="application/pdf" ;;
281
+ *.md) mime_type="text/markdown" ;;
282
+ *.html) mime_type="text/html" ;;
283
+ esac
284
+
285
+ local presign_url="${DEVAUDIT_BASE_URL}/api/evidence/upload-url"
286
+ local complete_url="${DEVAUDIT_BASE_URL}/api/evidence/upload-complete"
287
+
288
+ # --- Step 1: Request presigned upload URL ---
289
+ local attempt backoff http_code curl_exit resp_body upload_url evidence_id
290
+ attempt=1
291
+ backoff=$INITIAL_BACKOFF_SECONDS
292
+ upload_url=""
293
+ evidence_id=""
294
+ while [ "$attempt" -le "$PRESIGNED_MAX_ATTEMPTS" ]; do
295
+ resp_body=$(mktemp)
296
+ http_code=$(curl -s -o "$resp_body" -w "%{http_code}" \
297
+ -X POST -L --max-redirs 3 \
298
+ --connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS" \
299
+ --max-time "$UPLOAD_MAX_TIME_SECONDS" \
300
+ "$presign_url" \
301
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
302
+ -H "Content-Type: application/json" \
303
+ -d "{
304
+ \"projectSlug\": \"${PROJECT_SLUG}\",
305
+ \"requirementId\": \"${REQUIREMENT_ID}\",
306
+ \"evidenceType\": \"${EVIDENCE_TYPE}\",
307
+ \"fileName\": \"${filename}\",
308
+ \"fileSizeBytes\": ${file_size},
309
+ \"mimeType\": \"${mime_type}\",
310
+ \"metadata\": ${METADATA},
311
+ \"releaseVersion\": \"${RELEASE_VERSION}\",
312
+ \"createReleaseIfMissing\": \"${CREATE_RELEASE_IF_MISSING}\",
313
+ \"releaseBranch\": \"${BRANCH}\",
314
+ \"environment\": \"${ENVIRONMENT}\",
315
+ \"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
316
+ \"sdlcStage\": \"${SDLC_STAGE}\"
317
+ }") || curl_exit=$?
318
+ curl_exit=${curl_exit:-0}
319
+ if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
320
+ upload_url=$(jq -r '.uploadUrl // empty' "$resp_body" 2>/dev/null || true)
321
+ evidence_id=$(jq -r '.evidenceId // empty' "$resp_body" 2>/dev/null || true)
322
+ rm -f "$resp_body"
323
+ if [ -n "$upload_url" ] && [ -n "$evidence_id" ] && [ "$upload_url" != "null" ] && [ "$evidence_id" != "null" ]; then
324
+ break
325
+ fi
326
+ # Portal responded 2xx but didn't return presigned URL fields —
327
+ # presigned URL flow not configured. Fall back to multipart.
328
+ echo -n "(portal did not return presigned URL, falling back to multipart) "
329
+ return 255
330
+ fi
331
+ rm -f "$resp_body"
332
+ if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
333
+ if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
334
+ echo -n "(step 1: HTTP ${http_code}, retry in ${backoff}s) "
335
+ sleep "$backoff"
336
+ attempt=$((attempt + 1))
337
+ backoff=$((backoff * 2))
338
+ continue
339
+ fi
340
+ fi
341
+ # Non-retriable error (4xx other than 429).
342
+ echo -n "(step 1 failed: HTTP ${http_code}) "
343
+ return 1
344
+ done
345
+
346
+ if [ -z "$upload_url" ] || [ -z "$evidence_id" ]; then
347
+ echo -n "(step 1: no presigned URL after ${attempt} attempts) "
348
+ return 1
349
+ fi
350
+
351
+ # --- Step 2: Upload directly to R2 via presigned URL ---
352
+ attempt=1
353
+ backoff=$INITIAL_BACKOFF_SECONDS
354
+ while [ "$attempt" -le "$PRESIGNED_MAX_ATTEMPTS" ]; do
355
+ http_code=$(curl -s -o /dev/null -w "%{http_code}" \
356
+ -X PUT \
357
+ -H "Content-Type: ${mime_type}" \
358
+ --connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS" \
359
+ --max-time "$PRESIGNED_UPLOAD_MAX_TIME_SECONDS" \
360
+ --data-binary @"$file" \
361
+ "$upload_url") || curl_exit=$?
362
+ curl_exit=${curl_exit:-0}
363
+ if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
364
+ break
365
+ fi
366
+ if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
367
+ if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
368
+ echo -n "(step 2: HTTP ${http_code}, retry in ${backoff}s) "
369
+ sleep "$backoff"
370
+ attempt=$((attempt + 1))
371
+ backoff=$((backoff * 2))
372
+ continue
373
+ fi
374
+ fi
375
+ echo -n "(step 2 failed: HTTP ${http_code}) "
376
+ return 1
377
+ done
378
+
379
+ # --- Step 3: Notify portal that upload is complete ---
380
+ attempt=1
381
+ backoff=$INITIAL_BACKOFF_SECONDS
382
+ while [ "$attempt" -le "$PRESIGNED_MAX_ATTEMPTS" ]; do
383
+ http_code=$(curl -s -o /dev/null -w "%{http_code}" \
384
+ -X POST -L --max-redirs 3 \
385
+ --connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS" \
386
+ --max-time "$UPLOAD_MAX_TIME_SECONDS" \
387
+ "$complete_url" \
388
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
389
+ -H "Content-Type: application/json" \
390
+ -d "{\"evidenceId\": \"${evidence_id}\"}") || curl_exit=$?
391
+ curl_exit=${curl_exit:-0}
392
+ if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
393
+ return 0
394
+ fi
395
+ if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
396
+ if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
397
+ echo -n "(step 3: HTTP ${http_code}, retry in ${backoff}s) "
398
+ sleep "$backoff"
399
+ attempt=$((attempt + 1))
400
+ backoff=$((backoff * 2))
401
+ continue
402
+ fi
403
+ fi
404
+ echo -n "(step 3 failed: HTTP ${http_code}) "
405
+ return 1
406
+ done
407
+ return 1
408
+ }
409
+
256
410
  for FILE in "${FILES[@]}"; do
257
411
  FILENAME=$(basename "$FILE")
258
412
  if is_unedited_starter_stub "$FILE"; then
@@ -262,6 +416,27 @@ for FILE in "${FILES[@]}"; do
262
416
  fi
263
417
  FILE_SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
264
418
  echo -n "Uploading ${FILENAME}... "
419
+
420
+ # DevAudit-Installer#189 — large files use the presigned R2 URL flow
421
+ # to bypass the portal's multipart body parser limit. If the portal
422
+ # doesn't support presigned URLs (returns 255), fall back to multipart.
423
+ if [ "$FILE_SIZE" -ge "$PRESIGNED_THRESHOLD_BYTES" ]; then
424
+ PRESIGNED_RESULT=0
425
+ upload_presigned "$FILE" || PRESIGNED_RESULT=$?
426
+ if [ "$PRESIGNED_RESULT" -eq 0 ]; then
427
+ echo "OK (${FILE_SIZE} bytes, presigned R2 upload)"
428
+ SUCCEEDED=$((SUCCEEDED + 1))
429
+ TOTAL_SIZE=$((TOTAL_SIZE + FILE_SIZE))
430
+ continue
431
+ elif [ "$PRESIGNED_RESULT" -ne 255 ]; then
432
+ echo "FAILED (presigned upload after ${PRESIGNED_MAX_ATTEMPTS} attempts)"
433
+ FAILED=$((FAILED + 1))
434
+ continue
435
+ fi
436
+ # Result 255 — portal doesn't support presigned URLs, fall through
437
+ # to the existing multipart flow.
438
+ fi
439
+
265
440
  # `-L` follows 3xx redirects (devaudit-installer#143). The portal host
266
441
  # has moved before (devaudit.metasession.co → devaudit.ai); without -L
267
442
  # every consumer's CI silently fails on a stale base URL. `--max-redirs 3`
@@ -639,6 +639,13 @@ jobs:
639
639
  (cd e2e-artifacts && zip -qr playwright-report.zip playwright-report/) 2>/dev/null || true
640
640
  fi
641
641
  if [ -f e2e-artifacts/playwright-report.zip ]; then
642
+ # DevAudit-Installer#189 — the Playwright HTML report zip
643
+ # (~50–100MB) exceeds the portal's multipart body parser
644
+ # limit. The JSON results above are the canonical evidence;
645
+ # the HTML report is a human-facing convenience. Soft-fail
646
+ # so the job doesn't block on a portal-side size limit.
647
+ # Once the portal's presigned R2 URL flow is live (#189
648
+ # Option C), this can be reverted to a hard fail.
642
649
  if bash scripts/upload-evidence.sh \
643
650
  {{PROJECT_SLUG}} "$REQ" test_report \
644
651
  e2e-artifacts/playwright-report.zip \
@@ -647,8 +654,7 @@ jobs:
647
654
  then
648
655
  :
649
656
  else
650
- echo "::warning::playwright HTML report upload failed for ${REQ}"
651
- UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
657
+ echo "::warning::playwright HTML report upload failed for ${REQ} (non-blocking — JSON results are canonical evidence)"
652
658
  fi
653
659
  fi
654
660
  done