@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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +18 -0
- package/sdlc/files/ci/check-release-approval.yml.template +39 -4
- package/sdlc/files/ci/ci.yml.template +70 -11
- package/sdlc/files/ci/compliance-evidence.yml.template +47 -9
- package/sdlc/files/ci/python/ci.yml.template +52 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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}"
|
|
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}"
|
|
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}"
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|