@metasession.co/devaudit-cli 0.3.12 → 0.3.13
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/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/files/_common/0-project-setup.md +10 -4
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
- package/sdlc/files/_common/README_TEMPLATE.md +8 -8
- package/sdlc/files/_common/Test_Architecture.md +15 -15
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
- package/sdlc/files/_common/Test_Policy.md +2 -2
- package/sdlc/files/_common/Test_Strategy.md +10 -10
- package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
- package/sdlc/files/_common/governance/dpia.md.template +3 -3
- package/sdlc/files/_common/governance/incident-report.md.template +3 -3
- package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
- package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
- package/sdlc/files/_common/governance/risk-register.md.template +1 -1
- package/sdlc/files/_common/governance/ropa.md.template +2 -2
- package/sdlc/files/_common/joining-an-existing-project.md +4 -4
- package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
- package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
- package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +302 -64
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -82
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
- package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
- package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
- package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
- package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
- package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
- package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
- package/sdlc/files/ci/ci.yml.template +82 -28
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +204 -20
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
- package/sdlc/files/ci/python/ci.yml.template +60 -12
- package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +44 -2
- package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
- package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
# close-out-release.sh — Reconcile the local compliance tree after a release
|
|
3
3
|
# is marked `released` on the DevAudit portal.
|
|
4
4
|
#
|
|
5
|
-
# For
|
|
6
|
-
# the release PR + records the sign-off), flips the matching
|
|
7
|
-
# row -> RELEASED, and moves the ticket from
|
|
8
|
-
# compliance/approved-releases/.
|
|
9
|
-
#
|
|
5
|
+
# For the governing requirement it: flips the release ticket Status -> RELEASED
|
|
6
|
+
# (backlinks the release PR + records the sign-off), flips the matching
|
|
7
|
+
# compliance/RTM.md row -> RELEASED, and moves the ticket from
|
|
8
|
+
# compliance/pending-releases/ to compliance/approved-releases/.
|
|
9
|
+
#
|
|
10
|
+
# If the release carries a bundle manifest, explicit predecessor members are
|
|
11
|
+
# reconciled too: their release tickets move to compliance/superseded-releases/,
|
|
12
|
+
# ticket status flips to SUPERSEDED, the successor + reason are recorded, and
|
|
13
|
+
# tracked REQ rows in compliance/RTM.md flip to SUPERSEDED. This keeps absorbed
|
|
14
|
+
# predecessor releases from looking abandoned after the successor approval
|
|
15
|
+
# envelope closes.
|
|
16
|
+
#
|
|
17
|
+
# The script stages the changes but does NOT commit — the caller (the close-out
|
|
18
|
+
# workflow, or a human) commits/opens the PR.
|
|
10
19
|
#
|
|
11
20
|
# Usage:
|
|
12
21
|
# ./scripts/close-out-release.sh <REQ-ID> [--release-pr <url-or-number>]
|
|
@@ -42,10 +51,147 @@ fi
|
|
|
42
51
|
|
|
43
52
|
PENDING="compliance/pending-releases/RELEASE-TICKET-${REQ_ID}.md"
|
|
44
53
|
APPROVED_DIR="compliance/approved-releases"
|
|
54
|
+
SUPERSEDED_DIR="compliance/superseded-releases"
|
|
45
55
|
APPROVED="${APPROVED_DIR}/RELEASE-TICKET-${REQ_ID}.md"
|
|
46
56
|
RTM="compliance/RTM.md"
|
|
47
57
|
TODAY="$(date +%Y-%m-%d)"
|
|
48
58
|
|
|
59
|
+
find_bundle_manifest_file() {
|
|
60
|
+
local req_id="$1"
|
|
61
|
+
local candidate=""
|
|
62
|
+
for candidate in \
|
|
63
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${req_id}.json" \
|
|
64
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${req_id}.json" \
|
|
65
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.json"; do
|
|
66
|
+
if [ -f "$candidate" ]; then
|
|
67
|
+
printf '%s' "$candidate"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
find_release_ticket_file() {
|
|
75
|
+
local version="$1"
|
|
76
|
+
local candidate=""
|
|
77
|
+
for candidate in \
|
|
78
|
+
"compliance/pending-releases/RELEASE-TICKET-${version}.md" \
|
|
79
|
+
"compliance/approved-releases/RELEASE-TICKET-${version}.md" \
|
|
80
|
+
"compliance/superseded-releases/RELEASE-TICKET-${version}.md"; do
|
|
81
|
+
if [ -f "$candidate" ]; then
|
|
82
|
+
printf '%s' "$candidate"
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
return 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
update_rtm_status() {
|
|
90
|
+
local req_id="$1"
|
|
91
|
+
local target_status="$2"
|
|
92
|
+
local tmp_file
|
|
93
|
+
tmp_file="$(mktemp)"
|
|
94
|
+
if [ -f "$RTM" ] && grep -qE "^\| ${req_id} " "$RTM"; then
|
|
95
|
+
awk -v req="$req_id" -v target_status="$target_status" '
|
|
96
|
+
BEGIN { FS="|"; OFS="|"; statuscol=0 }
|
|
97
|
+
/\\\|/ { gsub(/\\\|/, "\001", $0) }
|
|
98
|
+
{
|
|
99
|
+
cand=0; idseen=0
|
|
100
|
+
for (i=1; i<=NF; i++) {
|
|
101
|
+
c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
|
|
102
|
+
if (c=="Status") cand=i
|
|
103
|
+
if (c=="ID" || c=="REQ-ID" || c=="REQ ID" || c ~ /^Requirement/) idseen=1
|
|
104
|
+
}
|
|
105
|
+
if (cand>0 && idseen) statuscol=cand
|
|
106
|
+
}
|
|
107
|
+
$0 ~ ("^\\| " req " ") && statuscol>0 {
|
|
108
|
+
cell=$statuscol
|
|
109
|
+
note=""
|
|
110
|
+
if (match(cell, /\(/)) note=substr(cell, RSTART)
|
|
111
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", note)
|
|
112
|
+
$statuscol = (note != "" ? " " target_status " " note " " : " " target_status " ")
|
|
113
|
+
gsub(/\001/, "\\|", $0)
|
|
114
|
+
print; next
|
|
115
|
+
}
|
|
116
|
+
{ gsub(/\001/, "\\|", $0); print }
|
|
117
|
+
' "$RTM" > "$tmp_file" && mv "$tmp_file" "$RTM"
|
|
118
|
+
git add "$RTM" 2>/dev/null || true
|
|
119
|
+
echo "RTM row ${req_id} -> ${target_status}."
|
|
120
|
+
else
|
|
121
|
+
rm -f "$tmp_file"
|
|
122
|
+
fi
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
mark_ticket_superseded() {
|
|
126
|
+
local version="$1"
|
|
127
|
+
local reason="$2"
|
|
128
|
+
local relationship="$3"
|
|
129
|
+
local source_path target_path tmp_file
|
|
130
|
+
source_path="$(find_release_ticket_file "$version" 2>/dev/null || true)"
|
|
131
|
+
[ -n "$source_path" ] || return 0
|
|
132
|
+
|
|
133
|
+
mkdir -p "$SUPERSEDED_DIR"
|
|
134
|
+
target_path="${SUPERSEDED_DIR}/RELEASE-TICKET-${version}.md"
|
|
135
|
+
if [ "$source_path" != "$target_path" ]; then
|
|
136
|
+
git mv "$source_path" "$target_path" 2>/dev/null || mv "$source_path" "$target_path"
|
|
137
|
+
echo "Moved predecessor ticket -> ${target_path}"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
tmp_file="$(mktemp)"
|
|
141
|
+
awk \
|
|
142
|
+
-v successor="$REQ_ID" \
|
|
143
|
+
-v superseded_on="$TODAY" \
|
|
144
|
+
-v superseded_reason="$reason" \
|
|
145
|
+
-v superseded_relationship="$relationship" '
|
|
146
|
+
BEGIN {
|
|
147
|
+
status_done=0
|
|
148
|
+
successor_seen=0
|
|
149
|
+
reason_seen=0
|
|
150
|
+
relationship_seen=0
|
|
151
|
+
date_seen=0
|
|
152
|
+
}
|
|
153
|
+
/^\*\*Status:\*\*/ && status_done==0 {
|
|
154
|
+
print "**Status:** SUPERSEDED"
|
|
155
|
+
status_done=1
|
|
156
|
+
next
|
|
157
|
+
}
|
|
158
|
+
/^\*\*Superseded by:\*\*/ {
|
|
159
|
+
print "**Superseded by:** " successor
|
|
160
|
+
successor_seen=1
|
|
161
|
+
next
|
|
162
|
+
}
|
|
163
|
+
/^\*\*Supersession reason:\*\*/ {
|
|
164
|
+
print "**Supersession reason:** " superseded_reason
|
|
165
|
+
reason_seen=1
|
|
166
|
+
next
|
|
167
|
+
}
|
|
168
|
+
/^\*\*Supersession relationship:\*\*/ {
|
|
169
|
+
print "**Supersession relationship:** " superseded_relationship
|
|
170
|
+
relationship_seen=1
|
|
171
|
+
next
|
|
172
|
+
}
|
|
173
|
+
/^\*\*Superseded on:\*\*/ {
|
|
174
|
+
print "**Superseded on:** " superseded_on
|
|
175
|
+
date_seen=1
|
|
176
|
+
next
|
|
177
|
+
}
|
|
178
|
+
{ print }
|
|
179
|
+
/^\*\*DevAudit Release:\*\*/ {
|
|
180
|
+
if (!successor_seen) print "**Superseded by:** " successor
|
|
181
|
+
if (!reason_seen) print "**Supersession reason:** " superseded_reason
|
|
182
|
+
if (!relationship_seen) print "**Supersession relationship:** " superseded_relationship
|
|
183
|
+
if (!date_seen) print "**Superseded on:** " superseded_on
|
|
184
|
+
}
|
|
185
|
+
' "$target_path" > "$tmp_file"
|
|
186
|
+
mv "$tmp_file" "$target_path"
|
|
187
|
+
git add "$target_path" 2>/dev/null || true
|
|
188
|
+
echo "Ticket ${version} -> SUPERSEDED."
|
|
189
|
+
|
|
190
|
+
if printf '%s' "$version" | grep -qE '^REQ-[0-9]{3,}$'; then
|
|
191
|
+
update_rtm_status "$version" "SUPERSEDED"
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
49
195
|
# ── Optional portal safety check ─────────────────────────────────────────────
|
|
50
196
|
if [ -n "${DEVAUDIT_API_KEY:-}" ] && [ -n "${DEVAUDIT_BASE_URL:-}" ]; then
|
|
51
197
|
BASE="${DEVAUDIT_BASE_URL%/}"
|
|
@@ -76,6 +222,7 @@ fi
|
|
|
76
222
|
|
|
77
223
|
# ── Move ticket pending -> approved (if still pending) ───────────────────────
|
|
78
224
|
mkdir -p "$APPROVED_DIR"
|
|
225
|
+
mkdir -p "$SUPERSEDED_DIR"
|
|
79
226
|
if [ -f "$PENDING" ]; then
|
|
80
227
|
git mv "$PENDING" "$APPROVED" 2>/dev/null || mv "$PENDING" "$APPROVED"
|
|
81
228
|
echo "Moved ticket -> ${APPROVED}"
|
|
@@ -115,56 +262,30 @@ mv "$TMP" "$APPROVED"
|
|
|
115
262
|
git add "$APPROVED" 2>/dev/null || true
|
|
116
263
|
echo "Ticket Status -> RELEASED."
|
|
117
264
|
|
|
118
|
-
|
|
119
|
-
if
|
|
120
|
-
# Table-aware Status column resolution (#72): the previous version locked
|
|
121
|
-
# `statuscol` on the FIRST header that contained "Status", which mangled the
|
|
122
|
-
# wrong column when the RTM has a small legend table above the main matrix
|
|
123
|
-
# (e.g. a 2-column legend with `Status | Description` columns → statuscol=2,
|
|
124
|
-
# then the awk overwrote col-1 REQ-ID for every row).
|
|
125
|
-
#
|
|
126
|
-
# Fix: re-evaluate `statuscol` on EVERY header-shaped row (a row whose cells
|
|
127
|
-
# carry the literal header text "Status" + an ID-like column header). The
|
|
128
|
-
# legend has "Status" but no ID-like column → not locked; the main RTM has
|
|
129
|
-
# both → locks correctly. Data rows don't carry the literal "Status" header
|
|
130
|
-
# text in any cell, so they don't re-trigger the lock. Separator rows
|
|
131
|
-
# (`|---|---|…`) are left intact and don't affect `statuscol`.
|
|
132
|
-
awk -v req="$REQ_ID" '
|
|
133
|
-
BEGIN { FS="|"; OFS="|"; statuscol=0 }
|
|
134
|
-
# Protect escaped pipes (\|) from being treated as column delimiters.
|
|
135
|
-
# Replacing \| with \001 in $0 forces awk to re-split on only unescaped
|
|
136
|
-
# pipes, so literal pipes inside cell content (e.g. regex patterns like
|
|
137
|
-
# stop\|unsubscribe, enum values open\|in_progress\|closed) do not create
|
|
138
|
-
# phantom extra columns. Placeholders are restored to \| before output.
|
|
139
|
-
/\\\|/ { gsub(/\\\|/, "\001", $0) }
|
|
140
|
-
# Header detection: scan every row; require both a "Status" header cell
|
|
141
|
-
# AND an ID-like header cell in the same row before locking statuscol to
|
|
142
|
-
# this row''s column index.
|
|
143
|
-
{
|
|
144
|
-
cand=0; idseen=0
|
|
145
|
-
for (i=1; i<=NF; i++) {
|
|
146
|
-
c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
|
|
147
|
-
if (c=="Status") cand=i
|
|
148
|
-
if (c=="ID" || c=="REQ-ID" || c=="REQ ID" || c ~ /^Requirement/) idseen=1
|
|
149
|
-
}
|
|
150
|
-
if (cand>0 && idseen) statuscol=cand
|
|
151
|
-
}
|
|
152
|
-
$0 ~ ("^\\| " req " ") && statuscol>0 {
|
|
153
|
-
cell=$statuscol
|
|
154
|
-
note=""
|
|
155
|
-
if (match(cell, /\(/)) note=substr(cell, RSTART) # preserve any " (requirement note)"
|
|
156
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", note)
|
|
157
|
-
$statuscol = (note != "" ? " RELEASED " note " " : " RELEASED ")
|
|
158
|
-
gsub(/\001/, "\\|", $0)
|
|
159
|
-
print; next
|
|
160
|
-
}
|
|
161
|
-
{ gsub(/\001/, "\\|", $0); print }
|
|
162
|
-
' "$RTM" > "$TMP" && mv "$TMP" "$RTM"
|
|
163
|
-
git add "$RTM" 2>/dev/null || true
|
|
164
|
-
echo "RTM row ${REQ_ID} -> RELEASED."
|
|
165
|
-
else
|
|
265
|
+
update_rtm_status "$REQ_ID" "RELEASED" || true
|
|
266
|
+
if ! grep -qE "^\| ${REQ_ID} " "$RTM" 2>/dev/null; then
|
|
166
267
|
echo "::warning::No RTM row for ${REQ_ID} in ${RTM} — skipped RTM flip."
|
|
167
|
-
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# ── Reconcile explicit predecessors from the bundle manifest ──────────────────
|
|
271
|
+
BUNDLE_MANIFEST="$(find_bundle_manifest_file "$REQ_ID" 2>/dev/null || true)"
|
|
272
|
+
if [ -n "$BUNDLE_MANIFEST" ] && command -v jq >/dev/null 2>&1; then
|
|
273
|
+
while IFS=$'\t' read -r member_version member_reason member_relationship; do
|
|
274
|
+
[ -n "$member_version" ] || continue
|
|
275
|
+
mark_ticket_superseded "$member_version" "$member_reason" "$member_relationship"
|
|
276
|
+
done < <(
|
|
277
|
+
jq -r --arg successor "$REQ_ID" '
|
|
278
|
+
(.members // [])
|
|
279
|
+
| map(select((.relationship // "") == "superseded" or (.relationship // "") == "absorbed"))
|
|
280
|
+
| .[]
|
|
281
|
+
| [
|
|
282
|
+
(.version // ""),
|
|
283
|
+
(.reason // ("Absorbed into successor release " + $successor + ".")),
|
|
284
|
+
(.relationship // "superseded")
|
|
285
|
+
]
|
|
286
|
+
| @tsv
|
|
287
|
+
' "$BUNDLE_MANIFEST" 2>/dev/null || true
|
|
288
|
+
)
|
|
168
289
|
fi
|
|
169
290
|
|
|
170
291
|
echo "Close-out staged for ${REQ_ID}. Commit + open a PR to develop to land it."
|
|
@@ -195,6 +195,96 @@ EOF
|
|
|
195
195
|
rm -rf "$(dirname "$dir")"
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
# ── Case 4: bundled close-out supersedes predecessor tickets ──────────────────
|
|
199
|
+
{
|
|
200
|
+
dir="$(mktemp -d)/cli-close-out-fixture-4"
|
|
201
|
+
mkdir -p "$dir/compliance/pending-releases" \
|
|
202
|
+
"$dir/compliance/approved-releases" \
|
|
203
|
+
"$dir/compliance/superseded-releases"
|
|
204
|
+
cd "$dir"
|
|
205
|
+
git init -q --initial-branch=main >/dev/null
|
|
206
|
+
git config user.email "test@example.com"
|
|
207
|
+
git config user.name "test"
|
|
208
|
+
cat > compliance/RTM.md <<'EOF'
|
|
209
|
+
# Requirements Traceability Matrix
|
|
210
|
+
|
|
211
|
+
| REQ-ID | Source | Risk | Evidence | Status | Owner | Date |
|
|
212
|
+
| ------- | ------ | ---- | -------- | ------ | ----- | ---- |
|
|
213
|
+
| REQ-090 | #500 | HIGH | compliance/evidence/REQ-090/ | TESTED - PENDING SIGN-OFF | test | 2026-07-16 |
|
|
214
|
+
| REQ-089 | #499 | LOW | compliance/evidence/REQ-089/ | TESTED - PENDING SIGN-OFF | test | 2026-07-15 |
|
|
215
|
+
EOF
|
|
216
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-090.md <<'EOF'
|
|
217
|
+
# Release Ticket: REQ-090
|
|
218
|
+
|
|
219
|
+
**Status:** TESTED - PENDING SIGN-OFF
|
|
220
|
+
**DevAudit Release:** REQ-090
|
|
221
|
+
EOF
|
|
222
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-089.md <<'EOF'
|
|
223
|
+
# Release Ticket: REQ-089
|
|
224
|
+
|
|
225
|
+
**Status:** TESTED - PENDING SIGN-OFF
|
|
226
|
+
**DevAudit Release:** REQ-089
|
|
227
|
+
EOF
|
|
228
|
+
cat > compliance/pending-releases/RELEASE-TICKET-v2026.07.10.md <<'EOF'
|
|
229
|
+
# Release Ticket: v2026.07.10
|
|
230
|
+
|
|
231
|
+
**Status:** TESTED - PENDING SIGN-OFF
|
|
232
|
+
**DevAudit Release:** v2026.07.10
|
|
233
|
+
EOF
|
|
234
|
+
cat > compliance/pending-releases/BUNDLED-CHANGES-REQ-090.json <<'EOF'
|
|
235
|
+
{
|
|
236
|
+
"schemaVersion": 1,
|
|
237
|
+
"approvalRelease": { "version": "REQ-090" },
|
|
238
|
+
"coreRelease": { "version": "REQ-090" },
|
|
239
|
+
"members": [
|
|
240
|
+
{
|
|
241
|
+
"version": "REQ-089",
|
|
242
|
+
"role": "predecessor",
|
|
243
|
+
"relationship": "superseded",
|
|
244
|
+
"reason": "Explicit predecessor release absorbed into approval envelope REQ-090."
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"version": "v2026.07.10",
|
|
248
|
+
"role": "housekeeping",
|
|
249
|
+
"relationship": "absorbed",
|
|
250
|
+
"reason": "Explicit housekeeping release ticket carried forward into approval envelope REQ-090."
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
"nonReleaseWorkItems": [],
|
|
254
|
+
"manifestHash": "sha256:test"
|
|
255
|
+
}
|
|
256
|
+
EOF
|
|
257
|
+
git add -A
|
|
258
|
+
git commit -q -m "fixture: bundled close-out"
|
|
259
|
+
unset DEVAUDIT_API_KEY DEVAUDIT_BASE_URL || true
|
|
260
|
+
bash "$HELPER" REQ-090 >/dev/null 2>&1 || true
|
|
261
|
+
|
|
262
|
+
[ -f compliance/approved-releases/RELEASE-TICKET-REQ-090.md ] \
|
|
263
|
+
&& assert_eq "current release moved to approved-releases" "yes" "yes" \
|
|
264
|
+
|| assert_eq "current release moved to approved-releases" "yes" "no"
|
|
265
|
+
[ -f compliance/superseded-releases/RELEASE-TICKET-REQ-089.md ] \
|
|
266
|
+
&& assert_eq "predecessor moved to superseded-releases" "yes" "yes" \
|
|
267
|
+
|| assert_eq "predecessor moved to superseded-releases" "yes" "no"
|
|
268
|
+
[ -f compliance/superseded-releases/RELEASE-TICKET-v2026.07.10.md ] \
|
|
269
|
+
&& assert_eq "housekeeping predecessor moved to superseded-releases" "yes" "yes" \
|
|
270
|
+
|| assert_eq "housekeeping predecessor moved to superseded-releases" "yes" "no"
|
|
271
|
+
|
|
272
|
+
grep -qF '**Status:** SUPERSEDED' compliance/superseded-releases/RELEASE-TICKET-REQ-089.md \
|
|
273
|
+
&& assert_eq "predecessor status flipped" "yes" "yes" \
|
|
274
|
+
|| assert_eq "predecessor status flipped" "yes" "no"
|
|
275
|
+
grep -qF '**Superseded by:** REQ-090' compliance/superseded-releases/RELEASE-TICKET-REQ-089.md \
|
|
276
|
+
&& assert_eq "predecessor successor recorded" "yes" "yes" \
|
|
277
|
+
|| assert_eq "predecessor successor recorded" "yes" "no"
|
|
278
|
+
grep -qF 'Explicit predecessor release absorbed into approval envelope REQ-090.' compliance/superseded-releases/RELEASE-TICKET-REQ-089.md \
|
|
279
|
+
&& assert_eq "predecessor reason recorded" "yes" "yes" \
|
|
280
|
+
|| assert_eq "predecessor reason recorded" "yes" "no"
|
|
281
|
+
|
|
282
|
+
row=$(grep -m1 -E "^\| REQ-089 " compliance/RTM.md || true)
|
|
283
|
+
col5=$(echo "$row" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$6); print $6}')
|
|
284
|
+
assert_eq "predecessor RTM row flipped" "SUPERSEDED" "$col5"
|
|
285
|
+
rm -rf "$(dirname "$dir")"
|
|
286
|
+
}
|
|
287
|
+
|
|
198
288
|
echo
|
|
199
289
|
echo "Result: $PASS passed, $FAIL failed"
|
|
200
290
|
[ "$FAIL" = "0" ]
|
|
@@ -41,6 +41,21 @@ find_bundled_changes_file() {
|
|
|
41
41
|
return 1
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
find_bundled_manifest_file() {
|
|
45
|
+
local req_id="$1"
|
|
46
|
+
local candidate=""
|
|
47
|
+
for candidate in \
|
|
48
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${req_id}.json" \
|
|
49
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${req_id}.json" \
|
|
50
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.json"; do
|
|
51
|
+
if [ -f "$candidate" ]; then
|
|
52
|
+
printf '%s' "$candidate"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
extract_release_metadata() {
|
|
45
60
|
local req_id="$1"
|
|
46
61
|
RELEASE_TITLE=""
|
|
@@ -140,7 +155,19 @@ extract_release_metadata() {
|
|
|
140
155
|
bundled_file=$(find_bundled_changes_file "$req_id" 2>/dev/null || true)
|
|
141
156
|
if [ -n "$bundled_file" ]; then
|
|
142
157
|
local bundled_note
|
|
158
|
+
local bundled_manifest=""
|
|
159
|
+
local manifest_hash=""
|
|
160
|
+
bundled_manifest=$(find_bundled_manifest_file "$req_id" 2>/dev/null || true)
|
|
161
|
+
if [ -n "$bundled_manifest" ] && command -v jq >/dev/null 2>&1; then
|
|
162
|
+
manifest_hash=$(jq -r '.manifestHash // empty' "$bundled_manifest" 2>/dev/null || true)
|
|
163
|
+
fi
|
|
143
164
|
bundled_note="Bundled release context: see \`${bundled_file}\`."
|
|
165
|
+
if [ -n "$bundled_manifest" ]; then
|
|
166
|
+
bundled_note="${bundled_note} Manifest: \`${bundled_manifest}\`."
|
|
167
|
+
fi
|
|
168
|
+
if [ -n "$manifest_hash" ]; then
|
|
169
|
+
bundled_note="${bundled_note} Hash: \`${manifest_hash}\`."
|
|
170
|
+
fi
|
|
144
171
|
if [ -n "$RELEASE_SUMMARY" ]; then
|
|
145
172
|
case "$RELEASE_SUMMARY" in
|
|
146
173
|
*"Bundled release context:"*) ;;
|
|
@@ -374,13 +374,25 @@ cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md" <<'BUNDLE'
|
|
|
374
374
|
|
|
375
375
|
- `abc1234` chore: sync templates
|
|
376
376
|
BUNDLE
|
|
377
|
+
cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.json" <<'BUNDLEJSON'
|
|
378
|
+
{
|
|
379
|
+
"schemaVersion": 1,
|
|
380
|
+
"approvalRelease": { "version": "REQ-017" },
|
|
381
|
+
"coreRelease": { "version": "REQ-017" },
|
|
382
|
+
"members": [],
|
|
383
|
+
"nonReleaseWorkItems": [
|
|
384
|
+
{ "kind": "housekeeping_commit", "title": "chore: sync templates", "reference": "abc1234" }
|
|
385
|
+
],
|
|
386
|
+
"manifestHash": "sha256:test-bundle-hash"
|
|
387
|
+
}
|
|
388
|
+
BUNDLEJSON
|
|
377
389
|
|
|
378
390
|
source "$HELPER"
|
|
379
391
|
extract_release_metadata "REQ-017"
|
|
380
392
|
assert_eq "Bundled note appended to summary" \
|
|
381
393
|
"Core tracked change summary.
|
|
382
394
|
|
|
383
|
-
Bundled release context: see \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md\`." \
|
|
395
|
+
Bundled release context: see \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md\`. Manifest: \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.json\`. Hash: \`sha256:test-bundle-hash\`." \
|
|
384
396
|
"$RELEASE_SUMMARY"
|
|
385
397
|
echo ""
|
|
386
398
|
|