@metasession.co/devaudit-cli 0.3.7 → 0.3.9

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.3.7",
3
+ "version": "0.3.9",
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.3.7",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.3.9",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -21,6 +21,11 @@
21
21
  # release-ticket H1). Forwarded as
22
22
  # `releaseTitle`; the portal no-clobbers
23
23
  # existing non-null values.
24
+ # --release-summary <text> Reviewer-facing short description of what
25
+ # the release covers. Forwarded as
26
+ # `releaseSummary`; the portal no-clobbers
27
+ # existing non-null values.
28
+ # DevAudit-Installer#285.
24
29
  # --change-type <type> Conventional-commit prefix (feat / fix /
25
30
  # refactor / perf / chore / docs / ci /
26
31
  # build / test / compliance / revert) for
@@ -49,6 +54,14 @@
49
54
  # Issue from: Project Settings → API Keys in
50
55
  # META-COMPLY's web UI.
51
56
  #
57
+ # Large-file uploads (>=25MB):
58
+ # Files above 25MB use a presigned R2 upload flow (3-step: request URL →
59
+ # PUT to R2 → notify portal) instead of multipart POST. This requires
60
+ # the Portal to be configured with R2 evidence storage
61
+ # (R2_EVIDENCE_BUCKET and related env vars). If R2 is not configured,
62
+ # the Portal returns a clear error which is surfaced to the operator.
63
+ # #298 — the Portal response body is included in the error output.
64
+ #
52
65
  # Examples:
53
66
  # ./scripts/upload-evidence.sh meta-ats REQ-001 screenshot \
54
67
  # compliance/evidence/REQ-001/screenshots/
@@ -80,6 +93,7 @@ CREATE_RELEASE_IF_MISSING=false
80
93
  ENVIRONMENT=""
81
94
  EVIDENCE_CATEGORY=""
82
95
  RELEASE_TITLE=""
96
+ RELEASE_SUMMARY=""
83
97
  CHANGE_TYPE=""
84
98
  GATE_STATUS=""
85
99
  SDLC_STAGE=""
@@ -103,6 +117,7 @@ while [ "$#" -gt 0 ]; do
103
117
  # the portal's findOrCreateRelease no-clobber backfill. Both optional;
104
118
  # unknown change-type values are dropped server-side, not 400'd.
105
119
  --release-title) RELEASE_TITLE="$2"; shift 2 ;;
120
+ --release-summary) RELEASE_SUMMARY="$2"; shift 2 ;;
106
121
  --change-type) CHANGE_TYPE="$2"; shift 2 ;;
107
122
  # passed/failed/skipped — surfaces failed gates on the portal so
108
123
  # ran-and-failed != never-ran. Unknown values dropped server-side.
@@ -320,6 +335,8 @@ upload_presigned() {
320
335
  \"releaseBranch\": \"${BRANCH}\",
321
336
  \"environment\": \"${ENVIRONMENT}\",
322
337
  \"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
338
+ \"releaseTitle\": \"${RELEASE_TITLE}\",
339
+ \"releaseSummary\": \"${RELEASE_SUMMARY}\",
323
340
  \"sdlcStage\": \"${SDLC_STAGE}\",
324
341
  \"testCycleId\": \"${TEST_CYCLE}\"
325
342
  }") || curl_exit=$?
@@ -336,9 +353,9 @@ upload_presigned() {
336
353
  echo -n "(portal did not return presigned URL, falling back to multipart) "
337
354
  return 255
338
355
  fi
339
- rm -f "$resp_body"
340
356
  if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
341
357
  if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
358
+ rm -f "$resp_body"
342
359
  echo -n "(step 1: HTTP ${http_code}, retry in ${backoff}s) "
343
360
  sleep "$backoff"
344
361
  attempt=$((attempt + 1))
@@ -346,8 +363,20 @@ upload_presigned() {
346
363
  continue
347
364
  fi
348
365
  fi
349
- # Non-retriable error (4xx other than 429).
350
- echo -n "(step 1 failed: HTTP ${http_code}) "
366
+ # Non-retriable error (4xx other than 429) or exhausted retries.
367
+ # #298 Surface the Portal response body so operators see WHY it
368
+ # failed (e.g. "Presigned uploads require R2 storage configuration")
369
+ # instead of just "HTTP 501". Include up to 500 chars of the body.
370
+ local err_excerpt=""
371
+ if [ -s "$resp_body" ]; then
372
+ err_excerpt=$(head -c 500 "$resp_body")
373
+ fi
374
+ rm -f "$resp_body"
375
+ echo -n "(step 1 failed: HTTP ${http_code}"
376
+ if [ -n "$err_excerpt" ]; then
377
+ echo -n " — ${err_excerpt}"
378
+ fi
379
+ echo ") "
351
380
  return 1
352
381
  done
353
382
 
@@ -469,6 +498,7 @@ for FILE in "${FILES[@]}"; do
469
498
  [ -n "$ENVIRONMENT" ] && CURL_ARGS+=(-F "environment=${ENVIRONMENT}")
470
499
  [ -n "$EVIDENCE_CATEGORY" ] && CURL_ARGS+=(-F "evidenceCategory=${EVIDENCE_CATEGORY}")
471
500
  [ -n "$RELEASE_TITLE" ] && CURL_ARGS+=(-F "releaseTitle=${RELEASE_TITLE}")
501
+ [ -n "$RELEASE_SUMMARY" ] && CURL_ARGS+=(-F "releaseSummary=${RELEASE_SUMMARY}")
472
502
  [ -n "$CHANGE_TYPE" ] && CURL_ARGS+=(-F "changeType=${CHANGE_TYPE}")
473
503
  [ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
474
504
  [ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # close-out-contract.sh — Shared constants for release close-out automation.
3
+ #
4
+ # Sourced by close-out-release.yml.template and any CI logic that needs to
5
+ # recognise or suppress reconciliation pushes (devaudit#281, devaudit#284).
6
+ #
7
+ # Usage: source scripts/close-out-contract.sh
8
+ # CLOSEOUT_BRANCH_PREFIX — reserved automation branch prefix
9
+ # CLOSEOUT_MARKER — structured marker for PR/commit bodies
10
+ # CLOSEOUT_MARKER_REGEX — grep pattern to detect the marker
11
+
12
+ # Reserved branch prefix for automation. Humans should not use this for
13
+ # unrelated housekeeping work.
14
+ CLOSEOUT_BRANCH_PREFIX="chore/close-out-"
15
+
16
+ # Structured close-out marker emitted in the PR body / merge commit body.
17
+ # Format: Release-Closeout: REQ-XXX
18
+ CLOSEOUT_MARKER_PREFIX="Release-Closeout:"
19
+ CLOSEOUT_MARKER_REGEX='^Release-Closeout:[[:space:]]*REQ-[0-9]{3,}'
20
+
21
+ # Emit the marker for a given REQ-ID.
22
+ closeout_marker() {
23
+ printf '%s %s\n' "$CLOSEOUT_MARKER_PREFIX" "$1"
24
+ }
25
+
26
+ # Check if a string (e.g. commit body, PR body) contains the close-out marker.
27
+ is_closeout_marker() {
28
+ grep -qE "$CLOSEOUT_MARKER_REGEX" <<< "$1"
29
+ }
30
+
31
+ # Extract the REQ-ID from a close-out marker string.
32
+ closeout_marker_req() {
33
+ grep -oE "$CLOSEOUT_MARKER_REGEX" <<< "$1" | grep -oE 'REQ-[0-9]{3,}' | head -1
34
+ }
@@ -16,6 +16,12 @@
16
16
  # 4-bis. RTM.md IN PROGRESS row: exactly one tracked REQ marked
17
17
  # IN PROGRESS in compliance/RTM.md
18
18
  # -> REQ-XXX
19
+ # 4-ter. Close-out marker: commit body contains
20
+ # "Release-Closeout: REQ-XXX"
21
+ # (devaudit#284 — suppresses false
22
+ # housekeeping stubs from release
23
+ # reconciliation merges)
24
+ # -> empty/skip
19
25
  # 5. Fallback: bare date -> v2026.05.17
20
26
  #
21
27
  # Step 4 (DevAudit-Installer#92) handles `chore:` / `docs:` / `ci:`
@@ -127,5 +133,17 @@ if [ -f "$RTM_PATH" ]; then
127
133
  fi
128
134
  fi
129
135
 
136
+ # 4-ter. Close-out marker suppression (devaudit#284).
137
+ # A push that is solely the result of completing a tracked release's
138
+ # reconciliation/close-out path must not derive a bare-date housekeeping
139
+ # release. The close-out workflow (devaudit#281) emits a structured
140
+ # `Release-Closeout: REQ-XXX` marker in the merge commit body. When
141
+ # present, emit no version. Workflow callers translate the empty result
142
+ # to an explicit `skip` sentinel so reconciliation pushes neither create
143
+ # housekeeping releases nor attach new evidence to an already released REQ.
144
+ if echo "$BODY" | grep -qE '^Release-Closeout:[[:space:]]*REQ-[0-9]{3,}'; then
145
+ exit 0
146
+ fi
147
+
130
148
  # 5. Fallback: bare date in UTC
131
149
  echo "v$(date -u +%Y.%m.%d)"
@@ -266,6 +266,63 @@ cat > compliance/RTM.md <<'RTM'
266
266
  RTM
267
267
  assert_eq "RTM two IN PROGRESS w/ escaped pipes -> bare date $TODAY" "$TODAY" "$(run_helper)"
268
268
 
269
+ # Case 21: close-out reconciliation marker suppresses release registration.
270
+ # The workflow caller translates this empty stdout to version=skip so the
271
+ # reconciliation push neither creates housekeeping stubs nor attaches evidence
272
+ # to an already released REQ.
273
+ make_fixture "$WORK/c21" "chore(release): reconcile REQ-089 close-out
274
+
275
+ Release-Closeout: REQ-089"
276
+ assert_eq "close-out marker -> empty version" "" "$(run_helper)"
277
+
278
+ # Case 22 (#571 Gap 3): close-out marker with no REQ digits — must NOT
279
+ # suppress the bare-date fallback. The regex requires REQ-[0-9]{3,} so
280
+ # a malformed marker like "Release-Closeout: REQ-AB" should fall through
281
+ # to the bare date.
282
+ make_fixture "$WORK/c22" "chore(release): reconcile close-out
283
+
284
+ Release-Closeout: REQ-AB"
285
+ assert_eq "malformed close-out marker -> bare date $TODAY" "$TODAY" "$(run_helper)"
286
+
287
+ # Case 23 (#571 Gap 3): close-out marker with extra whitespace — must
288
+ # still suppress (regex allows leading whitespace after the colon).
289
+ make_fixture "$WORK/c23" "chore(release): reconcile REQ-100 close-out
290
+
291
+ Release-Closeout: REQ-100"
292
+ assert_eq "close-out marker with extra whitespace -> empty" "" "$(run_helper)"
293
+
294
+ # Case 24 (#571 Gap 3): no ticket file, no RTM, no git tags, no close-out
295
+ # marker — the ultimate fallback chain reaches the bare date. This tests
296
+ # that every prior step's `exit 0` guard correctly falls through when
297
+ # its condition is not met.
298
+ make_fixture "$WORK/c24" "chore: misc cleanup with no compliance state"
299
+ assert_eq "no compliance state at all -> bare date $TODAY" "$TODAY" "$(run_helper)"
300
+
301
+ # Case 25 (#571 Gap 3): pending-releases dir exists but is empty — step 4
302
+ # must not crash and must fall through to step 4-bis / bare date.
303
+ make_fixture "$WORK/c25" "chore: devaudit update to 0.1.30"
304
+ mkdir -p compliance/pending-releases
305
+ assert_eq "empty pending-releases dir -> bare date $TODAY" "$TODAY" "$(run_helper)"
306
+
307
+ # Case 26 (#571 Gap 3): RTM.md exists but has no IN PROGRESS rows —
308
+ # step 4-bis must fall through to the bare date.
309
+ make_fixture "$WORK/c26" "chore: devaudit update to 0.1.30"
310
+ mkdir -p compliance
311
+ cat > compliance/RTM.md <<'RTM'
312
+ | REQ-ID | Status |
313
+ | ------- | ------------- |
314
+ | REQ-001 | RELEASED |
315
+ | REQ-002 | DRAFT |
316
+ RTM
317
+ assert_eq "RTM with no IN PROGRESS -> bare date $TODAY" "$TODAY" "$(run_helper)"
318
+
319
+ # Case 27 (#571 Gap 3): RTM.md exists but is empty (no rows at all) —
320
+ # step 4-bis must not crash and must fall through.
321
+ make_fixture "$WORK/c27" "chore: devaudit update to 0.1.30"
322
+ mkdir -p compliance
323
+ echo "# Requirements Traceability Matrix" > compliance/RTM.md
324
+ assert_eq "empty RTM.md -> bare date $TODAY" "$TODAY" "$(run_helper)"
325
+
269
326
  echo ""
270
327
  echo "=== Summary: $PASS pass / $FAIL fail ==="
271
328
 
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # extract-release-metadata.sh — Extract canonical tracked-release metadata.
3
+ #
4
+ # DevAudit-Installer#285 — Provides one shared extraction source of truth for
5
+ # tracked-release title and summary, usable from both CI workflow paths
6
+ # (ci.yml.template and compliance-evidence.yml.template).
7
+ #
8
+ # Usage:
9
+ # source scripts/extract-release-metadata.sh
10
+ # extract_release_metadata "REQ-089"
11
+ #
12
+ # Sets the following shell variables:
13
+ # RELEASE_TITLE — canonical human title (empty if none found)
14
+ # RELEASE_SUMMARY — reviewer-facing summary from ## Summary section (empty if none)
15
+ #
16
+ # Title fallback chain (normative per #285):
17
+ # 1. **Requirement:** line in RELEASE-TICKET-REQ-XXX.md → human part after "REQ-XXX —"
18
+ # 2. GitHub issue title from RTM row (best-effort, requires gh CLI)
19
+ # 3. Normalised ticket H1 only if no better source exists
20
+ # 4. Empty rather than garbage
21
+ #
22
+ # Summary rules:
23
+ # - Capture content between ## Summary and the next ## heading
24
+ # - Trim boilerplate / empty placeholders
25
+ # - Empty if section absent or still a stub
26
+
27
+ set -euo pipefail
28
+
29
+ extract_release_metadata() {
30
+ local req_id="$1"
31
+ RELEASE_TITLE=""
32
+ RELEASE_SUMMARY=""
33
+
34
+ local ticket_file=""
35
+ for FILE in \
36
+ "compliance/pending-releases/RELEASE-TICKET-${req_id}.md" \
37
+ "compliance/approved-releases/RELEASE-TICKET-${req_id}.md"; do
38
+ if [ -f "$FILE" ]; then
39
+ ticket_file="$FILE"
40
+ break
41
+ fi
42
+ done
43
+
44
+ # --- Title extraction ---
45
+ # 1. Try **Requirement:** REQ-XXX — <human title>
46
+ if [ -n "$ticket_file" ]; then
47
+ local req_line
48
+ req_line=$(grep -m1 '^\*\*Requirement:\*\*' "$ticket_file" 2>/dev/null || true)
49
+ if [ -n "$req_line" ]; then
50
+ # Extract the human part after "REQ-XXX —"
51
+ # Format: **Requirement:** REQ-089 — Some human title here
52
+ RELEASE_TITLE=$(printf '%s' "$req_line" \
53
+ | sed -E 's/^\*\*Requirement:\*\*[[:space:]]*//' \
54
+ | sed -E 's/^REQ-[0-9]+[[:space:]]*[—–:-][[:space:]]*//' \
55
+ | sed -E 's/[[:space:]]*$//')
56
+ fi
57
+ fi
58
+
59
+ # 2. Fallback: GitHub issue title from RTM row
60
+ if [ -z "$RELEASE_TITLE" ]; then
61
+ local rtm_row
62
+ rtm_row=$(grep -m1 "| ${req_id} " compliance/RTM.md 2>/dev/null || true)
63
+ if [ -n "$rtm_row" ] && command -v gh >/dev/null 2>&1; then
64
+ # Try to extract issue number from RTM row if present
65
+ local issue_num
66
+ issue_num=$(printf '%s' "$rtm_row" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
67
+ if [ -n "$issue_num" ]; then
68
+ RELEASE_TITLE=$(gh issue view "$issue_num" --json title --jq '.title' 2>/dev/null || true)
69
+ fi
70
+ fi
71
+ fi
72
+
73
+ # 3. Fallback: normalised ticket H1 (least preferred)
74
+ if [ -z "$RELEASE_TITLE" ] && [ -n "$ticket_file" ]; then
75
+ local h1_line
76
+ h1_line=$(grep -m1 '^# ' "$ticket_file" 2>/dev/null || true)
77
+ if [ -n "$h1_line" ]; then
78
+ # Strip the leading "# " and any "Release Ticket — REQ-XXX" prefix
79
+ RELEASE_TITLE=$(printf '%s' "$h1_line" \
80
+ | sed -E 's/^# *//' \
81
+ | sed -E 's/^Release Ticket[[:space:]]*[—:-][[:space:]]*//' \
82
+ | sed -E 's/^REQ-[0-9]+[[:space:]]*[—:-][[:space:]]*//' \
83
+ | sed -E 's/[[:space:]]*$//')
84
+ fi
85
+ fi
86
+
87
+ # 4. If still empty, leave empty rather than inventing garbage
88
+
89
+ # --- Summary extraction ---
90
+ # Capture content between ## Summary and the next ## heading
91
+ local summary_raw
92
+ summary_raw=""
93
+ if [ -n "$ticket_file" ]; then
94
+ summary_raw=$(awk '
95
+ /^## Summary/ { found=1; next }
96
+ /^## / { if (found) exit }
97
+ found { print }
98
+ ' "$ticket_file" 2>/dev/null || true)
99
+ fi
100
+
101
+ if [ -n "$summary_raw" ]; then
102
+ # Trim leading/trailing blank lines and trailing whitespace per line
103
+ RELEASE_SUMMARY=$(printf '%s' "$summary_raw" \
104
+ | sed -E 's/[[:space:]]+$//' \
105
+ | awk 'NF { p=1 } p { print }' \
106
+ | awk '{ lines[NR]=$0 } END { for (i=NR; i>=1; i--) { if (lines[i] ~ /[^[:space:]]/) { last=i; break } } for (i=1; i<=last; i++) print lines[i] }')
107
+ fi
108
+
109
+ # If summary is just placeholder/stub text, clear it
110
+ if [ -n "$RELEASE_SUMMARY" ]; then
111
+ local summary_lower
112
+ summary_lower=$(printf '%s' "$RELEASE_SUMMARY" | tr '[:upper:]' '[:lower:]')
113
+ case "$summary_lower" in
114
+ *"placeholder"*|*"todo"*|*"tbd"*|*"to be completed"*|*"stub"*)
115
+ RELEASE_SUMMARY=""
116
+ ;;
117
+ esac
118
+ fi
119
+ }