@metasession.co/devaudit-cli 0.3.8 → 0.3.10

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.8",
3
+ "version": "0.3.10",
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.8",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.3.10",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -54,6 +54,14 @@
54
54
  # Issue from: Project Settings → API Keys in
55
55
  # META-COMPLY's web UI.
56
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
+ #
57
65
  # Examples:
58
66
  # ./scripts/upload-evidence.sh meta-ats REQ-001 screenshot \
59
67
  # compliance/evidence/REQ-001/screenshots/
@@ -345,9 +353,9 @@ upload_presigned() {
345
353
  echo -n "(portal did not return presigned URL, falling back to multipart) "
346
354
  return 255
347
355
  fi
348
- rm -f "$resp_body"
349
356
  if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
350
357
  if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
358
+ rm -f "$resp_body"
351
359
  echo -n "(step 1: HTTP ${http_code}, retry in ${backoff}s) "
352
360
  sleep "$backoff"
353
361
  attempt=$((attempt + 1))
@@ -355,8 +363,20 @@ upload_presigned() {
355
363
  continue
356
364
  fi
357
365
  fi
358
- # Non-retriable error (4xx other than 429).
359
- 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 ") "
360
380
  return 1
361
381
  done
362
382
 
@@ -152,9 +152,10 @@ Two distinct credentials exist; conflating them is what causes the silent-CI-tok
152
152
  |---|---|---|---|---|
153
153
  | **Personal PAT** | `mctok_…` | `~/.config/devaudit/auth.json` (per developer) | You (the user) | Your local CLI commands |
154
154
  | **Project API key** | `dak_…` | Repo secret `DEVAUDIT_API_KEY` | The project | CI's `devaudit push` calls |
155
- | **Operator's PAT** | `mctok_…` | Repo secret `DEVAUDIT_USER_TOKEN` | The operator (singular) | CI's mutation calls (release register, approval submit) |
155
+ | **Operator's PAT** | `mctok_…` | Repo secret `DEVAUDIT_USER_TOKEN` | The operator (singular) | CI's DevAudit portal calls that need human attribution (release submit / approval flows) |
156
+ | **GitHub workflow token** | `${{ github.token }}` | Issued automatically per workflow run | The GitHub Actions workflow | CI's GitHub repo mutations (checkout, branch push, PR, issue, comment, label, check-run updates) |
156
157
 
157
- **Never paste your personal PAT into a repo secret.** Repo `DEVAUDIT_USER_TOKEN` is operator-owned. CI portal mutations are attributed to whoever's PAT is there — if it's yours, your name shows up against every release the team ships, and the moment your PAT expires CI breaks. Rotation belongs to the operator: `devaudit install --force-team-config` on their machine.
158
+ **Never paste your personal PAT into a repo secret.** Repo `DEVAUDIT_USER_TOKEN` is operator-owned and is not the default GitHub auth path for workflow repo mutations. CI portal mutations are attributed to whoever's PAT is there — if it's yours, your name shows up against every release the team ships, and the moment your PAT expires CI portal actions break. Rotation belongs to the operator: `devaudit install --force-team-config` on their machine.
158
159
 
159
160
  If `devaudit auth status` shows the wrong user, run `devaudit auth logout && devaudit auth login` and paste the right PAT.
160
161
 
@@ -181,10 +182,10 @@ The synced CI gates expect a specific environment. Here's what you need locally
181
182
 
182
183
  | Surface | CI | Local (your machine) |
183
184
  |---|---|---|
184
- | Personal identity | `secrets.DEVAUDIT_USER_TOKEN` (operator's) | `~/.config/devaudit/auth.json` (yours) — `devaudit auth login` |
185
+ | Personal identity | `secrets.DEVAUDIT_USER_TOKEN` (operator's, portal-only) | `~/.config/devaudit/auth.json` (yours) — `devaudit auth login` |
185
186
  | Project API key | `secrets.DEVAUDIT_API_KEY` | Usually unset locally — only needed if you're testing `devaudit push` against the live portal; ask the operator if you need to debug it |
186
187
  | Portal URL | `vars.DEVAUDIT_BASE_URL` | `~/.config/devaudit/auth.json` (set by `auth login`) or `$DEVAUDIT_BASE_URL` env |
187
- | GitHub auth | `${{ github.token }}` (auto) | `gh auth login` |
188
+ | GitHub auth | `${{ github.token }}` (auto, repo mutations) | `gh auth login` |
188
189
  | Node | matrix-pinned to project's `node_version` | nvm / volta / whatever — `devaudit doctor` checks ≥ 22 |
189
190
  | `jq` / `curl` | always present on GH runners | install via package manager — `devaudit doctor` flags absence |
190
191
 
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # close-out-contract.test.sh — Tests for the close-out marker helper.
3
+ #
4
+ # Usage:
5
+ # ./scripts/close-out-contract.test.sh
6
+
7
+ set -euo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ # shellcheck source=/dev/null
11
+ source "$SCRIPT_DIR/close-out-contract.sh"
12
+
13
+ PASS=0
14
+ FAIL=0
15
+
16
+ assert_eq() {
17
+ local desc="$1" want="$2" got="$3"
18
+ if [ "$got" = "$want" ]; then
19
+ echo " PASS: $desc"
20
+ PASS=$((PASS + 1))
21
+ else
22
+ echo " FAIL: $desc"
23
+ echo " want: $want"
24
+ echo " got: $got"
25
+ FAIL=$((FAIL + 1))
26
+ fi
27
+ }
28
+
29
+ assert_true() {
30
+ local desc="$1"
31
+ shift
32
+ if "$@"; then
33
+ echo " PASS: $desc"
34
+ PASS=$((PASS + 1))
35
+ else
36
+ echo " FAIL: $desc"
37
+ FAIL=$((FAIL + 1))
38
+ fi
39
+ }
40
+
41
+ assert_false() {
42
+ local desc="$1"
43
+ shift
44
+ if "$@"; then
45
+ echo " FAIL: $desc"
46
+ FAIL=$((FAIL + 1))
47
+ else
48
+ echo " PASS: $desc"
49
+ PASS=$((PASS + 1))
50
+ fi
51
+ }
52
+
53
+ echo "=== close-out-contract.sh tests ==="
54
+
55
+ assert_eq "branch prefix constant" "chore/close-out-" "$CLOSEOUT_BRANCH_PREFIX"
56
+ assert_eq "marker prefix constant" "Release-Closeout:" "$CLOSEOUT_MARKER_PREFIX"
57
+ assert_eq "closeout_marker emits structured line" "Release-Closeout: REQ-090" "$(closeout_marker REQ-090)"
58
+
59
+ assert_true "is_closeout_marker accepts canonical marker" is_closeout_marker "Release-Closeout: REQ-090"
60
+ assert_true "is_closeout_marker accepts extra whitespace" is_closeout_marker "Release-Closeout: REQ-123"
61
+ assert_false "is_closeout_marker rejects wrong prefix" is_closeout_marker "Closeout: REQ-090"
62
+ assert_false "is_closeout_marker rejects short req ids" is_closeout_marker "Release-Closeout: REQ-9"
63
+ assert_false "is_closeout_marker rejects bare prose mention" is_closeout_marker "This closes REQ-090 after release."
64
+
65
+ assert_eq "closeout_marker_req extracts req id" "REQ-090" "$(closeout_marker_req "Release-Closeout: REQ-090")"
66
+ assert_eq "closeout_marker_req extracts first req id from mixed text" "REQ-111" "$(closeout_marker_req $'x\nRelease-Closeout: REQ-111\nmore')"
67
+ assert_eq "closeout_marker_req returns empty when marker absent" "" "$(closeout_marker_req "no marker here" || true)"
68
+
69
+ echo
70
+ echo "Result: $PASS passed, $FAIL failed"
71
+ [ "$FAIL" = "0" ]
@@ -275,6 +275,54 @@ make_fixture "$WORK/c21" "chore(release): reconcile REQ-089 close-out
275
275
  Release-Closeout: REQ-089"
276
276
  assert_eq "close-out marker -> empty version" "" "$(run_helper)"
277
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
+
278
326
  echo ""
279
327
  echo "=== Summary: $PASS pass / $FAIL fail ==="
280
328
 
@@ -273,6 +273,91 @@ assert_eq "RTM issue title fallback without ticket" "Issue title from RTM" "$REL
273
273
  assert_empty "Summary empty without ticket" "$RELEASE_SUMMARY"
274
274
  echo ""
275
275
 
276
+ # --- Test 12 (#571 Gap 3): No ticket, no RTM, no gh CLI — all fallbacks empty ---
277
+ echo "--- Test 12: All fallbacks unreachable ---"
278
+ make_fixture "$WORK/test12"
279
+ source "$HELPER"
280
+ extract_release_metadata "REQ-999"
281
+ assert_empty "Title empty when no ticket, no RTM, no gh" "$RELEASE_TITLE"
282
+ assert_empty "Summary empty when no ticket" "$RELEASE_SUMMARY"
283
+ echo ""
284
+
285
+ # --- Test 13 (#571 Gap 3): Ticket exists but has no **Requirement:** line,
286
+ # no H1, and no ## Summary — all extraction targets are unreachable ---
287
+ echo "--- Test 13: Ticket with no extractable content ---"
288
+ make_fixture "$WORK/test13"
289
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-012.md" <<'TICKET'
290
+ Some prose without any standard headings.
291
+
292
+ Just a paragraph.
293
+ TICKET
294
+
295
+ source "$HELPER"
296
+ extract_release_metadata "REQ-012"
297
+ assert_empty "Title empty when no Requirement line or H1" "$RELEASE_TITLE"
298
+ assert_empty "Summary empty when no ## Summary section" "$RELEASE_SUMMARY"
299
+ echo ""
300
+
301
+ # --- Test 14 (#571 Gap 3): RTM row exists but has no issue number,
302
+ # so the gh CLI fallback is unreachable. Title should be empty. ---
303
+ echo "--- Test 14: RTM row without issue number ---"
304
+ make_fixture "$WORK/test14"
305
+ cat > compliance/RTM.md <<'RTM'
306
+ | REQ-ID | Issue | Risk | Evidence | Status |
307
+ | ------- | ----- | ---- | -------- | ------ |
308
+ | REQ-013 | | LOW | n/a | DRAFT |
309
+ RTM
310
+ source "$HELPER"
311
+ extract_release_metadata "REQ-013"
312
+ assert_empty "Title empty when RTM row has no issue number" "$RELEASE_TITLE"
313
+ echo ""
314
+
315
+ # --- Test 15 (#571 Gap 3): gh CLI exists but issue lookup fails —
316
+ # must fall through to H1 fallback, not error out ---
317
+ echo "--- Test 15: gh CLI fails, falls through to H1 ---"
318
+ make_fixture "$WORK/test15"
319
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-014.md" <<'TICKET'
320
+ # Fix critical payment bug
321
+
322
+ ## Summary
323
+ Fixes the payment redirect issue.
324
+ TICKET
325
+ cat > compliance/RTM.md <<'RTM'
326
+ | REQ-ID | Issue | Risk | Evidence | Status |
327
+ | ------- | ----- | ---- | -------- | ------ |
328
+ | REQ-014 | #999 | LOW | n/a | DRAFT |
329
+ RTM
330
+ mkdir -p bin
331
+ cat > bin/gh <<'GH'
332
+ #!/usr/bin/env bash
333
+ # Simulate gh CLI failing (e.g. rate limited, issue not found)
334
+ exit 1
335
+ GH
336
+ chmod +x bin/gh
337
+ PATH="$PWD/bin:$PATH"
338
+ source "$HELPER"
339
+ extract_release_metadata "REQ-014"
340
+ assert_eq "H1 fallback when gh CLI fails" "Fix critical payment bug" "$RELEASE_TITLE"
341
+ echo ""
342
+
343
+ # --- Test 16 (#571 Gap 3): Summary section exists but is only whitespace ---
344
+ echo "--- Test 16: Whitespace-only summary is cleared ---"
345
+ make_fixture "$WORK/test16"
346
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-015.md" <<'TICKET'
347
+ **Requirement:** REQ-015 — Feature fifteen
348
+
349
+ ## Summary
350
+
351
+ ## Changes
352
+ - Change A
353
+ TICKET
354
+
355
+ source "$HELPER"
356
+ extract_release_metadata "REQ-015"
357
+ assert_eq "Title extracted" "Feature fifteen" "$RELEASE_TITLE"
358
+ assert_empty "Whitespace-only summary cleared" "$RELEASE_SUMMARY"
359
+ echo ""
360
+
276
361
  echo ""
277
362
  echo "=== Results: $PASS passed, $FAIL failed ==="
278
363
  exit $FAIL
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # prepare-release-pr.sh — ensure there is one truthful release PR.
3
+ #
4
+ # Usage:
5
+ # ./scripts/prepare-release-pr.sh [--apply] [--mode=update|recreate]
6
+ #
7
+ # Default mode is dry-run + update. Dry-run prints the action it would take.
8
+ # With --apply:
9
+ # - no existing PR: creates one
10
+ # - current PR: updates title/body only when needed
11
+ # - stale PR + mode=update: rewrites title/body and comments
12
+ # - stale PR + mode=recreate: closes old PR and opens a fresh one
13
+
14
+ set -euo pipefail
15
+
16
+ APPLY=false
17
+ MODE="update"
18
+ for arg in "$@"; do
19
+ case "$arg" in
20
+ --apply) APPLY=true ;;
21
+ --mode=update) MODE="update" ;;
22
+ --mode=recreate) MODE="recreate" ;;
23
+ *) echo "Unknown argument: $arg" >&2; exit 2 ;;
24
+ esac
25
+ done
26
+
27
+ INTEGRATION_BRANCH=$(jq -r '.integration_branch // "develop"' sdlc-config.json 2>/dev/null || echo "develop")
28
+ RELEASE_BRANCH=$(jq -r '.release_branch // "main"' sdlc-config.json 2>/dev/null || echo "main")
29
+
30
+ if [ ! -x scripts/derive-release-version.sh ]; then
31
+ chmod +x scripts/derive-release-version.sh 2>/dev/null || true
32
+ fi
33
+ CURRENT_RELEASE=$(./scripts/derive-release-version.sh)
34
+ if [ -z "$CURRENT_RELEASE" ]; then
35
+ echo "No active release version derived; close-out reconciliation appears in progress." >&2
36
+ exit 1
37
+ fi
38
+
39
+ TITLE="Release: ${CURRENT_RELEASE}"
40
+ BODY=$(cat <<EOF
41
+ ## Release
42
+
43
+ - Release: ${CURRENT_RELEASE}
44
+ - Base: ${RELEASE_BRANCH}
45
+ - Head: ${INTEGRATION_BRANCH}
46
+
47
+ ## DevAudit
48
+
49
+ - Release context is derived from the current ${INTEGRATION_BRANCH} head.
50
+ - If this PR replaced an older release PR, the older PR was superseded because it no longer matched the governing release context.
51
+ EOF
52
+ )
53
+
54
+ OPEN_PRS=$(gh pr list --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --state open --json number,title,body,url --limit 10)
55
+ COUNT=$(echo "$OPEN_PRS" | jq 'length')
56
+
57
+ if [ "$COUNT" -eq 0 ]; then
58
+ echo "No open ${INTEGRATION_BRANCH}->${RELEASE_BRANCH} PR found."
59
+ if [ "$APPLY" = "true" ]; then
60
+ gh pr create --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --title "$TITLE" --body "$BODY"
61
+ else
62
+ echo "Dry run: would create release PR titled '$TITLE'."
63
+ fi
64
+ exit 0
65
+ fi
66
+
67
+ if [ "$COUNT" -gt 1 ]; then
68
+ echo "ERROR: ${COUNT} open ${INTEGRATION_BRANCH}->${RELEASE_BRANCH} PRs found. Resolve duplicates manually." >&2
69
+ echo "$OPEN_PRS" | jq -r '.[] | " #\(.number) \(.title) \(.url)"' >&2
70
+ exit 1
71
+ fi
72
+
73
+ PR_NUMBER=$(echo "$OPEN_PRS" | jq -r '.[0].number')
74
+ PR_TITLE=$(echo "$OPEN_PRS" | jq -r '.[0].title // ""')
75
+ PR_BODY=$(echo "$OPEN_PRS" | jq -r '.[0].body // ""')
76
+ PR_URL=$(echo "$OPEN_PRS" | jq -r '.[0].url')
77
+
78
+ if printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -q "$CURRENT_RELEASE"; then
79
+ echo "Open release PR #${PR_NUMBER} already matches ${CURRENT_RELEASE}: ${PR_URL}"
80
+ exit 0
81
+ fi
82
+
83
+ echo "Open release PR #${PR_NUMBER} is stale for current release ${CURRENT_RELEASE}: ${PR_URL}"
84
+ echo "Current title: ${PR_TITLE}"
85
+
86
+ if [ "$APPLY" != "true" ]; then
87
+ echo "Dry run: would ${MODE} stale PR #${PR_NUMBER} for ${CURRENT_RELEASE}."
88
+ exit 0
89
+ fi
90
+
91
+ if [ "$MODE" = "recreate" ]; then
92
+ gh pr comment "$PR_NUMBER" --body "Superseded: current ${INTEGRATION_BRANCH} head now derives ${CURRENT_RELEASE}; this PR no longer matches the governing release context."
93
+ gh pr close "$PR_NUMBER" --comment "Closed as superseded by ${CURRENT_RELEASE} release context."
94
+ gh pr create --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --title "$TITLE" --body "$BODY"
95
+ else
96
+ gh pr edit "$PR_NUMBER" --title "$TITLE" --body "$BODY"
97
+ gh pr comment "$PR_NUMBER" --body "Release PR context refreshed: current ${INTEGRATION_BRANCH} head derives ${CURRENT_RELEASE}."
98
+ echo "Updated PR #${PR_NUMBER} to ${CURRENT_RELEASE}: ${PR_URL}"
99
+ fi