@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/README.md +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +33 -3
- package/sdlc/files/_common/scripts/close-out-contract.sh +34 -0
- package/sdlc/files/_common/scripts/derive-release-version.sh +18 -0
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +57 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +119 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +363 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +593 -0
- package/sdlc/files/_common/scripts/validate-test-summary.sh +37 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +87 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +16 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +7 -4
- package/sdlc/files/ci/check-release-approval.yml.template +80 -21
- package/sdlc/files/ci/ci-status-fallback.yml.template +3 -0
- package/sdlc/files/ci/ci.yml.template +24 -33
- package/sdlc/files/ci/close-out-release.yml.template +67 -11
- package/sdlc/files/ci/compliance-evidence.yml.template +32 -16
- package/sdlc/files/ci/quality-gates-provenance.yml.template +62 -0
- package/sdlc/files/stacks/node/hooks/pre-push +58 -8
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +22 -2
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +20 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|