@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/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +18 -0
- package/sdlc/files/_common/4-submit-for-review.md +2 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +20 -5
- 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/sdlc/files/sdlc-config.example.json +4 -0
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.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.
|
|
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
|
|
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
|
|
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
|
|
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. **
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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": "",
|