@metasession.co/devaudit-cli 0.1.19 → 0.1.21

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.19",
3
+ "version": "0.1.21",
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.19",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.1.21",
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
@@ -93,6 +93,8 @@ git diff origin/main..develop -- package.json | grep '^\+'
93
93
 
94
94
  ### Step 3: Create the PR
95
95
 
96
+ > The `--base main --head develop` below is the develop-first default. The branches are project-configured in `sdlc-config.json` — `release_branch` (default `main`) and `integration_branch` (default `develop`); a trunk-only project sets both to `main` and opens the feature branch directly against `main`.
97
+
96
98
  **For tracked requirements:**
97
99
 
98
100
  ```bash
@@ -47,6 +47,15 @@ Unit-test and integration-test work stays with this skill until a counterpart un
47
47
 
48
48
  A triage step (Phase 0) routes the issue, then up to five phases for tracked work. Phase 0 plus Phases 1–4 run in one Claude Code session; Phase 5 is invoked separately by the user after UAT. The off-ramps from Phase 0 (housekeeping / trivial / doc-only) don't enter Phase 1 — they run the **Lightweight path** (below), which the skill drives to merge.
49
49
 
50
+ **Branch targets are project-configured — never hardcode `main` / `develop`.** Read them once from `sdlc-config.json` and use them throughout:
51
+
52
+ ```bash
53
+ INTEGRATION_BRANCH=$(jq -r '.integration_branch // "develop"' sdlc-config.json) # where work lands + ci.yml uploads gate evidence
54
+ RELEASE_BRANCH=$(jq -r '.release_branch // "main"' sdlc-config.json) # the protected production branch
55
+ ```
56
+
57
+ For a **develop-first** repo these are `develop` and `main`: implementation lands on `$INTEGRATION_BRANCH`, and the UAT-approved release PR is `$INTEGRATION_BRANCH → $RELEASE_BRANCH`. A **trunk-only** repo sets both to `main`, collapsing the two hops into a single `feature → main` PR. Where the two branches differ, the release PR's head is `$INTEGRATION_BRANCH`; where they're equal, it's the feature branch.
58
+
50
59
  ### Phase 0 — Workflow triage (classify → announce → confirm → route)
51
60
 
52
61
  Runs **first**, before any `REQ-XXX` is assigned. It decides which of the six change-types in [`change-workflows.md`](https://github.com/metasession-dev/DevAudit-Installer/blob/main/docs/change-workflows.md) applies and what will — and won't — run. This is what stops every issue defaulting to maximum ceremony.
@@ -86,11 +95,11 @@ Only the **tracked** route continues into Phase 1; the others run the Lightweigh
86
95
 
87
96
  Reached from Phase 0 for non-tracked change-types. The skill drives this end-to-end; the only difference from the tracked cycle is the absence of *ceremony*, not the absence of *guidance*. It pauses only where a human is genuinely required (PR review, merge).
88
97
 
89
- 1. **Branch off `develop`** with a housekeeping prefix — `chore/…`, `docs/…`, `ci/…`, `build/…`, `test/…`, or `compliance/…` for a doc-only change against an existing REQ.
98
+ 1. **Branch off `$INTEGRATION_BRANCH`** with a housekeeping prefix — `chore/…`, `docs/…`, `ci/…`, `build/…`, `test/…`, or `compliance/…` for a doc-only change against an existing REQ.
90
99
  2. **Make the change**, single-purpose. If it turns out to touch runtime behaviour in `app/` / `lib/`, stop and reclassify as tracked — the commit-type rule is the backstop.
91
100
  3. **Run all gates locally** (`npm run lint`, `npx tsc --noEmit`, the test suite, `semgrep`, `npm audit` — or the stack-adapter equivalents). Trivial ≠ unverified; never `--no-verify`.
92
101
  4. **Commit** with a housekeeping type and **no** `REQ-XXX` — `docs:` / `chore:` / `ci:` / `build:` / `test:` / `revert:` are exempt from the `[REQ-XXX]` rule; a `compliance:` doc-only change references the existing REQ. `Co-Authored-By: Claude` if AI-assisted.
93
- 5. **Push and open the PR.** CI runs the same quality gates; `compliance-validation.yml` finds no `REQ-XXX` and skips artifact validation.
102
+ 5. **Push and open the PR** into `$INTEGRATION_BRANCH` (`gh pr create --base "$INTEGRATION_BRANCH" --head <branch>`). CI runs the same quality gates; `compliance-validation.yml` finds no `REQ-XXX` and skips artifact validation.
94
103
  6. **Report honest status** — wait for CI, name any failing check, fix and re-push. Never announce "ready" while a required check is red.
95
104
  7. **Guide review → merge.** A human still reviews the PR (separation of duties). There is **no** portal release approval, no UAT four-eyes, no Production gate, and no close-out. Merge once CI is green and the reviewer approves.
96
105
  8. **Done.** A housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`) with no approval gate; a doc-only push attaches its docs to the existing `REQ-XXX` release. No further action required — report completion and stop.
@@ -110,7 +119,7 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
110
119
 
111
120
  ### Phase 2 — Implement and test (SDLC stage 2)
112
121
 
113
- 1. **Branch off `develop`.** `git checkout -b feat/REQ-XXX-<slug>`. The slug is a kebab-case fragment of the issue title (max 6 words).
122
+ 1. **Branch off `$INTEGRATION_BRANCH`.** `git checkout "$INTEGRATION_BRANCH" && git pull && git checkout -b feat/REQ-XXX-<slug>`. The slug is a kebab-case fragment of the issue title (max 6 words).
114
123
  2. **Write failing tests first** per [`Test_Architecture.md`](../../Test_Architecture.md). Depth scales with risk class:
115
124
  - LOW — unit tests on the changed function(s); no e2e required unless the change touches a user-facing flow.
116
125
  - MEDIUM — unit + integration; e2e for any UI-facing change.
@@ -127,7 +136,9 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
127
136
  - `npm audit --audit-level=high` (or stack-adapter equivalent)
128
137
  6. **On gate failure**, iterate up to N=3 attempts. Each iteration: read the failure output, propose a fix, apply, re-run. On exhausted attempts, halt with the full failure output and surface to the human — never use `--no-verify`, `eslint-disable`, `@ts-expect-error`, `xfail`, or any other bypass.
129
138
  7. **Commit** using Conventional Commits with `Ref: REQ-XXX` trailer and `Co-Authored-By: Claude` trailer. One commit per logical step; never amend a commit that's already been pushed.
130
- 8. **Push** to the feature branch. No PR yet that happens in Phase 4.
139
+ 8. **Land the work on `$INTEGRATION_BRANCH`.** Push the feature branch, then:
140
+ - **If `$INTEGRATION_BRANCH` ≠ `$RELEASE_BRANCH`** (develop-first): open a PR `feat/REQ-XXX-<slug> → $INTEGRATION_BRANCH` and merge it once CI is green. This is the **integration hop** — there is no UAT four-eyes gate here (that's the release PR in Phase 4); for MEDIUM+ risk get a peer review on this PR per the project's norms. The push to `$INTEGRATION_BRANCH` is what triggers `ci.yml` to register the release and upload gate evidence.
141
+ - **If `$INTEGRATION_BRANCH` = `$RELEASE_BRANCH`** (trunk-only): do **not** merge to the protected branch here — leave the work on the feature branch; it becomes the release PR's head in Phase 4.
131
142
 
132
143
  ### Phase 3 — Compile evidence (SDLC stage 3)
133
144
 
@@ -156,7 +167,11 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
156
167
 
157
168
  ### Phase 4 — Submit for UAT review (SDLC stage 4)
158
169
 
159
- 1. **Open the PR.** `gh pr create --base main --head <branch>`. PR body per the SDLC PR template (see [`.github/pull_request_template.md`](../../../../../.github/pull_request_template.md)):
170
+ 1. **Open the release PR** the PR that carries the UAT four-eyes approval gate (`check-release-approval.yml`), always into `$RELEASE_BRANCH`:
171
+ - develop-first (`$INTEGRATION_BRANCH` ≠ `$RELEASE_BRANCH`): `gh pr create --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH"` (e.g. `develop → main`). The implementation already landed on `$INTEGRATION_BRANCH` in Phase 2; this promotes it. (Note: if other work is also waiting on `$INTEGRATION_BRANCH`, this is a bundled release — every in-scope REQ keeps its own release record and Production approval.)
172
+ - trunk-only (`$INTEGRATION_BRANCH` = `$RELEASE_BRANCH`): `gh pr create --base "$RELEASE_BRANCH" --head feat/REQ-XXX-<slug>` (the feature branch from Phase 2).
173
+
174
+ PR body per the SDLC PR template (see [`.github/pull_request_template.md`](../../../../../.github/pull_request_template.md)):
160
175
  - Closes #N
161
176
  - REQ-XXX
162
177
  - Risk: <class>
@@ -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
@@ -6,6 +6,10 @@
6
6
  "node_version": 20,
7
7
  "runner": "self-hosted",
8
8
 
9
+ "_comment_branches": "Branch model the sdlc-implementer skill follows. integration_branch = where implementation PRs land and ci.yml uploads gate evidence on push (develop-first default). release_branch = the protected production branch the integration branch is promoted to via the UAT-approved release PR. Trunk-only projects set both to \"main\".",
10
+ "integration_branch": "develop",
11
+ "release_branch": "main",
12
+
9
13
  "source_dirs": "app/ lib/ src/",
10
14
  "sast_baseline": 0,
11
15
  "accepted_dep_risks": "",