@metasession.co/devaudit-cli 0.1.20 → 0.1.22

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.1.20",
3
+ "version": "0.1.22",
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.1.20",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.1.22",
37
37
  "commander": "^12.1.0",
38
38
  "consola": "^3.2.3",
39
39
  "env-paths": "^3.0.0",
@@ -17,6 +17,15 @@
17
17
  # --category <cat> Evidence category: ci_pipeline, local_dev,
18
18
  # planning, test_report, security_scan,
19
19
  # release_artifact
20
+ # --release-title <text> Human title for the release row (e.g. the
21
+ # release-ticket H1). Forwarded as
22
+ # `releaseTitle`; the portal no-clobbers
23
+ # existing non-null values.
24
+ # --change-type <type> Conventional-commit prefix (feat / fix /
25
+ # refactor / perf / chore / docs / ci /
26
+ # build / test / compliance / revert) for
27
+ # the release row. Unknown values are
28
+ # silently dropped server-side.
20
29
  #
21
30
  # Required environment variables:
22
31
  # DEVAUDIT_BASE_URL e.g. https://meta-comply-production.up.railway.app
@@ -54,6 +63,8 @@ RELEASE_VERSION=""
54
63
  CREATE_RELEASE_IF_MISSING=false
55
64
  ENVIRONMENT=""
56
65
  EVIDENCE_CATEGORY=""
66
+ RELEASE_TITLE=""
67
+ CHANGE_TYPE=""
57
68
 
58
69
  while [ "$#" -gt 0 ]; do
59
70
  case "$1" in
@@ -64,6 +75,11 @@ while [ "$#" -gt 0 ]; do
64
75
  --create-release-if-missing) CREATE_RELEASE_IF_MISSING=true; shift ;;
65
76
  --environment) ENVIRONMENT="$2"; shift 2 ;;
66
77
  --category) EVIDENCE_CATEGORY="$2"; shift 2 ;;
78
+ # Descriptive title + conventional-commit change type passed through to
79
+ # the portal's findOrCreateRelease no-clobber backfill. Both optional;
80
+ # unknown change-type values are dropped server-side, not 400'd.
81
+ --release-title) RELEASE_TITLE="$2"; shift 2 ;;
82
+ --change-type) CHANGE_TYPE="$2"; shift 2 ;;
67
83
  *) echo "Unknown option: $1"; exit 1 ;;
68
84
  esac
69
85
  done
@@ -163,6 +179,8 @@ for FILE in "${FILES[@]}"; do
163
179
  [ -n "$BRANCH" ] && CURL_ARGS+=(-F "releaseBranch=${BRANCH}")
164
180
  [ -n "$ENVIRONMENT" ] && CURL_ARGS+=(-F "environment=${ENVIRONMENT}")
165
181
  [ -n "$EVIDENCE_CATEGORY" ] && CURL_ARGS+=(-F "evidenceCategory=${EVIDENCE_CATEGORY}")
182
+ [ -n "$RELEASE_TITLE" ] && CURL_ARGS+=(-F "releaseTitle=${RELEASE_TITLE}")
183
+ [ -n "$CHANGE_TYPE" ] && CURL_ARGS+=(-F "changeType=${CHANGE_TYPE}")
166
184
 
167
185
  ATTEMPT=1
168
186
  BACKOFF=$INITIAL_BACKOFF_SECONDS
@@ -70,8 +70,18 @@ jobs:
70
70
  esac
71
71
  # Bootstrap probe (#301): project may not exist in DevAudit yet —
72
72
  # the first compliance-evidence.yml run auto-creates it. A 404 here
73
- # means we're on the introducing PR; pass with a notice. 401/403
73
+ # means we're on the introducing PR; pass with a warning. 401/403
74
74
  # means the API key is invalid → fail (not bootstrap).
75
+ #
76
+ # Defense in depth (#74): if /api/ci/projects/<slug> 404s we
77
+ # cross-check against /api/ci/releases/resolve — a known-good
78
+ # read endpoint scoped to the same project. If THAT returns 2xx
79
+ # the project clearly exists, and the projects-endpoint 404 is a
80
+ # portal-side bug. Failing closed beats silently passing the
81
+ # four-eyes gate. The original "GET /api/ci/projects/<slug>"
82
+ # endpoint didn't exist on the portal before metasession-dev/
83
+ # devaudit#NN, so this exact false-positive was the universal
84
+ # state of the gate across every consumer.
75
85
  PROJ_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
76
86
  -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
77
87
  "${BASE%/}/api/ci/projects/${PROJECT_SLUG}" || echo "000")
@@ -80,9 +90,34 @@ jobs:
80
90
  echo "DevAudit project '${PROJECT_SLUG}' confirmed (HTTP ${PROJ_CODE})"
81
91
  ;;
82
92
  404)
83
- echo "::notice::DevAudit project '${PROJECT_SLUG}' does not exist yet (HTTP 404) — bootstrap mode. Gate passes. The project will be auto-created by the first compliance-evidence.yml run; enforcement kicks in on the next PR after that."
84
- echo "BOOTSTRAP_MODE=true" >> "$GITHUB_ENV"
85
- exit 0
93
+ # Cross-check: does the project actually exist? versionPrefix=v
94
+ # matches every release version shape (REQ-XXX, vYYYY.MM.DD,
95
+ # vX.Y.Z) — we don't care about the body, only whether the
96
+ # endpoint authorises the project. Endpoint returns 200 even
97
+ # when no releases match the prefix (just with latest: null).
98
+ CROSS_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
99
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
100
+ "${BASE%/}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=v" \
101
+ || echo "000")
102
+ case "$CROSS_CODE" in
103
+ 2*)
104
+ echo "::error::Portal /api/ci/projects/${PROJECT_SLUG} returned 404 but releases/resolve confirms the project exists (HTTP ${CROSS_CODE}). This is a portal-side issue (missing or broken endpoint), not a bootstrap. Failing closed to avoid silently bypassing the four-eyes gate. Triage at metasession-dev/devaudit (and/or DevAudit-Installer#75)."
105
+ exit 1
106
+ ;;
107
+ 404)
108
+ echo "::warning::DevAudit project '${PROJECT_SLUG}' does not exist yet (HTTP 404 from both /api/ci/projects and /api/ci/releases/resolve) — bootstrap mode. Gate passes. The project will be auto-created by the first compliance-evidence.yml run; enforcement kicks in on the next PR after that."
109
+ echo "BOOTSTRAP_MODE=true" >> "$GITHUB_ENV"
110
+ exit 0
111
+ ;;
112
+ 401|403)
113
+ echo "::error::DevAudit returned HTTP ${CROSS_CODE} for releases/resolve on project '${PROJECT_SLUG}' (cross-check after the projects endpoint 404'd) — API key is invalid or lacks access. Verify DEVAUDIT_API_KEY belongs to the right project."
114
+ exit 1
115
+ ;;
116
+ *)
117
+ echo "::error::Cross-check endpoint /api/ci/releases/resolve returned unexpected HTTP ${CROSS_CODE} after the projects endpoint 404'd. Investigate before retrying."
118
+ exit 1
119
+ ;;
120
+ esac
86
121
  ;;
87
122
  401|403)
88
123
  echo "::error::DevAudit returned HTTP ${PROJ_CODE} for project '${PROJECT_SLUG}' — API key is invalid or lacks access. Verify DEVAUDIT_API_KEY belongs to the right project."
@@ -212,20 +212,64 @@ jobs:
212
212
  --git-sha ${{ github.sha }} --branch ${{ github.ref_name }} || true
213
213
 
214
214
  - name: Sync known requirements from RTM
215
+ env:
216
+ GH_TOKEN: ${{ github.token }}
215
217
  run: |
216
- if [ -f "compliance/RTM.md" ]; then
217
- REQS=$(grep -oP 'REQ-\d+' compliance/RTM.md | sort -t- -k2 -n -u)
218
- if [ -n "$REQS" ]; then
219
- JSON_ARRAY=$(echo "$REQS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
220
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
221
- -X PATCH "${BASE}/api/ci/projects/{{PROJECT_SLUG}}/known-requirements" \
222
- -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
223
- -H "Content-Type: application/json" \
224
- -d "{\"requirements\": ${JSON_ARRAY}}")
225
- echo "known_requirements sync: HTTP ${HTTP_CODE}"
226
- echo "Synced $(echo "$REQS" | wc -w) requirements from RTM.md"
218
+ if [ ! -f "compliance/RTM.md" ]; then exit 0; fi
219
+
220
+ # Per REQ row in RTM we look up: title (release-ticket H1, else the
221
+ # linked issue's title via `gh`), and risk_class (the first LOW /
222
+ # MEDIUM / HIGH / CRITICAL token in the row). Both are optional —
223
+ # the portal renders gracefully with nulls. The portal endpoint
224
+ # accepts either a bare string[] (legacy) or rich rows.
225
+ REQS_JSON='[]'
226
+ while IFS= read -r REQ; do
227
+ [ -z "$REQ" ] && continue
228
+ ROW=$(grep -m1 -E "^\| ${REQ} " compliance/RTM.md || true)
229
+ TITLE=""
230
+ # Title: release-ticket H1 first (pending then approved)
231
+ for FILE in "compliance/pending-releases/RELEASE-TICKET-${REQ}.md" \
232
+ "compliance/approved-releases/RELEASE-TICKET-${REQ}.md"; do
233
+ if [ -f "$FILE" ]; then
234
+ TITLE=$(grep -m1 '^# ' "$FILE" \
235
+ | sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
236
+ break
237
+ fi
238
+ done
239
+ # Title fallback: first issue # found anywhere in the row
240
+ if [ -z "$TITLE" ] && [ -n "$ROW" ]; then
241
+ ISSUE_NUM=$(echo "$ROW" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
242
+ if [ -n "$ISSUE_NUM" ]; then
243
+ TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq .title 2>/dev/null || true)
244
+ fi
245
+ fi
246
+ # Risk: first LOW/MEDIUM/HIGH/CRITICAL token in the row
247
+ RISK=""
248
+ if [ -n "$ROW" ]; then
249
+ RISK=$(echo "$ROW" | grep -ioE '\b(LOW|MEDIUM|HIGH|CRITICAL)\b' | head -1 \
250
+ | tr 'a-z' 'A-Z')
227
251
  fi
252
+ REQS_JSON=$(echo "$REQS_JSON" | jq \
253
+ --arg id "$REQ" --arg title "$TITLE" --arg risk "$RISK" \
254
+ '. + [{
255
+ id: $id,
256
+ title: (if $title == "" then null else $title end),
257
+ riskClass: (if $risk == "" then null else $risk end)
258
+ }]')
259
+ done < <(grep -oE 'REQ-[0-9]+' compliance/RTM.md | sort -t- -k2 -n -u)
260
+
261
+ COUNT=$(echo "$REQS_JSON" | jq length)
262
+ if [ "$COUNT" = "0" ]; then
263
+ echo "No REQ-XXX rows in RTM.md — skipping sync"
264
+ exit 0
228
265
  fi
266
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
267
+ -X PATCH "${BASE}/api/ci/projects/{{PROJECT_SLUG}}/known-requirements" \
268
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
269
+ -H "Content-Type: application/json" \
270
+ -d "{\"requirements\": ${REQS_JSON}}")
271
+ echo "known_requirements sync: HTTP ${HTTP_CODE}"
272
+ echo "Synced ${COUNT} requirements from RTM.md (titles + risk_class)"
229
273
 
230
274
  # ──────────────────────────────────────────────
231
275
  # Upload Evidence to DevAudit (after gates pass)
@@ -357,6 +401,20 @@ jobs:
357
401
  echo "Uploading ${#SHOTS[@]} evidence screenshot(s) for: ${SHOT_REQS[*]}"
358
402
  SHOT_TMP="$(mktemp -d)"
359
403
  for REQ in "${SHOT_REQS[@]}"; do
404
+ # Per-REQ release metadata for the portal (no-clobbered on existing rows):
405
+ # release title from the pending ticket's H1; change type from the
406
+ # latest commit that references the REQ.
407
+ REQ_TITLE=""
408
+ if [ -f "compliance/pending-releases/RELEASE-TICKET-${REQ}.md" ]; then
409
+ REQ_TITLE=$(grep -m1 '^# ' "compliance/pending-releases/RELEASE-TICKET-${REQ}.md" \
410
+ | sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
411
+ fi
412
+ REQ_CT=$(git log --grep "\[${REQ}\]\|Ref: ${REQ}" --pretty=%s -1 2>/dev/null \
413
+ | grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
414
+ | head -1 || true)
415
+ REQ_META=()
416
+ [ -n "$REQ_TITLE" ] && REQ_META+=(--release-title "$REQ_TITLE")
417
+ [ -n "$REQ_CT" ] && REQ_META+=(--change-type "$REQ_CT")
360
418
  for PNG in "${SHOTS[@]}"; do
361
419
  # The folder is the (SRS) requirement id, the basename is the AC
362
420
  # slug (ACn-…). Upload as <srs-req>-<slug>.png so the reviewer can
@@ -367,6 +425,7 @@ jobs:
367
425
  bash scripts/upload-evidence.sh \
368
426
  {{PROJECT_SLUG}} "$REQ" screenshot "$NAMED" \
369
427
  --category test_report ${FLAGS} --release "$REQ" \
428
+ "${REQ_META[@]}" \
370
429
  || echo "::warning::evidence screenshot upload failed: ${PNG} -> ${REQ}"
371
430
  done
372
431
  done
@@ -110,16 +110,47 @@ jobs:
110
110
  FLAGS="${FLAGS} --create-release-if-missing --environment uat"
111
111
  DERIVED_RELEASE="${{ steps.version.outputs.version }}"
112
112
 
113
+ # Derive change_type for the bare-date (housekeeping) DERIVED_RELEASE:
114
+ # the prefix of the most recent commit's subject. No-op for tracked
115
+ # releases — they get per-REQ derivation in the loop below.
116
+ DERIVED_CT=$(git log -1 --pretty=%s 2>/dev/null \
117
+ | grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
118
+ | head -1 || true)
119
+ DERIVED_META=()
120
+ [ -n "$DERIVED_CT" ] && DERIVED_META+=(--change-type "$DERIVED_CT")
121
+
113
122
  # Upload compliance docs (planning category)
114
123
  for DOC in compliance/RTM.md compliance/test-plan.md compliance/test-cases.md compliance/test-summary-report.md; do
115
124
  if [ -f "$DOC" ]; then
116
125
  echo "Uploading: $(basename "$DOC")"
117
126
  bash scripts/upload-evidence.sh \
118
127
  {{PROJECT_SLUG}} _compliance-docs compliance_document "$DOC" \
119
- --category planning ${FLAGS} --release "${DERIVED_RELEASE}" || echo "Warning: Failed to upload $(basename "$DOC")"
128
+ --category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
129
+ "${DERIVED_META[@]}" \
130
+ || echo "Warning: Failed to upload $(basename "$DOC")"
120
131
  fi
121
132
  done
122
133
 
134
+ # Helper: emit a `--release-title …` `--change-type …` pair for a given
135
+ # REQ, derived from its pending release-ticket H1 and the most recent
136
+ # commit attributed to that REQ. Empty pair when neither is available.
137
+ req_meta_args() {
138
+ local REQ="$1"; local TITLE=""; local CT=""
139
+ for FILE in "compliance/pending-releases/RELEASE-TICKET-${REQ}.md" \
140
+ "compliance/approved-releases/RELEASE-TICKET-${REQ}.md"; do
141
+ if [ -f "$FILE" ]; then
142
+ TITLE=$(grep -m1 '^# ' "$FILE" \
143
+ | sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
144
+ break
145
+ fi
146
+ done
147
+ CT=$(git log --grep "\[${REQ}\]\|Ref: ${REQ}" --pretty=%s -1 2>/dev/null \
148
+ | grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
149
+ | head -1 || true)
150
+ [ -n "$TITLE" ] && printf -- '--release-title %q ' "$TITLE"
151
+ [ -n "$CT" ] && printf -- '--change-type %q ' "$CT"
152
+ }
153
+
123
154
  # Upload release tickets (pending only)
124
155
  if [ -d "compliance/pending-releases" ]; then
125
156
  for TICKET in compliance/pending-releases/*.md; do
@@ -130,14 +161,18 @@ jobs:
130
161
  case "$TICKET_BASE" in
131
162
  RELEASE-TICKET-REQ-*)
132
163
  TICKET_REQ="${TICKET_BASE#RELEASE-TICKET-}"
133
- TICKET_OWNER="$TICKET_REQ"; TICKET_RELEASE="$TICKET_REQ" ;;
164
+ TICKET_OWNER="$TICKET_REQ"; TICKET_RELEASE="$TICKET_REQ"
165
+ TICKET_META_ARGS=$(req_meta_args "$TICKET_REQ") ;;
134
166
  *)
135
- TICKET_OWNER="_compliance-docs"; TICKET_RELEASE="$DERIVED_RELEASE" ;;
167
+ TICKET_OWNER="_compliance-docs"; TICKET_RELEASE="$DERIVED_RELEASE"
168
+ TICKET_META_ARGS="" ;;
136
169
  esac
137
170
  echo "Uploading: $(basename "$TICKET") -> release ${TICKET_RELEASE}"
138
- bash scripts/upload-evidence.sh \
139
- {{PROJECT_SLUG}} "${TICKET_OWNER}" compliance_document "$TICKET" \
140
- --category release_artifact ${FLAGS} --release "${TICKET_RELEASE}" || echo "Warning: Failed to upload $(basename "$TICKET")"
171
+ eval "bash scripts/upload-evidence.sh \
172
+ {{PROJECT_SLUG}} \"${TICKET_OWNER}\" compliance_document \"$TICKET\" \
173
+ --category release_artifact ${FLAGS} --release \"${TICKET_RELEASE}\" \
174
+ ${TICKET_META_ARGS}" \
175
+ || echo "Warning: Failed to upload $(basename "$TICKET")"
141
176
  done
142
177
  fi
143
178
 
@@ -165,12 +200,15 @@ jobs:
165
200
  echo "Warning: pending ticket for ${REQ_ID} but no ${REQ_DIR} on disk"
166
201
  continue
167
202
  fi
203
+ REQ_META_ARGS=$(req_meta_args "$REQ_ID")
168
204
  for ARTIFACT in "$REQ_DIR"*.md; do
169
205
  [ -f "$ARTIFACT" ] || continue
170
206
  echo "Uploading: ${REQ_ID}/$(basename "$ARTIFACT")"
171
- bash scripts/upload-evidence.sh \
172
- {{PROJECT_SLUG}} "${REQ_ID}" compliance_document "$ARTIFACT" \
173
- --category planning ${FLAGS} --release "${REQ_ID}" || echo "Warning: Failed to upload $(basename "$ARTIFACT")"
207
+ eval "bash scripts/upload-evidence.sh \
208
+ {{PROJECT_SLUG}} \"${REQ_ID}\" compliance_document \"$ARTIFACT\" \
209
+ --category planning ${FLAGS} --release \"${REQ_ID}\" \
210
+ ${REQ_META_ARGS}" \
211
+ || echo "Warning: Failed to upload $(basename "$ARTIFACT")"
174
212
  done
175
213
  done
176
214
  fi
@@ -196,20 +196,61 @@ jobs:
196
196
  --git-sha ${{ github.sha }} --branch ${{ github.ref_name }} || true
197
197
 
198
198
  - name: Sync known requirements from RTM
199
+ env:
200
+ GH_TOKEN: ${{ github.token }}
199
201
  run: |
200
- if [ -f "compliance/RTM.md" ]; then
201
- REQS=$(grep -oP 'REQ-\d+' compliance/RTM.md | sort -t- -k2 -n -u)
202
- if [ -n "$REQS" ]; then
203
- JSON_ARRAY=$(echo "$REQS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
204
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
205
- -X PATCH "${BASE}/api/ci/projects/{{PROJECT_SLUG}}/known-requirements" \
206
- -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
207
- -H "Content-Type: application/json" \
208
- -d "{\"requirements\": ${JSON_ARRAY}}")
209
- echo "known_requirements sync: HTTP ${HTTP_CODE}"
210
- echo "Synced $(echo "$REQS" | wc -w) requirements from RTM.md"
202
+ if [ ! -f "compliance/RTM.md" ]; then exit 0; fi
203
+
204
+ # Per REQ row in RTM we look up: title (release-ticket H1, else the
205
+ # linked issue's title via `gh`), and risk_class (the first LOW /
206
+ # MEDIUM / HIGH / CRITICAL token in the row). Both are optional —
207
+ # the portal renders gracefully with nulls. The portal endpoint
208
+ # accepts either a bare string[] (legacy) or rich rows.
209
+ REQS_JSON='[]'
210
+ while IFS= read -r REQ; do
211
+ [ -z "$REQ" ] && continue
212
+ ROW=$(grep -m1 -E "^\| ${REQ} " compliance/RTM.md || true)
213
+ TITLE=""
214
+ for FILE in "compliance/pending-releases/RELEASE-TICKET-${REQ}.md" \
215
+ "compliance/approved-releases/RELEASE-TICKET-${REQ}.md"; do
216
+ if [ -f "$FILE" ]; then
217
+ TITLE=$(grep -m1 '^# ' "$FILE" \
218
+ | sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
219
+ break
220
+ fi
221
+ done
222
+ if [ -z "$TITLE" ] && [ -n "$ROW" ]; then
223
+ ISSUE_NUM=$(echo "$ROW" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
224
+ if [ -n "$ISSUE_NUM" ]; then
225
+ TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq .title 2>/dev/null || true)
226
+ fi
227
+ fi
228
+ RISK=""
229
+ if [ -n "$ROW" ]; then
230
+ RISK=$(echo "$ROW" | grep -ioE '\b(LOW|MEDIUM|HIGH|CRITICAL)\b' | head -1 \
231
+ | tr 'a-z' 'A-Z')
211
232
  fi
233
+ REQS_JSON=$(echo "$REQS_JSON" | jq \
234
+ --arg id "$REQ" --arg title "$TITLE" --arg risk "$RISK" \
235
+ '. + [{
236
+ id: $id,
237
+ title: (if $title == "" then null else $title end),
238
+ riskClass: (if $risk == "" then null else $risk end)
239
+ }]')
240
+ done < <(grep -oE 'REQ-[0-9]+' compliance/RTM.md | sort -t- -k2 -n -u)
241
+
242
+ COUNT=$(echo "$REQS_JSON" | jq length)
243
+ if [ "$COUNT" = "0" ]; then
244
+ echo "No REQ-XXX rows in RTM.md — skipping sync"
245
+ exit 0
212
246
  fi
247
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
248
+ -X PATCH "${BASE}/api/ci/projects/{{PROJECT_SLUG}}/known-requirements" \
249
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
250
+ -H "Content-Type: application/json" \
251
+ -d "{\"requirements\": ${REQS_JSON}}")
252
+ echo "known_requirements sync: HTTP ${HTTP_CODE}"
253
+ echo "Synced ${COUNT} requirements from RTM.md (titles + risk_class)"
213
254
 
214
255
  upload-evidence:
215
256
  name: Upload Evidence