@metasession.co/devaudit-cli 0.3.7 → 0.3.8

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.8",
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.8",
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
@@ -80,6 +85,7 @@ CREATE_RELEASE_IF_MISSING=false
80
85
  ENVIRONMENT=""
81
86
  EVIDENCE_CATEGORY=""
82
87
  RELEASE_TITLE=""
88
+ RELEASE_SUMMARY=""
83
89
  CHANGE_TYPE=""
84
90
  GATE_STATUS=""
85
91
  SDLC_STAGE=""
@@ -103,6 +109,7 @@ while [ "$#" -gt 0 ]; do
103
109
  # the portal's findOrCreateRelease no-clobber backfill. Both optional;
104
110
  # unknown change-type values are dropped server-side, not 400'd.
105
111
  --release-title) RELEASE_TITLE="$2"; shift 2 ;;
112
+ --release-summary) RELEASE_SUMMARY="$2"; shift 2 ;;
106
113
  --change-type) CHANGE_TYPE="$2"; shift 2 ;;
107
114
  # passed/failed/skipped — surfaces failed gates on the portal so
108
115
  # ran-and-failed != never-ran. Unknown values dropped server-side.
@@ -320,6 +327,8 @@ upload_presigned() {
320
327
  \"releaseBranch\": \"${BRANCH}\",
321
328
  \"environment\": \"${ENVIRONMENT}\",
322
329
  \"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
330
+ \"releaseTitle\": \"${RELEASE_TITLE}\",
331
+ \"releaseSummary\": \"${RELEASE_SUMMARY}\",
323
332
  \"sdlcStage\": \"${SDLC_STAGE}\",
324
333
  \"testCycleId\": \"${TEST_CYCLE}\"
325
334
  }") || curl_exit=$?
@@ -469,6 +478,7 @@ for FILE in "${FILES[@]}"; do
469
478
  [ -n "$ENVIRONMENT" ] && CURL_ARGS+=(-F "environment=${ENVIRONMENT}")
470
479
  [ -n "$EVIDENCE_CATEGORY" ] && CURL_ARGS+=(-F "evidenceCategory=${EVIDENCE_CATEGORY}")
471
480
  [ -n "$RELEASE_TITLE" ] && CURL_ARGS+=(-F "releaseTitle=${RELEASE_TITLE}")
481
+ [ -n "$RELEASE_SUMMARY" ] && CURL_ARGS+=(-F "releaseSummary=${RELEASE_SUMMARY}")
472
482
  [ -n "$CHANGE_TYPE" ] && CURL_ARGS+=(-F "changeType=${CHANGE_TYPE}")
473
483
  [ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
474
484
  [ -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,15 @@ 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
+
269
278
  echo ""
270
279
  echo "=== Summary: $PASS pass / $FAIL fail ==="
271
280
 
@@ -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
+ }
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env bash
2
+ # extract-release-metadata.test.sh — Tests for extract-release-metadata.sh.
3
+ #
4
+ # Creates throwaway release-ticket fixtures, sources the helper, and asserts
5
+ # on the extracted title and summary. Hermetic: runs inside mktemp'd
6
+ # directories that are torn down at the end.
7
+ #
8
+ # Usage:
9
+ # ./scripts/extract-release-metadata.test.sh
10
+
11
+ set -euo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ HELPER="$SCRIPT_DIR/extract-release-metadata.sh"
15
+ [ -x "$HELPER" ] || chmod +x "$HELPER"
16
+
17
+ PASS=0
18
+ FAIL=0
19
+
20
+ WORK=$(mktemp -d)
21
+ trap 'rm -rf "$WORK"' EXIT
22
+
23
+ # $1 = test name, $2 = expected, $3 = actual
24
+ assert_eq() {
25
+ local name="$1" expected="$2" actual="$3"
26
+ if [ "$expected" = "$actual" ]; then
27
+ echo " ✓ $name"
28
+ PASS=$((PASS + 1))
29
+ else
30
+ echo " ✗ $name"
31
+ echo " Expected: $expected"
32
+ echo " Actual: $actual"
33
+ FAIL=$((FAIL + 1))
34
+ fi
35
+ }
36
+
37
+ assert_empty() {
38
+ local name="$1" actual="$2"
39
+ if [ -z "$actual" ]; then
40
+ echo " ✓ $name"
41
+ PASS=$((PASS + 1))
42
+ else
43
+ echo " ✗ $name (expected empty, got: $actual)"
44
+ FAIL=$((FAIL + 1))
45
+ fi
46
+ }
47
+
48
+ # $1 = test dir
49
+ make_fixture() {
50
+ local dir="$1"
51
+ rm -rf "$dir"
52
+ mkdir -p "$dir/compliance/pending-releases"
53
+ mkdir -p "$dir/compliance/approved-releases"
54
+ cd "$dir"
55
+ }
56
+
57
+ echo "=== extract-release-metadata.sh tests ==="
58
+ echo ""
59
+
60
+ # --- Test 1: Extract title from **Requirement:** line ---
61
+ echo "--- Test 1: Title from **Requirement:** line ---"
62
+ make_fixture "$WORK/test1"
63
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-089.md" <<'TICKET'
64
+ # Release Ticket — REQ-089
65
+
66
+ **Requirement:** REQ-089 — Admin order management: portion size selection, manual price override, per-item special instructions, stock validation
67
+
68
+ ## Summary
69
+ This release implements admin order management features including portion size
70
+ selection, manual price override, and per-item special instructions.
71
+
72
+ ## Changes
73
+ - Added portion size dropdown
74
+ - Added manual price override
75
+ TICKET
76
+
77
+ source "$HELPER"
78
+ extract_release_metadata "REQ-089"
79
+ assert_eq "Title from Requirement line" \
80
+ "Admin order management: portion size selection, manual price override, per-item special instructions, stock validation" \
81
+ "$RELEASE_TITLE"
82
+ assert_eq "Summary extracted" \
83
+ "This release implements admin order management features including portion size
84
+ selection, manual price override, and per-item special instructions." \
85
+ "$RELEASE_SUMMARY"
86
+ echo ""
87
+
88
+ # --- Test 2: Fallback to H1 when no **Requirement:** line ---
89
+ echo "--- Test 2: Fallback to normalised H1 ---"
90
+ make_fixture "$WORK/test2"
91
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-001.md" <<'TICKET'
92
+ # Release Ticket — REQ-001
93
+
94
+ ## Summary
95
+ Some summary here.
96
+ TICKET
97
+
98
+ source "$HELPER"
99
+ extract_release_metadata "REQ-001"
100
+ assert_eq "H1 fallback strips 'Release Ticket —' prefix" \
101
+ "REQ-001" \
102
+ "$RELEASE_TITLE"
103
+ echo ""
104
+
105
+ # --- Test 3: Empty title when no ticket exists ---
106
+ echo "--- Test 3: Empty title when no ticket ---"
107
+ make_fixture "$WORK/test3"
108
+ source "$HELPER"
109
+ extract_release_metadata "REQ-999"
110
+ assert_empty "Title empty when no ticket" "$RELEASE_TITLE"
111
+ assert_empty "Summary empty when no ticket" "$RELEASE_SUMMARY"
112
+ echo ""
113
+
114
+ # --- Test 4: Summary with placeholder text is cleared ---
115
+ echo "--- Test 4: Placeholder summary is cleared ---"
116
+ make_fixture "$WORK/test4"
117
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-002.md" <<'TICKET'
118
+ # Release Ticket — REQ-002
119
+
120
+ **Requirement:** REQ-002 — Some feature
121
+
122
+ ## Summary
123
+ TODO: Add summary
124
+
125
+ ## Changes
126
+ - Stuff
127
+ TICKET
128
+
129
+ source "$HELPER"
130
+ extract_release_metadata "REQ-002"
131
+ assert_eq "Title still extracted" "Some feature" "$RELEASE_TITLE"
132
+ assert_empty "Placeholder summary cleared" "$RELEASE_SUMMARY"
133
+ echo ""
134
+
135
+ # --- Test 5: No Summary section → empty summary ---
136
+ echo "--- Test 5: No Summary section ---"
137
+ make_fixture "$WORK/test5"
138
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-003.md" <<'TICKET'
139
+ # Release Ticket — REQ-003
140
+
141
+ **Requirement:** REQ-003 — Another feature
142
+
143
+ ## Changes
144
+ - Change A
145
+ TICKET
146
+
147
+ source "$HELPER"
148
+ extract_release_metadata "REQ-003"
149
+ assert_eq "Title extracted" "Another feature" "$RELEASE_TITLE"
150
+ assert_empty "Summary empty when no section" "$RELEASE_SUMMARY"
151
+ echo ""
152
+
153
+ # --- Test 6: Summary stops at next ## heading ---
154
+ echo "--- Test 6: Summary stops at next ## ---"
155
+ make_fixture "$WORK/test6"
156
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-004.md" <<'TICKET'
157
+ # Release Ticket — REQ-004
158
+
159
+ **Requirement:** REQ-004 — Feature four
160
+
161
+ ## Summary
162
+ Line one of summary.
163
+ Line two of summary.
164
+
165
+ ## Changes
166
+ This should NOT be in summary.
167
+ TICKET
168
+
169
+ source "$HELPER"
170
+ extract_release_metadata "REQ-004"
171
+ assert_eq "Title" "Feature four" "$RELEASE_TITLE"
172
+ assert_eq "Summary stops at next ##" \
173
+ "Line one of summary.
174
+ Line two of summary." \
175
+ "$RELEASE_SUMMARY"
176
+ echo ""
177
+
178
+ # --- Test 7: Approved-releases fallback when no pending ticket ---
179
+ echo "--- Test 7: Approved-releases fallback ---"
180
+ make_fixture "$WORK/test7"
181
+ cat > "compliance/approved-releases/RELEASE-TICKET-REQ-005.md" <<'TICKET'
182
+ # Release Ticket — REQ-005
183
+
184
+ **Requirement:** REQ-005 — Approved feature
185
+
186
+ ## Summary
187
+ Summary from approved ticket.
188
+ TICKET
189
+
190
+ source "$HELPER"
191
+ extract_release_metadata "REQ-005"
192
+ assert_eq "Title from approved-releases" "Approved feature" "$RELEASE_TITLE"
193
+ assert_eq "Summary from approved-releases" "Summary from approved ticket." "$RELEASE_SUMMARY"
194
+ echo ""
195
+
196
+ # --- Test 8: Em-dash variant in Requirement line ---
197
+ echo "--- Test 8: Em-dash variant ---"
198
+ make_fixture "$WORK/test8"
199
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-006.md" <<'TICKET'
200
+ # Release Ticket — REQ-006
201
+
202
+ **Requirement:** REQ-006 — Feature with em-dash
203
+
204
+ ## Summary
205
+ Summary.
206
+ TICKET
207
+
208
+ source "$HELPER"
209
+ extract_release_metadata "REQ-006"
210
+ assert_eq "Em-dash title" "Feature with em-dash" "$RELEASE_TITLE"
211
+ echo ""
212
+
213
+ # --- Test 9: H1 with human title (not just REQ-XXX) ---
214
+ echo "--- Test 9: H1 with human title fallback ---"
215
+ make_fixture "$WORK/test9"
216
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-007.md" <<'TICKET'
217
+ # Fix login redirect bug
218
+
219
+ ## Summary
220
+ Fixes the redirect issue.
221
+ TICKET
222
+
223
+ source "$HELPER"
224
+ extract_release_metadata "REQ-007"
225
+ assert_eq "H1 human title fallback" "Fix login redirect bug" "$RELEASE_TITLE"
226
+ echo ""
227
+
228
+ # --- Test 10: Summary with leading/trailing blank lines trimmed ---
229
+ echo "--- Test 10: Summary blank line trimming ---"
230
+ make_fixture "$WORK/test10"
231
+ printf '%s\n' \
232
+ '# Release Ticket — REQ-008' \
233
+ '' \
234
+ '**Requirement:** REQ-008 — Feature eight' \
235
+ '' \
236
+ '## Summary' \
237
+ '' \
238
+ '' \
239
+ 'Actual summary content.' \
240
+ '' \
241
+ '' \
242
+ '## Changes' \
243
+ '- Change' > "compliance/pending-releases/RELEASE-TICKET-REQ-008.md"
244
+
245
+ source "$HELPER"
246
+ extract_release_metadata "REQ-008"
247
+ assert_eq "Title" "Feature eight" "$RELEASE_TITLE"
248
+ assert_eq "Summary trimmed" "Actual summary content." "$RELEASE_SUMMARY"
249
+ echo ""
250
+
251
+ # --- Test 11: RTM issue title fallback without release ticket ---
252
+ echo "--- Test 11: RTM issue title fallback without release ticket ---"
253
+ make_fixture "$WORK/test11"
254
+ cat > compliance/RTM.md <<'RTM'
255
+ | REQ-ID | Issue | Risk | Evidence | Status |
256
+ | ------- | ----- | ---- | -------- | ------ |
257
+ | REQ-011 | #456 | LOW | n/a | DRAFT |
258
+ RTM
259
+ mkdir -p bin
260
+ cat > bin/gh <<'GH'
261
+ #!/usr/bin/env bash
262
+ if [ "$1" = "issue" ] && [ "$2" = "view" ] && [ "$3" = "456" ]; then
263
+ printf '%s\n' "Issue title from RTM"
264
+ exit 0
265
+ fi
266
+ exit 1
267
+ GH
268
+ chmod +x bin/gh
269
+ PATH="$PWD/bin:$PATH"
270
+ source "$HELPER"
271
+ extract_release_metadata "REQ-011"
272
+ assert_eq "RTM issue title fallback without ticket" "Issue title from RTM" "$RELEASE_TITLE"
273
+ assert_empty "Summary empty without ticket" "$RELEASE_SUMMARY"
274
+ echo ""
275
+
276
+ echo ""
277
+ echo "=== Results: $PASS passed, $FAIL failed ==="
278
+ exit $FAIL
@@ -133,6 +133,43 @@ for REQ in $REQUIREMENTS; do
133
133
  EXIT_CODE=1
134
134
  fi
135
135
 
136
+ # Check 7: If E2E gate result is SKIPPED, require an ## Accepted skips section (devaudit-installer#279)
137
+ if grep -qi '| *e2e.*| *skipped' "$SUMMARY" 2>/dev/null; then
138
+ if ! grep -qi '## Accepted skips' "$SUMMARY" 2>/dev/null; then
139
+ echo " ERROR: E2E gate result is SKIPPED but no '## Accepted skips' section found (devaudit-installer#279)"
140
+ echo " Add an '## Accepted skips' section with a table row for each accepted skip."
141
+ EXIT_CODE=1
142
+ else
143
+ # Extract the Accepted skips section and check for data rows
144
+ ACCEPTED_SKIPS_SECTION=$(awk '/^## Accepted skips/{found=1; next} /^## /{found=0} found' "$SUMMARY")
145
+ # Count rows that look like table data (start with |, not separator, not header)
146
+ # Separator rows contain only dashes and pipes: |------|------|
147
+ # Header rows contain column names: | Spec | Test | ...
148
+ # Data rows contain actual values: | e2e/foo.spec.ts | ... |
149
+ DATA_ROWS=$(echo "$ACCEPTED_SKIPS_SECTION" | grep '^|' | grep -v '^|[[:space:]]*-' | grep -v 'Spec.*Test.*REQ/AC' | wc -l || true)
150
+ if [ "$DATA_ROWS" -lt 1 ] 2>/dev/null; then
151
+ echo " ERROR: '## Accepted skips' section exists but has no table data rows (devaudit-installer#279)"
152
+ echo " Add at least one row with: Spec, Test, REQ/AC, Classification, Resolution attempted, Approved by, Rationale"
153
+ EXIT_CODE=1
154
+ else
155
+ # Check that each data row has a non-empty "Approved by" column (6th content column = field 7 with awk -F|)
156
+ SKIP_FAIL=0
157
+ echo "$ACCEPTED_SKIPS_SECTION" | grep '^|' | grep -v '^|[[:space:]]*-' | grep -v 'Spec.*Test.*REQ/AC' | while IFS= read -r row; do
158
+ APPROVED_BY=$(echo "$row" | awk -F'|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$7); print $7}')
159
+ if [ -z "$APPROVED_BY" ]; then
160
+ echo " ERROR: Accepted skip row has empty 'Approved by' value (devaudit-installer#279)"
161
+ echo " Row: $row"
162
+ fi
163
+ done | grep -q ERROR && SKIP_FAIL=1
164
+ if [ "$SKIP_FAIL" -eq 1 ]; then
165
+ EXIT_CODE=1
166
+ else
167
+ echo " OK: E2E gate result is SKIPPED with '## Accepted skips' section"
168
+ fi
169
+ fi
170
+ fi
171
+ fi
172
+
136
173
  echo ""
137
174
  done
138
175