@metasession.co/devaudit-cli 0.1.28 → 0.1.29
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 +12 -0
- package/sdlc/files/_common/scripts/derive-release-version.sh +36 -0
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +83 -0
- package/sdlc/files/_common/scripts/validate-commits.sh +4 -1
- package/sdlc/files/ci/ci.yml.template +61 -3
- package/sdlc/files/ci/python/ci.yml.template +61 -4
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.29",
|
|
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.29",
|
|
37
37
|
"commander": "^12.1.0",
|
|
38
38
|
"consola": "^3.2.3",
|
|
39
39
|
"env-paths": "^3.0.0",
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
# build / test / compliance / revert) for
|
|
27
27
|
# the release row. Unknown values are
|
|
28
28
|
# silently dropped server-side.
|
|
29
|
+
# --gate-status <status> `passed` / `failed` / `skipped`. Lets the
|
|
30
|
+
# portal distinguish a gate that ran-and-
|
|
31
|
+
# failed from one that never ran. Forwarded
|
|
32
|
+
# as `gateStatus`; unknown values are
|
|
33
|
+
# silently dropped server-side.
|
|
34
|
+
# DevAudit-Installer#96.
|
|
29
35
|
#
|
|
30
36
|
# Required environment variables:
|
|
31
37
|
# DEVAUDIT_BASE_URL e.g. https://meta-comply-production.up.railway.app
|
|
@@ -65,6 +71,7 @@ ENVIRONMENT=""
|
|
|
65
71
|
EVIDENCE_CATEGORY=""
|
|
66
72
|
RELEASE_TITLE=""
|
|
67
73
|
CHANGE_TYPE=""
|
|
74
|
+
GATE_STATUS=""
|
|
68
75
|
|
|
69
76
|
while [ "$#" -gt 0 ]; do
|
|
70
77
|
case "$1" in
|
|
@@ -80,6 +87,10 @@ while [ "$#" -gt 0 ]; do
|
|
|
80
87
|
# unknown change-type values are dropped server-side, not 400'd.
|
|
81
88
|
--release-title) RELEASE_TITLE="$2"; shift 2 ;;
|
|
82
89
|
--change-type) CHANGE_TYPE="$2"; shift 2 ;;
|
|
90
|
+
# passed/failed/skipped — surfaces failed gates on the portal so
|
|
91
|
+
# ran-and-failed != never-ran. Unknown values dropped server-side.
|
|
92
|
+
# DevAudit-Installer#96.
|
|
93
|
+
--gate-status) GATE_STATUS="$2"; shift 2 ;;
|
|
83
94
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
84
95
|
esac
|
|
85
96
|
done
|
|
@@ -181,6 +192,7 @@ for FILE in "${FILES[@]}"; do
|
|
|
181
192
|
[ -n "$EVIDENCE_CATEGORY" ] && CURL_ARGS+=(-F "evidenceCategory=${EVIDENCE_CATEGORY}")
|
|
182
193
|
[ -n "$RELEASE_TITLE" ] && CURL_ARGS+=(-F "releaseTitle=${RELEASE_TITLE}")
|
|
183
194
|
[ -n "$CHANGE_TYPE" ] && CURL_ARGS+=(-F "changeType=${CHANGE_TYPE}")
|
|
195
|
+
[ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
|
|
184
196
|
|
|
185
197
|
ATTEMPT=1
|
|
186
198
|
BACKOFF=$INITIAL_BACKOFF_SECONDS
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
# 4. Pending release ticket on disk: exactly one
|
|
14
14
|
# compliance/pending-releases/RELEASE-TICKET-REQ-XXX.md
|
|
15
15
|
# -> REQ-XXX
|
|
16
|
+
# 4-bis. RTM.md IN PROGRESS row: exactly one tracked REQ marked
|
|
17
|
+
# IN PROGRESS in compliance/RTM.md
|
|
18
|
+
# -> REQ-XXX
|
|
16
19
|
# 5. Fallback: bare date -> v2026.05.17
|
|
17
20
|
#
|
|
18
21
|
# Step 4 (DevAudit-Installer#92) handles `chore:` / `docs:` / `ci:`
|
|
@@ -23,6 +26,12 @@
|
|
|
23
26
|
# bare date — when exactly one ticket is open, attribute to it.
|
|
24
27
|
# Multiple open tickets stays ambiguous → bare-date fallback.
|
|
25
28
|
#
|
|
29
|
+
# Step 4-bis (DevAudit-Installer#95) is the zero-ceremony equivalent:
|
|
30
|
+
# RTM.md is the file the operator already maintains as the source of
|
|
31
|
+
# truth for release state. When step 4 finds no ticket and exactly one
|
|
32
|
+
# RTM row is IN PROGRESS, attribute to it. RTM_PATH defaults to
|
|
33
|
+
# compliance/RTM.md and is overridable via env.
|
|
34
|
+
#
|
|
26
35
|
# The id is taken from a bracketed [REQ-XXX] tag (subject or body) or the
|
|
27
36
|
# `Ref:` line — NOT from unbracketed prose (e.g. "target close: REQ-002" must
|
|
28
37
|
# not win over "Ref: REQ-001"). Step 3 exists because a "Merge pull request"
|
|
@@ -83,5 +92,32 @@ if [ -d compliance/pending-releases ]; then
|
|
|
83
92
|
fi
|
|
84
93
|
fi
|
|
85
94
|
|
|
95
|
+
# 4-bis. RTM.md IN PROGRESS row: when exactly one REQ row in
|
|
96
|
+
# compliance/RTM.md (or $RTM_PATH) is marked IN PROGRESS, attribute the
|
|
97
|
+
# in-flight release to it. Reads the file the operator already
|
|
98
|
+
# maintains so chore/docs/ci sync commits don't need a manually-dropped
|
|
99
|
+
# pending-tickets file. Same exactly-one guard as step 4 — zero or
|
|
100
|
+
# multiple IN PROGRESS rows → ambiguous, fall through.
|
|
101
|
+
# DevAudit-Installer#95.
|
|
102
|
+
RTM_PATH="${RTM_PATH:-compliance/RTM.md}"
|
|
103
|
+
if [ -f "$RTM_PATH" ]; then
|
|
104
|
+
# Match REQ rows whose status column starts with `IN PROGRESS`.
|
|
105
|
+
# `\|[[:space:]]+IN PROGRESS` requires a pipe followed by whitespace,
|
|
106
|
+
# so legend rows (`| \`IN PROGRESS\``) and prose mentions don't match.
|
|
107
|
+
# Variable padding between REQ-ID and Status (Issue/Risk/Evidence
|
|
108
|
+
# columns) is fine — only the leading REQ-XXX and the status-cell
|
|
109
|
+
# marker matter.
|
|
110
|
+
IN_PROGRESS_REQS=$(grep -E '\|[[:space:]]+IN PROGRESS' "$RTM_PATH" 2>/dev/null \
|
|
111
|
+
| grep -oE '^\|[[:space:]]*REQ-[0-9]+' \
|
|
112
|
+
| grep -oE 'REQ-[0-9]+' | sort -u || true)
|
|
113
|
+
if [ -n "$IN_PROGRESS_REQS" ]; then
|
|
114
|
+
IN_PROGRESS_COUNT=$(echo "$IN_PROGRESS_REQS" | grep -c .)
|
|
115
|
+
if [ "$IN_PROGRESS_COUNT" = "1" ]; then
|
|
116
|
+
echo "$IN_PROGRESS_REQS"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
|
|
86
122
|
# 5. Fallback: bare date in UTC
|
|
87
123
|
echo "v$(date -u +%Y.%m.%d)"
|
|
@@ -154,6 +154,89 @@ cat > compliance/pending-releases/RELEASE-TICKET-REQ-051.md <<'TICKET'
|
|
|
154
154
|
TICKET
|
|
155
155
|
assert_eq "subject [REQ-099] beats pending REQ-051 -> REQ-099" "REQ-099" "$(run_helper)"
|
|
156
156
|
|
|
157
|
+
# Case 13 (DevAudit-Installer#95): step-4-bis. No subject/body tag,
|
|
158
|
+
# no pending ticket, but RTM.md has exactly one IN PROGRESS row.
|
|
159
|
+
# Attribute to that REQ. Tests the zero-ceremony fallback that
|
|
160
|
+
# survives chore/docs/ci sync commits when no operator state file
|
|
161
|
+
# has been dropped.
|
|
162
|
+
make_fixture "$WORK/c13" "chore: devaudit update to 0.1.29"
|
|
163
|
+
mkdir -p compliance
|
|
164
|
+
cat > compliance/RTM.md <<'RTM'
|
|
165
|
+
# RTM
|
|
166
|
+
| REQ-ID | Issue | Risk | Evidence | Status | Approver | Date |
|
|
167
|
+
| ------- | ----- | ---- | ---------------------------- | ------------------- | -------- | ---------- |
|
|
168
|
+
| REQ-100 | #10 | LOW | compliance/evidence/REQ-100/ | APPROVED - DEPLOYED | dev | 2026-05-30 |
|
|
169
|
+
| REQ-101 | #11 | MED | compliance/evidence/REQ-101/ | IN PROGRESS | dev | 2026-06-01 |
|
|
170
|
+
RTM
|
|
171
|
+
assert_eq "RTM single IN PROGRESS row -> REQ-101" "REQ-101" "$(run_helper)"
|
|
172
|
+
|
|
173
|
+
# Case 14: step-4-bis ambiguity guard. Two IN PROGRESS rows → falls
|
|
174
|
+
# through to the bare date rather than guessing.
|
|
175
|
+
make_fixture "$WORK/c14" "chore: devaudit update to 0.1.29"
|
|
176
|
+
mkdir -p compliance
|
|
177
|
+
cat > compliance/RTM.md <<'RTM'
|
|
178
|
+
| REQ-ID | Status |
|
|
179
|
+
| ------- | -------------- |
|
|
180
|
+
| REQ-101 | IN PROGRESS |
|
|
181
|
+
| REQ-102 | IN PROGRESS |
|
|
182
|
+
RTM
|
|
183
|
+
assert_eq "RTM two IN PROGRESS rows -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
184
|
+
|
|
185
|
+
# Case 15: step-4-bis must ignore legend rows that mention IN PROGRESS
|
|
186
|
+
# inside backticks (the wawagardenbar-app RTM convention) and prose
|
|
187
|
+
# mentions in description columns.
|
|
188
|
+
make_fixture "$WORK/c15" "chore: devaudit update to 0.1.29"
|
|
189
|
+
mkdir -p compliance
|
|
190
|
+
cat > compliance/RTM.md <<'RTM'
|
|
191
|
+
# RTM
|
|
192
|
+
## Conventions
|
|
193
|
+
| Value | Meaning |
|
|
194
|
+
| ------------------ | ----------------------------- |
|
|
195
|
+
| `IN PROGRESS` | Active development underway |
|
|
196
|
+
|
|
197
|
+
| REQ-ID | Status |
|
|
198
|
+
| ------- | ----------------------------------------------------------------- |
|
|
199
|
+
| REQ-200 | RELEASED (was IN PROGRESS during Q3, then deployed) |
|
|
200
|
+
RTM
|
|
201
|
+
assert_eq "RTM legend + prose mentions -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
202
|
+
|
|
203
|
+
# Case 16: step-4-bis with the real META-JOBS-shaped RTM row (long
|
|
204
|
+
# parenthetical commentary in the status cell). The status cell still
|
|
205
|
+
# starts with `IN PROGRESS` after the pipe.
|
|
206
|
+
make_fixture "$WORK/c16" "chore: devaudit update to 0.1.29"
|
|
207
|
+
mkdir -p compliance
|
|
208
|
+
cat > compliance/RTM.md <<'RTM'
|
|
209
|
+
| REQ-ID | Issue | Risk | Evidence | Status | Approver | Date |
|
|
210
|
+
| ------- | ----- | ----------- | ---------------------------- | --------------------------------------------------------------------- | ---------- | ---------- |
|
|
211
|
+
| REQ-056 | #117 | MEDIUM-HIGH | compliance/evidence/REQ-056/ | IN PROGRESS (WhatsApp inbound-message router; many details follow...) | ostendo-io | 2026-06-01 |
|
|
212
|
+
RTM
|
|
213
|
+
assert_eq "RTM long parenthetical status -> REQ-056" "REQ-056" "$(run_helper)"
|
|
214
|
+
|
|
215
|
+
# Case 17: step-4-bis must NOT win over a pending ticket on disk.
|
|
216
|
+
# Step 4 returns first.
|
|
217
|
+
make_fixture "$WORK/c17" "chore: devaudit update to 0.1.29"
|
|
218
|
+
mkdir -p compliance/pending-releases compliance
|
|
219
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-301.md <<'TICKET'
|
|
220
|
+
# Release Ticket: REQ-301
|
|
221
|
+
TICKET
|
|
222
|
+
cat > compliance/RTM.md <<'RTM'
|
|
223
|
+
| REQ-ID | Status |
|
|
224
|
+
| ------- | ----------- |
|
|
225
|
+
| REQ-302 | IN PROGRESS |
|
|
226
|
+
RTM
|
|
227
|
+
assert_eq "pending ticket REQ-301 beats RTM IN PROGRESS REQ-302" "REQ-301" "$(run_helper)"
|
|
228
|
+
|
|
229
|
+
# Case 18: step-4-bis respects RTM_PATH env override.
|
|
230
|
+
make_fixture "$WORK/c18" "chore: devaudit update to 0.1.29"
|
|
231
|
+
mkdir -p docs
|
|
232
|
+
cat > docs/custom-RTM.md <<'RTM'
|
|
233
|
+
| REQ-ID | Status |
|
|
234
|
+
| ------- | ----------- |
|
|
235
|
+
| REQ-400 | IN PROGRESS |
|
|
236
|
+
RTM
|
|
237
|
+
GOT=$(RTM_PATH=docs/custom-RTM.md run_helper)
|
|
238
|
+
assert_eq "RTM_PATH=docs/custom-RTM.md -> REQ-400" "REQ-400" "$GOT"
|
|
239
|
+
|
|
157
240
|
echo ""
|
|
158
241
|
echo "=== Summary: $PASS pass / $FAIL fail ==="
|
|
159
242
|
|
|
@@ -22,7 +22,10 @@ echo "Comparing: $BASE_BRANCH...HEAD"
|
|
|
22
22
|
echo ""
|
|
23
23
|
|
|
24
24
|
# Conventional Commit regex: type(optional-scope): description
|
|
25
|
-
|
|
25
|
+
# Scope accepts anything except `)` so multi-scope subjects like
|
|
26
|
+
# `feat(auth,profile):` and `fix(rewards/expiry):` validate. The closing-paren
|
|
27
|
+
# guard prevents pathological inputs. DevAudit-Installer#93.
|
|
28
|
+
CC_REGEX='^(feat|fix|docs|test|refactor|chore|compliance|security|perf|ci|build|revert)(\([^)]+\))?!?: .+'
|
|
26
29
|
|
|
27
30
|
COMMITS=$(git log "$BASE_BRANCH"..HEAD --format='%H' || true)
|
|
28
31
|
|
|
@@ -76,11 +76,13 @@ jobs:
|
|
|
76
76
|
# ── Gate 1: TypeScript ──
|
|
77
77
|
|
|
78
78
|
- name: TypeScript Check
|
|
79
|
+
id: typescript
|
|
79
80
|
run: npx tsc --noEmit
|
|
80
81
|
|
|
81
82
|
# ── Gate 2: SAST (Semgrep) ──
|
|
82
83
|
|
|
83
84
|
- name: SAST Scan
|
|
85
|
+
id: sast
|
|
84
86
|
run: |
|
|
85
87
|
# --output writes the JSON report to the file directly; stderr
|
|
86
88
|
# (progress/metrics/version notices) goes to /dev/null. Using
|
|
@@ -106,6 +108,7 @@ jobs:
|
|
|
106
108
|
# ── Gate 3: Dependency Audit ──
|
|
107
109
|
|
|
108
110
|
- name: Dependency Audit
|
|
111
|
+
id: dep-audit
|
|
109
112
|
run: |
|
|
110
113
|
# stderr → /dev/null so warnings can't corrupt the JSON (DevAudit #48)
|
|
111
114
|
npm audit --json > dependency-audit.json 2>/dev/null || true
|
|
@@ -144,10 +147,34 @@ jobs:
|
|
|
144
147
|
# ── Gate 5: Build ──
|
|
145
148
|
|
|
146
149
|
- name: Build Check
|
|
150
|
+
id: build
|
|
147
151
|
run: npm run build
|
|
148
152
|
env:
|
|
149
153
|
{{BUILD_ENV}}
|
|
150
154
|
|
|
155
|
+
# ── Summarise per-gate outcomes ──
|
|
156
|
+
#
|
|
157
|
+
# Runs unconditionally so a failed earlier gate doesn't strand the
|
|
158
|
+
# outcome of the gates that did run. Step outcomes are one of
|
|
159
|
+
# `success` / `failure` / `skipped` / `cancelled`. The upload-evidence
|
|
160
|
+
# job maps these to `passed` / `failed` / `skipped` and sends the
|
|
161
|
+
# result as `gateStatus=` on each per-gate upload, so the portal can
|
|
162
|
+
# render a failed gate as failed (with the run artefact) rather than
|
|
163
|
+
# showing it as missing. DevAudit-Installer#96.
|
|
164
|
+
|
|
165
|
+
- name: Summarise gate outcomes
|
|
166
|
+
if: always()
|
|
167
|
+
run: |
|
|
168
|
+
cat > gate-outcomes.json <<EOF
|
|
169
|
+
{
|
|
170
|
+
"typescript": "${{ steps.typescript.outcome }}",
|
|
171
|
+
"sast": "${{ steps.sast.outcome }}",
|
|
172
|
+
"dependency_audit": "${{ steps.dep-audit.outcome }}",
|
|
173
|
+
"build": "${{ steps.build.outcome }}"
|
|
174
|
+
}
|
|
175
|
+
EOF
|
|
176
|
+
cat gate-outcomes.json
|
|
177
|
+
|
|
151
178
|
# ── Upload artifacts ──
|
|
152
179
|
|
|
153
180
|
- uses: actions/upload-artifact@v4
|
|
@@ -162,6 +189,7 @@ jobs:
|
|
|
162
189
|
e2e-auth-results.json
|
|
163
190
|
playwright-report/
|
|
164
191
|
coverage/coverage-summary.json
|
|
192
|
+
gate-outcomes.json
|
|
165
193
|
compliance/evidence/*/screenshots/*.png
|
|
166
194
|
retention-days: 90
|
|
167
195
|
|
|
@@ -278,7 +306,11 @@ jobs:
|
|
|
278
306
|
name: Upload Evidence
|
|
279
307
|
runs-on: {{RUNNER}}
|
|
280
308
|
needs: [quality-gates, register-release]
|
|
281
|
-
|
|
309
|
+
# `always()` instead of `!failure()` so failed gates still upload their
|
|
310
|
+
# evidence — `status=failed` is itself the audit trail. `!cancelled()`
|
|
311
|
+
# still guards against partial state on operator-cancel.
|
|
312
|
+
# DevAudit-Installer#96.
|
|
313
|
+
if: ${{ always() && !cancelled() && vars.DEVAUDIT_BASE_URL != '' && needs.register-release.result == 'success' }}
|
|
282
314
|
env:
|
|
283
315
|
DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
|
|
284
316
|
DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
|
|
@@ -315,6 +347,32 @@ jobs:
|
|
|
315
347
|
fi
|
|
316
348
|
}
|
|
317
349
|
|
|
350
|
+
# Map step outcomes from gate-outcomes.json (written by the
|
|
351
|
+
# `Summarise gate outcomes` step) to gateStatus values the
|
|
352
|
+
# uploader forwards to the portal. Failed gates upload as
|
|
353
|
+
# gateStatus=failed so the portal can distinguish a gate that
|
|
354
|
+
# ran-and-failed from one that never ran. DevAudit-Installer#96.
|
|
355
|
+
gate_status() {
|
|
356
|
+
case "$1" in
|
|
357
|
+
success) echo passed ;;
|
|
358
|
+
failure) echo failed ;;
|
|
359
|
+
*) echo skipped ;;
|
|
360
|
+
esac
|
|
361
|
+
}
|
|
362
|
+
STATUS_TYPESCRIPT=skipped
|
|
363
|
+
STATUS_SAST=skipped
|
|
364
|
+
STATUS_DEPAUDIT=skipped
|
|
365
|
+
STATUS_BUILD=skipped
|
|
366
|
+
if [ -f ci-evidence/gate-outcomes.json ]; then
|
|
367
|
+
STATUS_TYPESCRIPT=$(gate_status "$(jq -r '.typescript // "skipped"' ci-evidence/gate-outcomes.json)")
|
|
368
|
+
STATUS_SAST=$(gate_status "$(jq -r '.sast // "skipped"' ci-evidence/gate-outcomes.json)")
|
|
369
|
+
STATUS_DEPAUDIT=$(gate_status "$(jq -r '.dependency_audit // "skipped"' ci-evidence/gate-outcomes.json)")
|
|
370
|
+
STATUS_BUILD=$(gate_status "$(jq -r '.build // "skipped"' ci-evidence/gate-outcomes.json)")
|
|
371
|
+
upload gate-outcomes.json \
|
|
372
|
+
{{PROJECT_SLUG}} _compliance-docs compliance_document ci-evidence/gate-outcomes.json \
|
|
373
|
+
--category ci_pipeline ${FLAGS}
|
|
374
|
+
fi
|
|
375
|
+
|
|
318
376
|
# Re-generate gate evidence as fallback if artifact download failed
|
|
319
377
|
mkdir -p ci-evidence
|
|
320
378
|
if [ ! -f ci-evidence/sast-results.json ]; then
|
|
@@ -337,7 +395,7 @@ jobs:
|
|
|
337
395
|
if [ -f ci-evidence/sast-results.json ]; then
|
|
338
396
|
upload sast-results.json \
|
|
339
397
|
{{PROJECT_SLUG}} _compliance-docs sast_report ci-evidence/sast-results.json \
|
|
340
|
-
--category security_scan ${FLAGS}
|
|
398
|
+
--category security_scan --gate-status "$STATUS_SAST" ${FLAGS}
|
|
341
399
|
fi
|
|
342
400
|
|
|
343
401
|
# Upload dependency audit — precise evidence_type=dependency_audit
|
|
@@ -345,7 +403,7 @@ jobs:
|
|
|
345
403
|
if [ -f ci-evidence/dependency-audit.json ]; then
|
|
346
404
|
upload dependency-audit.json \
|
|
347
405
|
{{PROJECT_SLUG}} _compliance-docs dependency_audit ci-evidence/dependency-audit.json \
|
|
348
|
-
--category security_scan ${FLAGS}
|
|
406
|
+
--category security_scan --gate-status "$STATUS_DEPAUDIT" ${FLAGS}
|
|
349
407
|
fi
|
|
350
408
|
|
|
351
409
|
# Upload E2E test results (ci_pipeline category)
|
|
@@ -61,19 +61,23 @@ jobs:
|
|
|
61
61
|
# ── Gate 1: Lint + format (ruff) ──
|
|
62
62
|
|
|
63
63
|
- name: Ruff lint
|
|
64
|
+
id: ruff-lint
|
|
64
65
|
run: ruff check {{SOURCE_DIRS}}
|
|
65
66
|
|
|
66
67
|
- name: Ruff format check
|
|
68
|
+
id: ruff-format
|
|
67
69
|
run: ruff format --check {{SOURCE_DIRS}}
|
|
68
70
|
|
|
69
71
|
# ── Gate 2: Type check (mypy --strict) ──
|
|
70
72
|
|
|
71
73
|
- name: Type Check (mypy)
|
|
74
|
+
id: mypy
|
|
72
75
|
run: mypy {{SOURCE_DIRS}}
|
|
73
76
|
|
|
74
77
|
# ── Gate 3: SAST (Semgrep) ──
|
|
75
78
|
|
|
76
79
|
- name: SAST Scan
|
|
80
|
+
id: sast
|
|
77
81
|
run: |
|
|
78
82
|
mkdir -p ci-evidence
|
|
79
83
|
semgrep scan --config auto {{SOURCE_DIRS}} \
|
|
@@ -95,6 +99,7 @@ jobs:
|
|
|
95
99
|
# ── Gate 4: Dependency Audit (pip-audit) ──
|
|
96
100
|
|
|
97
101
|
- name: Dependency Audit
|
|
102
|
+
id: dep-audit
|
|
98
103
|
run: |
|
|
99
104
|
mkdir -p ci-evidence
|
|
100
105
|
pip-audit --format=json > ci-evidence/dependency-audit.json 2>&1 || true
|
|
@@ -124,6 +129,7 @@ jobs:
|
|
|
124
129
|
{{DATABASE_URI_STEP}}
|
|
125
130
|
|
|
126
131
|
- name: Tests
|
|
132
|
+
id: tests
|
|
127
133
|
run: |
|
|
128
134
|
mkdir -p ci-evidence
|
|
129
135
|
pytest --junit-xml=ci-evidence/junit.xml --tb=short
|
|
@@ -131,10 +137,31 @@ jobs:
|
|
|
131
137
|
# ── Gate 6: Build ──
|
|
132
138
|
|
|
133
139
|
- name: Build Check
|
|
140
|
+
id: build
|
|
134
141
|
run: python -m build --sdist --wheel
|
|
135
142
|
env:
|
|
136
143
|
{{BUILD_ENV}}
|
|
137
144
|
|
|
145
|
+
# ── Summarise per-gate outcomes ──
|
|
146
|
+
# Runs unconditionally so a failed earlier gate doesn't strand the
|
|
147
|
+
# outcome of the gates that did run. DevAudit-Installer#96.
|
|
148
|
+
- name: Summarise gate outcomes
|
|
149
|
+
if: always()
|
|
150
|
+
run: |
|
|
151
|
+
mkdir -p ci-evidence
|
|
152
|
+
cat > ci-evidence/gate-outcomes.json <<EOF
|
|
153
|
+
{
|
|
154
|
+
"ruff_lint": "${{ steps.ruff-lint.outcome }}",
|
|
155
|
+
"ruff_format": "${{ steps.ruff-format.outcome }}",
|
|
156
|
+
"mypy": "${{ steps.mypy.outcome }}",
|
|
157
|
+
"sast": "${{ steps.sast.outcome }}",
|
|
158
|
+
"dependency_audit": "${{ steps.dep-audit.outcome }}",
|
|
159
|
+
"tests": "${{ steps.tests.outcome }}",
|
|
160
|
+
"build": "${{ steps.build.outcome }}"
|
|
161
|
+
}
|
|
162
|
+
EOF
|
|
163
|
+
cat ci-evidence/gate-outcomes.json
|
|
164
|
+
|
|
138
165
|
# ── Upload artifacts ──
|
|
139
166
|
|
|
140
167
|
# actions/upload-artifact@v4 doesn't honour the job's `working-directory`;
|
|
@@ -150,6 +177,7 @@ jobs:
|
|
|
150
177
|
{{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json
|
|
151
178
|
{{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json
|
|
152
179
|
{{WORKING_DIR_PREFIX}}ci-evidence/junit.xml
|
|
180
|
+
{{WORKING_DIR_PREFIX}}ci-evidence/gate-outcomes.json
|
|
153
181
|
{{WORKING_DIR_PREFIX}}dist/
|
|
154
182
|
retention-days: 90
|
|
155
183
|
|
|
@@ -256,7 +284,11 @@ jobs:
|
|
|
256
284
|
name: Upload Evidence
|
|
257
285
|
runs-on: {{RUNNER}}
|
|
258
286
|
needs: [quality-gates, register-release]
|
|
259
|
-
|
|
287
|
+
# `always()` instead of `!failure()` so failed gates still upload their
|
|
288
|
+
# evidence — `status=failed` is itself the audit trail. `!cancelled()`
|
|
289
|
+
# still guards against partial state on operator-cancel.
|
|
290
|
+
# DevAudit-Installer#96.
|
|
291
|
+
if: ${{ always() && !cancelled() && vars.DEVAUDIT_BASE_URL != '' && needs.register-release.result == 'success' }}
|
|
260
292
|
env:
|
|
261
293
|
DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
|
|
262
294
|
DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
|
|
@@ -293,25 +325,50 @@ jobs:
|
|
|
293
325
|
fi
|
|
294
326
|
}
|
|
295
327
|
|
|
328
|
+
# Map step outcomes from gate-outcomes.json (written by the
|
|
329
|
+
# `Summarise gate outcomes` step) to gateStatus values the
|
|
330
|
+
# uploader forwards to the portal. Failed gates upload as
|
|
331
|
+
# gateStatus=failed so the portal can distinguish a gate that
|
|
332
|
+
# ran-and-failed from one that never ran. DevAudit-Installer#96.
|
|
333
|
+
gate_status() {
|
|
334
|
+
case "$1" in
|
|
335
|
+
success) echo passed ;;
|
|
336
|
+
failure) echo failed ;;
|
|
337
|
+
*) echo skipped ;;
|
|
338
|
+
esac
|
|
339
|
+
}
|
|
340
|
+
STATUS_SAST=skipped
|
|
341
|
+
STATUS_DEPAUDIT=skipped
|
|
342
|
+
STATUS_TESTS=skipped
|
|
343
|
+
OUTCOMES_FILE="{{WORKING_DIR_PREFIX}}ci-evidence/gate-outcomes.json"
|
|
344
|
+
if [ -f "$OUTCOMES_FILE" ]; then
|
|
345
|
+
STATUS_SAST=$(gate_status "$(jq -r '.sast // "skipped"' "$OUTCOMES_FILE")")
|
|
346
|
+
STATUS_DEPAUDIT=$(gate_status "$(jq -r '.dependency_audit // "skipped"' "$OUTCOMES_FILE")")
|
|
347
|
+
STATUS_TESTS=$(gate_status "$(jq -r '.tests // "skipped"' "$OUTCOMES_FILE")")
|
|
348
|
+
upload gate-outcomes.json \
|
|
349
|
+
{{PROJECT_SLUG}} _compliance-docs compliance_document "$OUTCOMES_FILE" \
|
|
350
|
+
--category ci_pipeline ${FLAGS}
|
|
351
|
+
fi
|
|
352
|
+
|
|
296
353
|
mkdir -p {{WORKING_DIR_PREFIX}}ci-evidence
|
|
297
354
|
|
|
298
355
|
if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json ]; then
|
|
299
356
|
upload sast-results.json \
|
|
300
357
|
{{PROJECT_SLUG}} _compliance-docs audit_log {{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json \
|
|
301
|
-
--category security_scan ${FLAGS}
|
|
358
|
+
--category security_scan --gate-status "$STATUS_SAST" ${FLAGS}
|
|
302
359
|
fi
|
|
303
360
|
|
|
304
361
|
if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json ]; then
|
|
305
362
|
upload dependency-audit.json \
|
|
306
363
|
{{PROJECT_SLUG}} _compliance-docs audit_log {{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json \
|
|
307
|
-
--category security_scan ${FLAGS}
|
|
364
|
+
--category security_scan --gate-status "$STATUS_DEPAUDIT" ${FLAGS}
|
|
308
365
|
fi
|
|
309
366
|
|
|
310
367
|
# pytest junit.xml is the Python equivalent of e2e-results.json — same `e2e_result` evidence type
|
|
311
368
|
if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/junit.xml ]; then
|
|
312
369
|
upload junit.xml \
|
|
313
370
|
{{PROJECT_SLUG}} _compliance-docs e2e_result {{WORKING_DIR_PREFIX}}ci-evidence/junit.xml \
|
|
314
|
-
--category ci_pipeline ${FLAGS}
|
|
371
|
+
--category ci_pipeline --gate-status "$STATUS_TESTS" ${FLAGS}
|
|
315
372
|
fi
|
|
316
373
|
|
|
317
374
|
if [ -f "compliance/test-summary-report.md" ]; then
|