@metasession.co/devaudit-cli 0.3.11 → 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 +94 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/ai-rules/SDLC_RULES.md +27 -7
- package/sdlc/files/_common/0-project-setup.md +13 -7
- 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 +65 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +38 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +307 -58
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -78
- 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 +55 -1
- package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +169 -2
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +169 -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 +57 -3
- 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/check-release-approval.yml.template +18 -28
- package/sdlc/files/ci/ci.yml.template +95 -35
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +234 -49
- 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 +102 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +3 -3
- package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-release-pr-scope.test.sh — Tests for release PR metadata drift checks.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
HELPER="$SCRIPT_DIR/check-release-pr-scope.sh"
|
|
8
|
+
|
|
9
|
+
PASS=0
|
|
10
|
+
FAIL=0
|
|
11
|
+
WORK=$(mktemp -d)
|
|
12
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
13
|
+
|
|
14
|
+
assert_exit() {
|
|
15
|
+
local name="$1" expected="$2" actual="$3"
|
|
16
|
+
if [ "$expected" = "$actual" ]; then
|
|
17
|
+
echo " ✓ $name"
|
|
18
|
+
PASS=$((PASS + 1))
|
|
19
|
+
else
|
|
20
|
+
echo " ✗ $name"
|
|
21
|
+
echo " Expected exit: $expected"
|
|
22
|
+
echo " Actual exit: $actual"
|
|
23
|
+
FAIL=$((FAIL + 1))
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
assert_grep() {
|
|
28
|
+
local name="$1" pattern="$2" file="$3"
|
|
29
|
+
if grep -Eq "$pattern" "$file"; then
|
|
30
|
+
echo " ✓ $name"
|
|
31
|
+
PASS=$((PASS + 1))
|
|
32
|
+
else
|
|
33
|
+
echo " ✗ $name"
|
|
34
|
+
echo " Missing pattern: $pattern"
|
|
35
|
+
FAIL=$((FAIL + 1))
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
make_fixture() {
|
|
40
|
+
local dir="$1" subject="$2"
|
|
41
|
+
rm -rf "$dir"
|
|
42
|
+
mkdir -p "$dir/scripts" "$dir/compliance/pending-releases"
|
|
43
|
+
cp "$SCRIPT_DIR/derive-release-version.sh" "$dir/scripts/derive-release-version.sh"
|
|
44
|
+
cp "$SCRIPT_DIR/extract-release-metadata.sh" "$dir/scripts/extract-release-metadata.sh"
|
|
45
|
+
cp "$HELPER" "$dir/scripts/check-release-pr-scope.sh"
|
|
46
|
+
chmod +x "$dir/scripts/"*.sh
|
|
47
|
+
(
|
|
48
|
+
cd "$dir"
|
|
49
|
+
git init -q
|
|
50
|
+
git config user.email test@example.com
|
|
51
|
+
git config user.name tester
|
|
52
|
+
git commit --allow-empty -q -m "$subject"
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
run_check() {
|
|
57
|
+
local dir="$1" out="$2"
|
|
58
|
+
shift 2
|
|
59
|
+
(
|
|
60
|
+
cd "$dir"
|
|
61
|
+
"$@" bash scripts/check-release-pr-scope.sh
|
|
62
|
+
) >"$out" 2>&1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
echo "=== check-release-pr-scope.sh tests ==="
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
68
|
+
echo "--- Test 1: matching REQ scope passes ---"
|
|
69
|
+
make_fixture "$WORK/test1" "[REQ-093] feat: ship release"
|
|
70
|
+
cat > "$WORK/test1/compliance/pending-releases/RELEASE-TICKET-REQ-093.md" <<'EOF'
|
|
71
|
+
# Release Ticket — REQ-093
|
|
72
|
+
|
|
73
|
+
**Requirement:** REQ-093 — Example release
|
|
74
|
+
|
|
75
|
+
## Summary
|
|
76
|
+
Normal tracked release.
|
|
77
|
+
EOF
|
|
78
|
+
OUT="$WORK/test1.out"
|
|
79
|
+
if run_check "$WORK/test1" "$OUT" env PR_TITLE="Release: REQ-093" PR_BODY=$'## Release\n- Release: REQ-093\n'; then
|
|
80
|
+
CODE=0
|
|
81
|
+
else
|
|
82
|
+
CODE=$?
|
|
83
|
+
fi
|
|
84
|
+
assert_exit "matching REQ scope passes" 0 "$CODE"
|
|
85
|
+
assert_grep "success message emitted" 'Release Scope Integrity verified for REQ-093' "$OUT"
|
|
86
|
+
echo ""
|
|
87
|
+
|
|
88
|
+
echo "--- Test 2: stale REQ scope fails ---"
|
|
89
|
+
make_fixture "$WORK/test2" "[REQ-093] feat: ship release"
|
|
90
|
+
OUT="$WORK/test2.out"
|
|
91
|
+
if run_check "$WORK/test2" "$OUT" env PR_TITLE="Release: REQ-092" PR_BODY=$'## Release\n- Release: REQ-092\n'; then
|
|
92
|
+
CODE=0
|
|
93
|
+
else
|
|
94
|
+
CODE=$?
|
|
95
|
+
fi
|
|
96
|
+
assert_exit "stale REQ scope fails" 1 "$CODE"
|
|
97
|
+
assert_grep "declared scope printed" 'Declared PR scope: REQ-092' "$OUT"
|
|
98
|
+
assert_grep "derived scope printed" 'Derived effective scope: REQ-093' "$OUT"
|
|
99
|
+
echo ""
|
|
100
|
+
|
|
101
|
+
echo "--- Test 3: bundled release must mention bundled marker ---"
|
|
102
|
+
make_fixture "$WORK/test3" "[REQ-093] feat: bundled release"
|
|
103
|
+
cat > "$WORK/test3/compliance/pending-releases/RELEASE-TICKET-REQ-093.md" <<'EOF'
|
|
104
|
+
# Release Ticket — REQ-093
|
|
105
|
+
|
|
106
|
+
**Requirement:** REQ-093 — Example release
|
|
107
|
+
|
|
108
|
+
## Summary
|
|
109
|
+
Tracked release summary.
|
|
110
|
+
EOF
|
|
111
|
+
cat > "$WORK/test3/compliance/pending-releases/BUNDLED-CHANGES-REQ-093.md" <<'EOF'
|
|
112
|
+
# Bundled Changes — REQ-093
|
|
113
|
+
EOF
|
|
114
|
+
OUT="$WORK/test3.out"
|
|
115
|
+
if run_check "$WORK/test3" "$OUT" env PR_TITLE="Release: REQ-093" PR_BODY=$'## Release\n- Release: REQ-093\n'; then
|
|
116
|
+
CODE=0
|
|
117
|
+
else
|
|
118
|
+
CODE=$?
|
|
119
|
+
fi
|
|
120
|
+
assert_exit "missing bundled marker fails" 1 "$CODE"
|
|
121
|
+
assert_grep "bundled mismatch printed" 'bundled release context marker' "$OUT"
|
|
122
|
+
echo ""
|
|
123
|
+
|
|
124
|
+
echo "--- Test 4: hotfix branches skip scope integrity ---"
|
|
125
|
+
make_fixture "$WORK/test4" "[REQ-093] feat: ship release"
|
|
126
|
+
OUT="$WORK/test4.out"
|
|
127
|
+
if run_check "$WORK/test4" "$OUT" env HEAD_REF="hotfix/urgent-fix" PR_TITLE="fix: urgent prod bug" PR_BODY="HOTFIX"; then
|
|
128
|
+
CODE=0
|
|
129
|
+
else
|
|
130
|
+
CODE=$?
|
|
131
|
+
fi
|
|
132
|
+
assert_exit "hotfix branch skips" 0 "$CODE"
|
|
133
|
+
assert_grep "hotfix skip message" 'Hotfix PR detected' "$OUT"
|
|
134
|
+
echo ""
|
|
135
|
+
|
|
136
|
+
echo "=== check-release-pr-scope.test.sh: $PASS passed, $FAIL failed ==="
|
|
137
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -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" ]
|
|
@@ -26,6 +26,36 @@
|
|
|
26
26
|
|
|
27
27
|
set -euo pipefail
|
|
28
28
|
|
|
29
|
+
find_bundled_changes_file() {
|
|
30
|
+
local req_id="$1"
|
|
31
|
+
local candidate=""
|
|
32
|
+
for candidate in \
|
|
33
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${req_id}.md" \
|
|
34
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${req_id}.md" \
|
|
35
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.md"; do
|
|
36
|
+
if [ -f "$candidate" ]; then
|
|
37
|
+
printf '%s' "$candidate"
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
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
|
+
|
|
29
59
|
extract_release_metadata() {
|
|
30
60
|
local req_id="$1"
|
|
31
61
|
RELEASE_TITLE=""
|
|
@@ -116,4 +146,39 @@ extract_release_metadata() {
|
|
|
116
146
|
;;
|
|
117
147
|
esac
|
|
118
148
|
fi
|
|
149
|
+
|
|
150
|
+
# If the tracked release has a generated bundled-changes artefact,
|
|
151
|
+
# surface that fact in the release summary so the portal release row and
|
|
152
|
+
# workflow metadata don't rely on the GitHub PR body as the only bundle
|
|
153
|
+
# narrative. DevAudit-Installer#344.
|
|
154
|
+
local bundled_file=""
|
|
155
|
+
bundled_file=$(find_bundled_changes_file "$req_id" 2>/dev/null || true)
|
|
156
|
+
if [ -n "$bundled_file" ]; then
|
|
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
|
|
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
|
|
171
|
+
if [ -n "$RELEASE_SUMMARY" ]; then
|
|
172
|
+
case "$RELEASE_SUMMARY" in
|
|
173
|
+
*"Bundled release context:"*) ;;
|
|
174
|
+
*)
|
|
175
|
+
RELEASE_SUMMARY="${RELEASE_SUMMARY}
|
|
176
|
+
|
|
177
|
+
${bundled_note}"
|
|
178
|
+
;;
|
|
179
|
+
esac
|
|
180
|
+
else
|
|
181
|
+
RELEASE_SUMMARY="$bundled_note"
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
119
184
|
}
|
|
@@ -358,6 +358,44 @@ assert_eq "Title extracted" "Feature fifteen" "$RELEASE_TITLE"
|
|
|
358
358
|
assert_empty "Whitespace-only summary cleared" "$RELEASE_SUMMARY"
|
|
359
359
|
echo ""
|
|
360
360
|
|
|
361
|
+
# --- Test 17: Bundled changes file appends bundle note to summary ---
|
|
362
|
+
echo "--- Test 17: Bundled changes note appended ---"
|
|
363
|
+
make_fixture "$WORK/test17"
|
|
364
|
+
cat > "compliance/pending-releases/RELEASE-TICKET-REQ-017.md" <<'TICKET'
|
|
365
|
+
# Release Ticket — REQ-017
|
|
366
|
+
|
|
367
|
+
**Requirement:** REQ-017 — Feature seventeen
|
|
368
|
+
|
|
369
|
+
## Summary
|
|
370
|
+
Core tracked change summary.
|
|
371
|
+
TICKET
|
|
372
|
+
cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md" <<'BUNDLE'
|
|
373
|
+
## Bundled Changes
|
|
374
|
+
|
|
375
|
+
- `abc1234` chore: sync templates
|
|
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
|
|
389
|
+
|
|
390
|
+
source "$HELPER"
|
|
391
|
+
extract_release_metadata "REQ-017"
|
|
392
|
+
assert_eq "Bundled note appended to summary" \
|
|
393
|
+
"Core tracked change summary.
|
|
394
|
+
|
|
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\`." \
|
|
396
|
+
"$RELEASE_SUMMARY"
|
|
397
|
+
echo ""
|
|
398
|
+
|
|
361
399
|
echo ""
|
|
362
400
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
363
401
|
exit $FAIL
|