@metasession.co/devaudit-cli 0.3.25 → 0.3.27
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 +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +14 -6
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +44 -0
- package/sdlc/files/_common/scripts/upload-compliance-documents.sh +484 -0
- package/sdlc/files/ci/compliance-evidence.yml.template +8 -469
- package/sdlc/package.json +1 -1
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.27",
|
|
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.27",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"consola": "^3.2.3",
|
|
@@ -27,15 +27,23 @@ import re
|
|
|
27
27
|
title = os.environ.get("PR_TITLE", "")
|
|
28
28
|
body = os.environ.get("PR_BODY", "")
|
|
29
29
|
text = f"{title}\n{body}"
|
|
30
|
+
scope = r"(?:REQ-\d+|v\d+\.\d+\.\d+|v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?)"
|
|
31
|
+
|
|
32
|
+
# An explicit release field is authoritative. Do not let a historical REQ in
|
|
33
|
+
# the surrounding prose override it, and fail closed if its value is invalid.
|
|
34
|
+
explicit_field = re.search(r"^[ \t\-*]*release:\s*(.*)$", text, re.IGNORECASE | re.MULTILINE)
|
|
35
|
+
if explicit_field:
|
|
36
|
+
value = explicit_field.group(1).strip()
|
|
37
|
+
match = re.match(rf"`?({scope})`?(?![A-Za-z0-9_.-])", value, re.IGNORECASE)
|
|
38
|
+
if match:
|
|
39
|
+
print(match.group(1))
|
|
40
|
+
raise SystemExit(0)
|
|
41
|
+
|
|
30
42
|
patterns = [
|
|
31
|
-
|
|
32
|
-
r"\b(REQ-\d+)\b",
|
|
33
|
-
r"\b(v\d+\.\d+\.\d+)\b",
|
|
34
|
-
r"\b(v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?)\b",
|
|
43
|
+
rf"\b({scope})\b",
|
|
35
44
|
]
|
|
36
|
-
flags = re.MULTILINE
|
|
37
45
|
for pattern in patterns:
|
|
38
|
-
match = re.search(pattern, text,
|
|
46
|
+
match = re.search(pattern, text, re.IGNORECASE)
|
|
39
47
|
if match:
|
|
40
48
|
print(match.group(1))
|
|
41
49
|
break
|
|
@@ -223,5 +223,49 @@ assert_exit "bare-date release without exception marker fails" 1 "$CODE"
|
|
|
223
223
|
assert_grep "exception marker failure is clear" 'explicit standalone housekeeping exception' "$OUT"
|
|
224
224
|
echo ""
|
|
225
225
|
|
|
226
|
+
echo "--- Test 10: Markdown standalone scope wins over historical REQ prose ---"
|
|
227
|
+
make_fixture "$WORK/test10" "chore: standalone housekeeping release"
|
|
228
|
+
cat > "$WORK/test10/compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-${BARE_DATE_VERSION}.json" <<EOF
|
|
229
|
+
{
|
|
230
|
+
"schemaVersion": 1,
|
|
231
|
+
"version": "${BARE_DATE_VERSION}",
|
|
232
|
+
"releaseMode": "standalone_housekeeping",
|
|
233
|
+
"reason": "Reviewed operational change cannot wait for the next tracked requirement release."
|
|
234
|
+
}
|
|
235
|
+
EOF
|
|
236
|
+
OUT="$WORK/test10.out"
|
|
237
|
+
if run_check "$WORK/test10" "$OUT" env PR_TITLE="Standalone housekeeping promotion: ${BARE_DATE_VERSION}" PR_BODY=$"## Scope\n- Release: \`${BARE_DATE_VERSION}\`\n- Historical context: REQ-094 close-out recovery.\n"; then
|
|
238
|
+
CODE=0
|
|
239
|
+
else
|
|
240
|
+
CODE=$?
|
|
241
|
+
fi
|
|
242
|
+
assert_exit "Markdown standalone scope wins over historical REQ prose" 0 "$CODE"
|
|
243
|
+
assert_grep "Markdown standalone scope is verified" "Release Scope Integrity verified for ${BARE_DATE_VERSION}" "$OUT"
|
|
244
|
+
echo ""
|
|
245
|
+
|
|
246
|
+
echo "--- Test 11: Markdown tracked scope passes ---"
|
|
247
|
+
make_fixture "$WORK/test11" "[REQ-093] feat: ship release"
|
|
248
|
+
OUT="$WORK/test11.out"
|
|
249
|
+
if run_check "$WORK/test11" "$OUT" env PR_TITLE="release: REQ-093" PR_BODY=$'## Scope\n- Release: `REQ-093`\n'; then
|
|
250
|
+
CODE=0
|
|
251
|
+
else
|
|
252
|
+
CODE=$?
|
|
253
|
+
fi
|
|
254
|
+
assert_exit "Markdown tracked scope passes" 0 "$CODE"
|
|
255
|
+
assert_grep "Markdown tracked scope is verified" 'Release Scope Integrity verified for REQ-093' "$OUT"
|
|
256
|
+
echo ""
|
|
257
|
+
|
|
258
|
+
echo "--- Test 12: malformed explicit scope fails without falling back ---"
|
|
259
|
+
make_fixture "$WORK/test12" "[REQ-093] feat: ship release"
|
|
260
|
+
OUT="$WORK/test12.out"
|
|
261
|
+
if run_check "$WORK/test12" "$OUT" env PR_TITLE="prepare release" PR_BODY=$'## Scope\n- Release: `not-a-release`\n- Historical context: REQ-093\n'; then
|
|
262
|
+
CODE=0
|
|
263
|
+
else
|
|
264
|
+
CODE=$?
|
|
265
|
+
fi
|
|
266
|
+
assert_exit "malformed explicit scope fails" 1 "$CODE"
|
|
267
|
+
assert_grep "malformed explicit scope has clear error" 'do not declare a release scope' "$OUT"
|
|
268
|
+
echo ""
|
|
269
|
+
|
|
226
270
|
echo "=== check-release-pr-scope.test.sh: $PASS passed, $FAIL failed ==="
|
|
227
271
|
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Upload project, governance, release, and requirement evidence for one
|
|
3
|
+
# Stage 3 workflow invocation. Kept outside the workflow so GitHub Actions'
|
|
4
|
+
# 21 KB expression-template limit cannot make the workflow undispatchable.
|
|
5
|
+
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
: "${DEVAUDIT_PROJECT_SLUG:?DEVAUDIT_PROJECT_SLUG is required}"
|
|
9
|
+
: "${DEVAUDIT_GIT_SHA:?DEVAUDIT_GIT_SHA is required}"
|
|
10
|
+
: "${DEVAUDIT_CI_RUN_ID:?DEVAUDIT_CI_RUN_ID is required}"
|
|
11
|
+
: "${DEVAUDIT_BRANCH:?DEVAUDIT_BRANCH is required}"
|
|
12
|
+
: "${DEVAUDIT_RELEASE_VERSION:?DEVAUDIT_RELEASE_VERSION is required}"
|
|
13
|
+
: "${DEVAUDIT_BASE_URL:?DEVAUDIT_BASE_URL is required}"
|
|
14
|
+
: "${DEVAUDIT_API_KEY:?DEVAUDIT_API_KEY is required}"
|
|
15
|
+
|
|
16
|
+
chmod +x scripts/upload-evidence.sh 2>/dev/null || true
|
|
17
|
+
# Common flags WITHOUT --release. Each upload appends its OWN
|
|
18
|
+
# --release so per-requirement artifacts land in their own release
|
|
19
|
+
# record (version = REQ-ID) instead of all collapsing into whichever
|
|
20
|
+
# single version the triggering commit derived. DevAudit #135 follow-
|
|
21
|
+
# up: this fixes release *attribution* (not just duplication) — an
|
|
22
|
+
# untagged docs commit must not sweep every in-scope REQ into a date
|
|
23
|
+
# release. Note: upload-evidence.sh keeps the LAST --release seen.
|
|
24
|
+
FLAGS="--git-sha ${DEVAUDIT_GIT_SHA} --ci-run-id ${DEVAUDIT_CI_RUN_ID} --branch ${DEVAUDIT_BRANCH}"
|
|
25
|
+
FLAGS="${FLAGS} --create-release-if-missing --environment uat --sdlc-stage 3"
|
|
26
|
+
DERIVED_RELEASE="${DEVAUDIT_RELEASE_VERSION}"
|
|
27
|
+
|
|
28
|
+
# Derive change_type for the bare-date (housekeeping) DERIVED_RELEASE:
|
|
29
|
+
# the prefix of the most recent commit's subject. No-op for tracked
|
|
30
|
+
# releases — they get per-REQ derivation in the loop below.
|
|
31
|
+
DERIVED_CT=$(git log -1 --pretty=%s 2>/dev/null \
|
|
32
|
+
| grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
|
|
33
|
+
| head -1 || true)
|
|
34
|
+
DERIVED_META=()
|
|
35
|
+
[ -n "$DERIVED_CT" ] && DERIVED_META+=(--change-type "$DERIVED_CT")
|
|
36
|
+
|
|
37
|
+
# Upload project-level planning docs with specific evidence_type
|
|
38
|
+
# so the portal's framework-coverage predicates attribute them
|
|
39
|
+
# correctly (DevAudit-Installer#205). Previously all three uploaded
|
|
40
|
+
# as compliance_document — the catch-all that satisfies no specific
|
|
41
|
+
# clause predicate.
|
|
42
|
+
#
|
|
43
|
+
# - RTM.md → rtm (ISO 29119 §3.3 traceability)
|
|
44
|
+
# - test-plan.md → test_plan (ISO 29119 §3.4 test plan)
|
|
45
|
+
# - test-cases.md → test_cases (ISO 29119 §3.4 test cases)
|
|
46
|
+
upload_project_doc() {
|
|
47
|
+
local DOC="$1" EVTYPE="$2"
|
|
48
|
+
if [ ! -f "$DOC" ]; then return 0; fi
|
|
49
|
+
echo "Uploading: $(basename "$DOC") (${EVTYPE})"
|
|
50
|
+
bash scripts/upload-evidence.sh \
|
|
51
|
+
${DEVAUDIT_PROJECT_SLUG} _compliance-docs "$EVTYPE" "$DOC" \
|
|
52
|
+
--category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
53
|
+
"${DERIVED_META[@]}" \
|
|
54
|
+
|| echo "Warning: Failed to upload $(basename "$DOC")"
|
|
55
|
+
}
|
|
56
|
+
upload_project_doc compliance/RTM.md rtm
|
|
57
|
+
upload_project_doc compliance/test-plan.md test_plan
|
|
58
|
+
upload_project_doc compliance/test-cases.md test_cases
|
|
59
|
+
|
|
60
|
+
# Project-level Test Summary Report — a hand-authored baseline
|
|
61
|
+
# describing the project's testing posture. As of v0.1.32 this is
|
|
62
|
+
# uploaded as `compliance_document` (NOT `test_report`) — the
|
|
63
|
+
# per-release Test Reports gate is now satisfied by the per-REQ
|
|
64
|
+
# `test-execution-summary.md` uploaded in the in-scope-requirements
|
|
65
|
+
# loop below, which carries fresh release-specific data. The
|
|
66
|
+
# project-level TSR continues to ship as a Documents-tab baseline
|
|
67
|
+
# but no longer poses as per-release test evidence.
|
|
68
|
+
# See DevAudit-Installer#101.
|
|
69
|
+
if [ -f "compliance/test-summary-report.md" ]; then
|
|
70
|
+
echo "Uploading: test-summary-report.md (compliance_document — baseline)"
|
|
71
|
+
bash scripts/upload-evidence.sh \
|
|
72
|
+
${DEVAUDIT_PROJECT_SLUG} _compliance-docs compliance_document compliance/test-summary-report.md \
|
|
73
|
+
--category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
74
|
+
"${DERIVED_META[@]}" \
|
|
75
|
+
|| echo "Warning: Failed to upload test-summary-report.md"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# NOTE: test-summary-report.md stays as compliance_document because
|
|
79
|
+
# it is a project-level evergreen baseline, NOT per-release test
|
|
80
|
+
# evidence. It does not satisfy any specific clause predicate —
|
|
81
|
+
# the per-REQ test-execution-summary.md (test_report) does that.
|
|
82
|
+
# This is the one intentional compliance_document use for project-
|
|
83
|
+
# level docs (DevAudit-Installer#205).
|
|
84
|
+
|
|
85
|
+
# Tier 3 per-event governance docs (devaudit#370 Phase 3a, narrowed
|
|
86
|
+
# in v0.1.39). Only periodic-review and incident-report are CI-uploaded
|
|
87
|
+
# — both are auto-generated by other workflows (periodic-review by the
|
|
88
|
+
# quarterly cron, incident-report by the incident-export workflow).
|
|
89
|
+
#
|
|
90
|
+
# Tier 1/2 governance docs (Test_Policy, AGENT, INSTRUCTIONS,
|
|
91
|
+
# Test_Strategy, Test_Architecture, Periodic_Security_Review_Schedule,
|
|
92
|
+
# ROPA, DPIA, AI Disclosure) are operator-uploaded via the portal
|
|
93
|
+
# Upload Evidence form. CI auto-upload was removed because placeholder
|
|
94
|
+
# starter content was landing as canonical evidence; the portal form
|
|
95
|
+
# ensures the operator reviews each refresh before it counts.
|
|
96
|
+
upload_governance() {
|
|
97
|
+
local FILE="$1" TYPE="$2"
|
|
98
|
+
if [ ! -f "$FILE" ]; then return 0; fi
|
|
99
|
+
echo "Uploading governance: $(basename "$FILE") (type=${TYPE})"
|
|
100
|
+
bash scripts/upload-evidence.sh \
|
|
101
|
+
${DEVAUDIT_PROJECT_SLUG} _compliance-docs "$TYPE" "$FILE" \
|
|
102
|
+
--category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
103
|
+
"${DERIVED_META[@]}" \
|
|
104
|
+
|| echo "Warning: Failed to upload $(basename "$FILE")"
|
|
105
|
+
}
|
|
106
|
+
# Recognise governance docs at top-level OR under compliance/governance/
|
|
107
|
+
# (operator's choice — both layouts are common).
|
|
108
|
+
upload_governance compliance/periodic-review.md periodic_review
|
|
109
|
+
upload_governance compliance/governance/periodic-review.md periodic_review
|
|
110
|
+
frontmatter_value() {
|
|
111
|
+
local FILE="$1" KEY="$2"
|
|
112
|
+
awk -v key="$KEY" '
|
|
113
|
+
NR == 1 && $0 == "---" { in_fm=1; next }
|
|
114
|
+
in_fm && $0 == "---" { exit }
|
|
115
|
+
in_fm && $0 ~ "^[[:space:]]*" key ":" {
|
|
116
|
+
sub("^[[:space:]]*" key ":[[:space:]]*", "")
|
|
117
|
+
gsub(/^["'\'' ]+|["'\'' ]+$/, "")
|
|
118
|
+
print
|
|
119
|
+
exit
|
|
120
|
+
}
|
|
121
|
+
' "$FILE" 2>/dev/null || true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
bundle_manifest_allows_source_release() {
|
|
125
|
+
local SOURCE_RELEASE="$1" MANIFEST=""
|
|
126
|
+
[ -n "$SOURCE_RELEASE" ] || return 1
|
|
127
|
+
for MANIFEST in \
|
|
128
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json" \
|
|
129
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json" \
|
|
130
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json"; do
|
|
131
|
+
[ -f "$MANIFEST" ] || continue
|
|
132
|
+
if jq -e --arg source "$SOURCE_RELEASE" '
|
|
133
|
+
any(.members[]?;
|
|
134
|
+
.version == $source
|
|
135
|
+
and (
|
|
136
|
+
.evidenceInheritancePolicy.mode == "all_eligible"
|
|
137
|
+
or any(.evidenceInheritancePolicy.scopes[]?; . == "release" or . == "stage" or . == "approval")
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
' "$MANIFEST" >/dev/null 2>&1; then
|
|
141
|
+
return 0
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
return 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Incident reports are selected by frontmatter ownership, not by
|
|
148
|
+
# repository-wide glob attribution. Historical files may stay on disk,
|
|
149
|
+
# but only the incident/no-incident artefact whose source release is
|
|
150
|
+
# the current derived release is uploaded to that release. If the
|
|
151
|
+
# current release has an explicit bundle manifest that grants
|
|
152
|
+
# inheritance for a predecessor, upload the file to the SOURCE release
|
|
153
|
+
# so the portal can inherit by lineage without relabelling ownership.
|
|
154
|
+
upload_incident_report() {
|
|
155
|
+
local FILE="$1"
|
|
156
|
+
if [ ! -f "$FILE" ]; then return 0; fi
|
|
157
|
+
local KIND SOURCE_RELEASE RELEASE_SCOPE SOURCE_ISSUE SEMANTIC_ID CONTENT_HASH TARGET_RELEASE
|
|
158
|
+
KIND="$(frontmatter_value "$FILE" incident_kind)"
|
|
159
|
+
SOURCE_RELEASE="$(frontmatter_value "$FILE" source_release)"
|
|
160
|
+
RELEASE_SCOPE="$(frontmatter_value "$FILE" release_scope)"
|
|
161
|
+
SOURCE_ISSUE="$(frontmatter_value "$FILE" source_issue)"
|
|
162
|
+
SEMANTIC_ID="$(frontmatter_value "$FILE" semantic_id)"
|
|
163
|
+
[ -n "$SEMANTIC_ID" ] || SEMANTIC_ID="$(frontmatter_value "$FILE" incident_id)"
|
|
164
|
+
|
|
165
|
+
case "$KIND" in
|
|
166
|
+
incident|no_incidents_attestation) ;;
|
|
167
|
+
"")
|
|
168
|
+
echo "::error::$(basename "$FILE"): missing required frontmatter incident_kind. Expected incident or no_incidents_attestation."
|
|
169
|
+
return 1 ;;
|
|
170
|
+
*)
|
|
171
|
+
echo "::error::$(basename "$FILE"): invalid incident_kind '${KIND}'. Expected incident or no_incidents_attestation."
|
|
172
|
+
return 1 ;;
|
|
173
|
+
esac
|
|
174
|
+
if [ "$KIND" = "no_incidents_attestation" ] && [ -z "$SOURCE_RELEASE" ]; then
|
|
175
|
+
echo "::error::$(basename "$FILE"): nil incident reports require source_release frontmatter."
|
|
176
|
+
return 1
|
|
177
|
+
fi
|
|
178
|
+
if [ "$KIND" = "incident" ]; then
|
|
179
|
+
if [ -z "$SOURCE_RELEASE" ] && [ -z "$RELEASE_SCOPE" ]; then
|
|
180
|
+
echo "::error::$(basename "$FILE"): incident reports require source_release or release_scope frontmatter."
|
|
181
|
+
return 1
|
|
182
|
+
fi
|
|
183
|
+
if [ -z "$SOURCE_ISSUE" ]; then
|
|
184
|
+
echo "::error::$(basename "$FILE"): incident reports require source_issue frontmatter."
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
fi
|
|
188
|
+
if [ -z "$SEMANTIC_ID" ]; then
|
|
189
|
+
echo "::error::$(basename "$FILE"): incident reports require semantic_id or incident_id frontmatter."
|
|
190
|
+
return 1
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
TARGET_RELEASE="${SOURCE_RELEASE:-$RELEASE_SCOPE}"
|
|
194
|
+
if [ "$TARGET_RELEASE" != "$DERIVED_RELEASE" ]; then
|
|
195
|
+
if bundle_manifest_allows_source_release "$TARGET_RELEASE"; then
|
|
196
|
+
echo "Incident $(basename "$FILE") is inherited by ${DERIVED_RELEASE}; preserving source release ${TARGET_RELEASE}."
|
|
197
|
+
else
|
|
198
|
+
echo "::notice::Skipping $(basename "$FILE"): source release ${TARGET_RELEASE} is not ${DERIVED_RELEASE} and no explicit bundle manifest grants inheritance."
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# Completeness gate — check for REPLACE markers in structured sections.
|
|
204
|
+
# §4 Root cause, §5 Impact, §9 Sign-off (populated reports).
|
|
205
|
+
# For nil reports, only check the Sign-off section.
|
|
206
|
+
if grep -q '## 4\. Root cause' "$FILE" 2>/dev/null; then
|
|
207
|
+
SECTION=$(sed -n '/^## 4\. Root cause/,/^## [0-9]/p' "$FILE")
|
|
208
|
+
if echo "$SECTION" | grep -q 'REPLACE'; then
|
|
209
|
+
echo "::warning::$(basename "$FILE"): §4 (Root cause) contains REPLACE markers — skipping upload. Complete the section and push to develop."
|
|
210
|
+
return 0
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
if grep -q '## 5\. Impact' "$FILE" 2>/dev/null; then
|
|
214
|
+
SECTION=$(sed -n '/^## 5\. Impact/,/^## [0-9]/p' "$FILE")
|
|
215
|
+
if echo "$SECTION" | grep -q 'REPLACE'; then
|
|
216
|
+
echo "::warning::$(basename "$FILE"): §5 (Impact) contains REPLACE markers — skipping upload. Complete the section and push to develop."
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
fi
|
|
220
|
+
if grep -q '## 9\. Sign-off' "$FILE" 2>/dev/null; then
|
|
221
|
+
SECTION=$(sed -n '/^## 9\. Sign-off/,/^## /p' "$FILE")
|
|
222
|
+
if echo "$SECTION" | grep -q 'REPLACE'; then
|
|
223
|
+
echo "::warning::$(basename "$FILE"): §9 (Sign-off) contains REPLACE markers — skipping upload. Complete the sign-off and push to develop."
|
|
224
|
+
return 0
|
|
225
|
+
fi
|
|
226
|
+
fi
|
|
227
|
+
# Nil reports have a simpler sign-off section — check it too.
|
|
228
|
+
if grep -q '## Sign-off' "$FILE" 2>/dev/null && ! grep -q '## 9\. Sign-off' "$FILE" 2>/dev/null; then
|
|
229
|
+
SECTION=$(sed -n '/^## Sign-off/,/^## /p' "$FILE")
|
|
230
|
+
if echo "$SECTION" | grep -q 'REPLACE'; then
|
|
231
|
+
echo "::warning::$(basename "$FILE"): Sign-off section contains REPLACE markers — skipping upload. Complete the sign-off and push to develop."
|
|
232
|
+
return 0
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
CONTENT_HASH="sha256:$(sha256sum "$FILE" | awk '{print $1}')"
|
|
236
|
+
echo "Uploading governance: $(basename "$FILE") (type=incident_report, source_release=${TARGET_RELEASE})"
|
|
237
|
+
bash scripts/upload-evidence.sh \
|
|
238
|
+
${DEVAUDIT_PROJECT_SLUG} _compliance-docs incident_report "$FILE" \
|
|
239
|
+
--category planning ${FLAGS} --release "${TARGET_RELEASE}" \
|
|
240
|
+
--evidence-scope release \
|
|
241
|
+
--meta-key "incident_kind=${KIND}" \
|
|
242
|
+
--meta-key "source_release=${TARGET_RELEASE}" \
|
|
243
|
+
--meta-key "source_issue=${SOURCE_ISSUE:-none}" \
|
|
244
|
+
--meta-key "semantic_id=${SEMANTIC_ID}" \
|
|
245
|
+
--meta-key "content_hash=${CONTENT_HASH}" \
|
|
246
|
+
"${DERIVED_META[@]}" \
|
|
247
|
+
|| echo "Warning: Failed to upload $(basename "$FILE")"
|
|
248
|
+
}
|
|
249
|
+
shopt -s nullglob
|
|
250
|
+
for f in compliance/incident-report*.md compliance/governance/incident-report*.md; do
|
|
251
|
+
upload_incident_report "$f"
|
|
252
|
+
done
|
|
253
|
+
# DevAudit-Installer#210 §8a: nil incident reports (per-release
|
|
254
|
+
# "no incidents" attestations). Glob alongside populated reports
|
|
255
|
+
# and upload as incident_report evidence.
|
|
256
|
+
for f in compliance/nil-incident-report*.md compliance/governance/nil-incident-report*.md; do
|
|
257
|
+
upload_incident_report "$f"
|
|
258
|
+
done
|
|
259
|
+
shopt -u nullglob
|
|
260
|
+
|
|
261
|
+
# ── Audit-log export (DevAudit-Installer#98 WS2) ──────────────
|
|
262
|
+
# Snapshot the portal's audit log for the rolling 90-day window
|
|
263
|
+
# and upload as `evidence_type=audit_log`. Closes three
|
|
264
|
+
# framework-coverage clauses on every release:
|
|
265
|
+
# - ISO27001.A.8.16 — Monitoring activities
|
|
266
|
+
# - EUAIA.Art-12 — Record-keeping (automatic logging)
|
|
267
|
+
# - GDPR.Art-32 — Security of processing (audit-log half)
|
|
268
|
+
#
|
|
269
|
+
# The portal endpoint defaults to the last 90 days when no
|
|
270
|
+
# `since`/`until` query params are passed; omit them so the
|
|
271
|
+
# consumer side stays zero-config. Endpoint shipped in
|
|
272
|
+
# META-COMPLY PR #413; project-scoped API key (uploader role)
|
|
273
|
+
# already has read access via `resolveCiUploadAuth`.
|
|
274
|
+
AUDIT_LOG_FILE="$(mktemp -t audit-log-XXXXXX.json)"
|
|
275
|
+
if curl -sSf -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
276
|
+
"${DEVAUDIT_BASE_URL%/}/api/ci/projects/${DEVAUDIT_PROJECT_SLUG}/audit-log/export" \
|
|
277
|
+
-o "$AUDIT_LOG_FILE"; then
|
|
278
|
+
echo "Uploading: audit-log.json (audit_log — 90-day window)"
|
|
279
|
+
bash scripts/upload-evidence.sh \
|
|
280
|
+
${DEVAUDIT_PROJECT_SLUG} _compliance-docs audit_log "$AUDIT_LOG_FILE" \
|
|
281
|
+
--category audit_log ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
282
|
+
"${DERIVED_META[@]}" \
|
|
283
|
+
|| echo "Warning: Failed to upload audit-log.json"
|
|
284
|
+
else
|
|
285
|
+
# Soft-fail: an export hiccup shouldn't break the rest of the
|
|
286
|
+
# evidence pipeline. Surfaces as a warning in the workflow log;
|
|
287
|
+
# the framework-coverage panel will show MISSING for the three
|
|
288
|
+
# clauses above until the next successful upload.
|
|
289
|
+
echo "::warning::Audit-log export failed — endpoint unreachable or 4xx/5xx. Three framework-coverage clauses (ISO27001.A.8.16, EUAIA.Art-12, GDPR.Art-32 audit-log half) will stay MISSING until the next run."
|
|
290
|
+
fi
|
|
291
|
+
rm -f "$AUDIT_LOG_FILE"
|
|
292
|
+
|
|
293
|
+
# Helper: emit `--release-title …` `--release-summary …` `--change-type …`
|
|
294
|
+
# args for a given REQ, derived from its release-ticket metadata and
|
|
295
|
+
# the most recent commit attributed to that REQ. Empty pair when
|
|
296
|
+
# neither is available. Uses the shared extraction helper so both
|
|
297
|
+
# workflow paths produce the same title/summary for the same REQ.
|
|
298
|
+
# DevAudit-Installer#285.
|
|
299
|
+
req_meta_args() {
|
|
300
|
+
local REQ="$1"; local CT=""
|
|
301
|
+
# Source the shared helper and extract canonical title + summary
|
|
302
|
+
source scripts/extract-release-metadata.sh 2>/dev/null || true
|
|
303
|
+
extract_release_metadata "$REQ" 2>/dev/null || true
|
|
304
|
+
CT=$(git log --grep "\[${REQ}\]\|Ref: ${REQ}" --pretty=%s -1 2>/dev/null \
|
|
305
|
+
| grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
|
|
306
|
+
| head -1 || true)
|
|
307
|
+
# Use if/then/fi (not `[ … ] && cmd`) — under bash -eo pipefail a
|
|
308
|
+
# trailing `[ … ] && cmd` whose test fails returns 1, and the
|
|
309
|
+
# function inherits that exit code; calling via $(req_meta_args …)
|
|
310
|
+
# then aborts the step silently (DevAudit-Installer#77).
|
|
311
|
+
if [ -n "$RELEASE_TITLE" ]; then printf -- '--release-title %q ' "$RELEASE_TITLE"; fi
|
|
312
|
+
if [ -n "$RELEASE_SUMMARY" ]; then printf -- '--release-summary %q ' "$RELEASE_SUMMARY"; fi
|
|
313
|
+
if [ -n "$CT" ]; then printf -- '--change-type %q ' "$CT"; fi
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Upload root-level housekeeping security summaries:
|
|
317
|
+
# compliance/security-summary-vYYYY.MM.DD.md
|
|
318
|
+
# compliance/security-summary-vYYYY.MM.DD.N.md
|
|
319
|
+
#
|
|
320
|
+
# These are generated by the housekeeping stub path and belong to
|
|
321
|
+
# the bare-date housekeeping release encoded in the filename, not
|
|
322
|
+
# to whichever release the current commit happened to derive.
|
|
323
|
+
shopt -s nullglob
|
|
324
|
+
for SECSUM in compliance/security-summary-*.md; do
|
|
325
|
+
[ -f "$SECSUM" ] || continue
|
|
326
|
+
SECSUM_VER=$(basename "$SECSUM" .md | sed 's/^security-summary-//')
|
|
327
|
+
if ! echo "$SECSUM_VER" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
|
|
328
|
+
echo "::warning::Skipping $(basename "$SECSUM"): filename does not encode a housekeeping release version."
|
|
329
|
+
continue
|
|
330
|
+
fi
|
|
331
|
+
echo "Uploading housekeeping security summary: $(basename "$SECSUM") -> release ${SECSUM_VER}"
|
|
332
|
+
bash scripts/upload-evidence.sh \
|
|
333
|
+
${DEVAUDIT_PROJECT_SLUG} _compliance-docs security_summary "$SECSUM" \
|
|
334
|
+
--category security ${FLAGS} --release "${SECSUM_VER}" \
|
|
335
|
+
"${DERIVED_META[@]}" \
|
|
336
|
+
|| echo "Warning: Failed to upload $(basename "$SECSUM")"
|
|
337
|
+
done
|
|
338
|
+
shopt -u nullglob
|
|
339
|
+
|
|
340
|
+
# Upload release tickets (pending only)
|
|
341
|
+
if [ -d "compliance/pending-releases" ]; then
|
|
342
|
+
for TICKET in compliance/pending-releases/*.md; do
|
|
343
|
+
[ -f "$TICKET" ] || continue
|
|
344
|
+
# A RELEASE-TICKET-REQ-XXX.md belongs to that requirement's
|
|
345
|
+
# release record; any other ticket rides the derived release.
|
|
346
|
+
TICKET_BASE=$(basename "$TICKET" .md)
|
|
347
|
+
case "$TICKET_BASE" in
|
|
348
|
+
RELEASE-TICKET-REQ-*)
|
|
349
|
+
TICKET_REQ="${TICKET_BASE#RELEASE-TICKET-}"
|
|
350
|
+
TICKET_OWNER="$TICKET_REQ"; TICKET_RELEASE="$TICKET_REQ"
|
|
351
|
+
TICKET_META_ARGS=$(req_meta_args "$TICKET_REQ") ;;
|
|
352
|
+
*)
|
|
353
|
+
TICKET_OWNER="_compliance-docs"; TICKET_RELEASE="$DERIVED_RELEASE"
|
|
354
|
+
TICKET_META_ARGS="" ;;
|
|
355
|
+
esac
|
|
356
|
+
echo "Uploading: $(basename "$TICKET") -> release ${TICKET_RELEASE}"
|
|
357
|
+
eval "bash scripts/upload-evidence.sh \
|
|
358
|
+
${DEVAUDIT_PROJECT_SLUG} \"${TICKET_OWNER}\" release_ticket \"$TICKET\" \
|
|
359
|
+
--category release_artifact ${FLAGS} --release \"${TICKET_RELEASE}\" \
|
|
360
|
+
${TICKET_META_ARGS}" \
|
|
361
|
+
|| echo "Warning: Failed to upload $(basename "$TICKET")"
|
|
362
|
+
done
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# Upload per-requirement evidence — scoped to requirements with a
|
|
366
|
+
# pending release ticket. Without this scoping every historical
|
|
367
|
+
# compliance/evidence/REQ-*/ folder would be re-uploaded on every
|
|
368
|
+
# run, re-populating the release-requirement matrix with the full
|
|
369
|
+
# project catalogue (DevAudit #135, sibling of #133).
|
|
370
|
+
IN_SCOPE_REQS=()
|
|
371
|
+
if [ -d compliance/pending-releases ]; then
|
|
372
|
+
for TICKET in compliance/pending-releases/RELEASE-TICKET-REQ-*.md; do
|
|
373
|
+
[ -f "$TICKET" ] || continue
|
|
374
|
+
REQ_ID=$(basename "$TICKET" .md | sed 's/^RELEASE-TICKET-//')
|
|
375
|
+
# devaudit-installer#192 — skip REQs already in a terminal
|
|
376
|
+
# release directory. See ci.yml.template for full rationale.
|
|
377
|
+
if compgen -G "compliance/approved-releases/RELEASE-TICKET-${REQ_ID}.md" > /dev/null 2>&1 \
|
|
378
|
+
|| compgen -G "compliance/superseded-releases/RELEASE-TICKET-${REQ_ID}.md" > /dev/null 2>&1; then
|
|
379
|
+
echo "Skipping ${REQ_ID}: already in a terminal release directory (stale pending ticket)"
|
|
380
|
+
continue
|
|
381
|
+
fi
|
|
382
|
+
IN_SCOPE_REQS+=("$REQ_ID")
|
|
383
|
+
done
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
if [ ${#IN_SCOPE_REQS[@]} -eq 0 ]; then
|
|
387
|
+
echo "No pending release tickets found — skipping per-requirement evidence upload"
|
|
388
|
+
else
|
|
389
|
+
echo "In-scope requirements for this release: ${IN_SCOPE_REQS[*]}"
|
|
390
|
+
for REQ_ID in "${IN_SCOPE_REQS[@]}"; do
|
|
391
|
+
REQ_DIR="compliance/evidence/${REQ_ID}/"
|
|
392
|
+
if [ ! -d "$REQ_DIR" ]; then
|
|
393
|
+
echo "Warning: pending ticket for ${REQ_ID} but no ${REQ_DIR} on disk"
|
|
394
|
+
continue
|
|
395
|
+
fi
|
|
396
|
+
REQ_META_ARGS=$(req_meta_args "$REQ_ID")
|
|
397
|
+
for ARTIFACT in "$REQ_DIR"*.md; do
|
|
398
|
+
[ -f "$ARTIFACT" ] || continue
|
|
399
|
+
# Per-REQ basename → (evidence_type, evidence_category) routing.
|
|
400
|
+
# Every SDLC-expected artifact has an explicit case with a
|
|
401
|
+
# dedicated evidence_type so the portal's framework-coverage
|
|
402
|
+
# predicates attribute them to the correct clause. The `*)`
|
|
403
|
+
# catch-all was removed (DevAudit-Installer#205) — unrecognized
|
|
404
|
+
# filenames are now skipped with a warning instead of being
|
|
405
|
+
# silently uploaded as compliance_document/planning, which
|
|
406
|
+
# caused wrong artifacts to satisfy wrong framework clauses.
|
|
407
|
+
#
|
|
408
|
+
# Evidence type → framework clause mapping:
|
|
409
|
+
#
|
|
410
|
+
# - test-execution-summary.md → test_report
|
|
411
|
+
# ISO 29119-3 §3.5.6 Test Completion Report.
|
|
412
|
+
# DevAudit-Installer#101.
|
|
413
|
+
#
|
|
414
|
+
# - srs-alignment.md → srs_alignment
|
|
415
|
+
# Output of requirements-aligner skill at Stage 3.
|
|
416
|
+
# DevAudit-Installer#119.
|
|
417
|
+
#
|
|
418
|
+
# - architecture-decision.md → architecture_decision
|
|
419
|
+
# ISO 27001 A.8.25 (Secure SDLC).
|
|
420
|
+
# DevAudit-Installer#120.
|
|
421
|
+
#
|
|
422
|
+
# - risk-assessment.md → risk_assessment
|
|
423
|
+
# SOC 2 CC3.2 (Risk identification).
|
|
424
|
+
# DevAudit-Installer#121.
|
|
425
|
+
#
|
|
426
|
+
# - test-scope.md → test_scope
|
|
427
|
+
# ISO 29119 §3.3 (Test scope).
|
|
428
|
+
#
|
|
429
|
+
# - test-plan.md → test_plan
|
|
430
|
+
# ISO 29119 §3.4 (Test plan, per-REQ).
|
|
431
|
+
#
|
|
432
|
+
# - implementation-plan.md → implementation_plan
|
|
433
|
+
# ISO 27001 A.8.25 (Secure SDLC planning).
|
|
434
|
+
#
|
|
435
|
+
# - security-summary.md → security_summary
|
|
436
|
+
# ISO 27001 A.8.28 / SOC2 CC6.1.
|
|
437
|
+
#
|
|
438
|
+
# - ai-use-note.md → ai_use_note
|
|
439
|
+
# EUAIA Art. 13 (Transparency).
|
|
440
|
+
#
|
|
441
|
+
# - ai-prompts.md → ai_prompt_log
|
|
442
|
+
# EUAIA Art. 12 (Record-keeping).
|
|
443
|
+
#
|
|
444
|
+
# - ai-agent-handoff.md → ai_agent_handoff
|
|
445
|
+
# EUAIA Art. 14 (Human oversight).
|
|
446
|
+
#
|
|
447
|
+
# DevAudit-Installer#146, #205.
|
|
448
|
+
BASENAME=$(basename "$ARTIFACT")
|
|
449
|
+
case "$BASENAME" in
|
|
450
|
+
test-execution-summary.md|test-summary-report.md)
|
|
451
|
+
EVTYPE=test_report; EVCAT=test_report ;;
|
|
452
|
+
srs-alignment.md)
|
|
453
|
+
EVTYPE=srs_alignment; EVCAT=planning ;;
|
|
454
|
+
architecture-decision.md)
|
|
455
|
+
EVTYPE=architecture_decision; EVCAT=planning ;;
|
|
456
|
+
risk-assessment.md)
|
|
457
|
+
EVTYPE=risk_assessment; EVCAT=planning ;;
|
|
458
|
+
test-scope.md)
|
|
459
|
+
EVTYPE=test_scope; EVCAT=planning ;;
|
|
460
|
+
test-plan.md)
|
|
461
|
+
EVTYPE=test_plan; EVCAT=planning ;;
|
|
462
|
+
implementation-plan.md)
|
|
463
|
+
EVTYPE=implementation_plan; EVCAT=planning ;;
|
|
464
|
+
security-summary.md)
|
|
465
|
+
EVTYPE=security_summary; EVCAT=security ;;
|
|
466
|
+
ai-use-note.md)
|
|
467
|
+
EVTYPE=ai_use_note; EVCAT=ai_governance ;;
|
|
468
|
+
ai-prompts.md)
|
|
469
|
+
EVTYPE=ai_prompt_log; EVCAT=ai_governance ;;
|
|
470
|
+
ai-agent-handoff.md)
|
|
471
|
+
EVTYPE=ai_agent_handoff; EVCAT=ai_governance ;;
|
|
472
|
+
*)
|
|
473
|
+
echo "::warning::Unrecognized artifact ${REQ_ID}/${BASENAME} — skipping upload. Add explicit routing in compliance-evidence.yml if this is a new evidence type. (DevAudit-Installer#205)"
|
|
474
|
+
continue ;;
|
|
475
|
+
esac
|
|
476
|
+
echo "Uploading: ${REQ_ID}/${BASENAME} (${EVTYPE})"
|
|
477
|
+
eval "bash scripts/upload-evidence.sh \
|
|
478
|
+
${DEVAUDIT_PROJECT_SLUG} \"${REQ_ID}\" ${EVTYPE} \"$ARTIFACT\" \
|
|
479
|
+
--category ${EVCAT} ${FLAGS} --release \"${REQ_ID}\" \
|
|
480
|
+
${REQ_META_ARGS}" \
|
|
481
|
+
|| echo "Warning: Failed to upload ${BASENAME}"
|
|
482
|
+
done
|
|
483
|
+
done
|
|
484
|
+
fi
|