@metasession.co/devaudit-cli 0.1.62 → 0.1.64

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.64",
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": "file:../plugin-sdk",
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`
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git init:*)",
5
+ "Bash(git add:*)",
6
+ "Bash(git branch:*)",
7
+ "Bash(git commit:*)",
8
+ "Bash(gh repo:*)"
9
+ ]
10
+ }
11
+ }
@@ -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
@@ -190,22 +190,42 @@ jobs:
190
190
  # PRs to develop should report Quality Gates without mutating DevAudit
191
191
  # releases or evidence. Release registration remains a develop-push /
192
192
  # manual-dispatch side effect.
193
- if: ${{ github.event_name != 'pull_request' && vars.DEVAUDIT_BASE_URL != '' }}
193
+ if: ${{ github.event_name != 'pull_request' }}
194
194
  outputs:
195
195
  version: ${{ steps.version.outputs.version }}
196
196
  env:
197
- DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
197
+ DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
198
198
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
199
199
  steps:
200
200
  - uses: actions/checkout@v6
201
201
 
202
- - name: Validate DevAudit env
202
+ - name: Resolve DevAudit base URL
203
203
  run: |
204
- if [ -z "${DEVAUDIT_BASE_URL}" ] || [ -z "${DEVAUDIT_API_KEY}" ]; then
205
- echo "::error::DEVAUDIT_BASE_URL (variable) and DEVAUDIT_API_KEY (secret) must both be set."
204
+ # Prefer sdlc-config.json devaudit.base_url (PR-visible) over the
205
+ # deprecated repo Variable matching the node template and
206
+ # compliance-evidence.yml. DevAudit-Installer#156: gating this job
207
+ # on `vars.DEVAUDIT_BASE_URL` silently skipped release registration
208
+ # + evidence upload for consumers that moved base_url into
209
+ # sdlc-config.json (the v1.23.0 direction).
210
+ BASE=""
211
+ if [ -f sdlc-config.json ]; then
212
+ CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
213
+ [ -n "$CONFIG_URL" ] && BASE="$CONFIG_URL"
214
+ fi
215
+ if [ -n "$BASE" ]; then
216
+ echo "Using devaudit.base_url from sdlc-config.json: $BASE"
217
+ elif [ -n "$DEVAUDIT_BASE_URL_VAR" ]; then
218
+ BASE="$DEVAUDIT_BASE_URL_VAR"
219
+ echo "::warning::Using repo Variable DEVAUDIT_BASE_URL (deprecated in v1.23.0). Move base_url to sdlc-config.json devaudit.base_url for PR-visible config."
220
+ else
221
+ echo "::warning::No DevAudit base URL configured — skipping release registration. Set devaudit.base_url in sdlc-config.json."
222
+ fi
223
+ if [ -n "$BASE" ] && [ -z "${DEVAUDIT_API_KEY}" ]; then
224
+ echo "::error::DEVAUDIT_API_KEY (secret) must be set when a DevAudit base URL is configured."
206
225
  exit 1
207
226
  fi
208
- echo "BASE=${DEVAUDIT_BASE_URL%/}" >> "$GITHUB_ENV"
227
+ echo "BASE=${BASE%/}" >> "$GITHUB_ENV"
228
+ echo "DEVAUDIT_BASE_URL=${BASE%/}" >> "$GITHUB_ENV"
209
229
 
210
230
  - name: Determine release version
211
231
  id: version
@@ -294,13 +314,33 @@ jobs:
294
314
  # evidence — `status=failed` is itself the audit trail. `!cancelled()`
295
315
  # still guards against partial state on operator-cancel.
296
316
  # DevAudit-Installer#96.
297
- if: ${{ always() && !cancelled() && vars.DEVAUDIT_BASE_URL != '' && needs.register-release.result == 'success' }}
317
+ if: ${{ always() && !cancelled() && needs.register-release.result == 'success' }}
298
318
  env:
299
- DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
319
+ DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
300
320
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
301
321
  steps:
302
322
  - uses: actions/checkout@v6
303
323
 
324
+ - name: Resolve DevAudit base URL
325
+ run: |
326
+ # Prefer sdlc-config.json devaudit.base_url over the deprecated repo
327
+ # Variable (DevAudit-Installer#156). When neither is set the upload
328
+ # step below no-ops via `if: env.DEVAUDIT_BASE_URL != ''`.
329
+ BASE=""
330
+ if [ -f sdlc-config.json ]; then
331
+ CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
332
+ [ -n "$CONFIG_URL" ] && BASE="$CONFIG_URL"
333
+ fi
334
+ if [ -n "$BASE" ]; then
335
+ echo "Using devaudit.base_url from sdlc-config.json: $BASE"
336
+ elif [ -n "$DEVAUDIT_BASE_URL_VAR" ]; then
337
+ BASE="$DEVAUDIT_BASE_URL_VAR"
338
+ echo "::warning::Using repo Variable DEVAUDIT_BASE_URL (deprecated in v1.23.0). Move base_url to sdlc-config.json devaudit.base_url for PR-visible config."
339
+ else
340
+ echo "::warning::No DevAudit base URL configured — skipping evidence upload. Set devaudit.base_url in sdlc-config.json."
341
+ fi
342
+ echo "DEVAUDIT_BASE_URL=${BASE%/}" >> "$GITHUB_ENV"
343
+
304
344
  # Download to workspace root: upload-artifact@v4 preserves the file's
305
345
  # workspace-relative path (e.g. mission-control-api/ci-evidence/sast.json
306
346
  # for a subdir project). Downloading with path: '.' restores files to