@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.3.25",
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.25",
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
- r"^[ \t\-*]*Release:\s*((?:REQ-\d+)|(?:v\d+\.\d+\.\d+)|(?:v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?))\b",
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, flags)
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