@metasession.co/devaudit-cli 0.1.69 → 0.1.70
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 +11 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +10 -1
- package/sdlc/CLAUDE.md +25 -2
- package/sdlc/SKILLS.md +1 -1
- package/sdlc/ai-rules/INSTRUCTIONS-SDLC.md +21 -5
- package/sdlc/files/_common/3-compile-evidence.md +63 -2
- package/sdlc/files/_common/4-submit-for-review.md +2 -1
- package/sdlc/files/_common/5-deploy-main.md +1 -0
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +1 -1
- package/sdlc/files/_common/Test_Strategy.md +1 -1
- package/sdlc/files/_common/governance/nil-incident-report.md.template +38 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +13 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +12 -2
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +6 -1
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +137 -25
- package/sdlc/files/_common/skills/sdlc-implementer/references/call-graph.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +27 -2
- package/sdlc/files/ci/ci.yml.template +83 -13
- package/sdlc/files/ci/compliance-evidence.yml.template +353 -53
- package/sdlc/files/ci/feature-e2e.yml.template +3 -2
- package/sdlc/files/ci/incident-export.yml.template +306 -8
- package/sdlc/files/ci/label-retention.yml.template +144 -0
- package/sdlc/files/ci/post-deploy-prod.yml.template +80 -3
- package/sdlc/files/ci/python/ci.yml.template +60 -16
- package/sdlc/files/sdlc-config.example.json +1 -1
- package/sdlc/.claude/settings.local.json +0 -11
|
@@ -9,15 +9,23 @@
|
|
|
9
9
|
# all want incident records as portal-resident evidence — not lost in
|
|
10
10
|
# GitHub issue threads.
|
|
11
11
|
# - When an issue labelled `incident` is closed, this workflow exports its
|
|
12
|
-
# title + body + comments + timeline into a structured markdown report
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
12
|
+
# title + body + comments + timeline into a structured markdown report.
|
|
13
|
+
#
|
|
14
|
+
# Routing (DevAudit-Installer#200 Fix 1):
|
|
15
|
+
# - Path A (baseline-only): If the issue's `### Framework attribution`
|
|
16
|
+
# section ticks ONLY `ISO29119.3.5.4` (no GDPR/SOC2/EUAIA), the report
|
|
17
|
+
# is direct-committed to develop — no PR, no REPLACE markers. The next
|
|
18
|
+
# compliance-evidence.yml run uploads it as `incident_report` evidence.
|
|
19
|
+
# - Path B (GDPR/SOC2/AI Act): If any of SOC2.CC7.2, GDPR.Art-33,
|
|
20
|
+
# GDPR.Art-34, or EUAIA.* are ticked, a PR is opened with REPLACE
|
|
21
|
+
# markers for GDPR triage + sign-off (current behaviour preserved).
|
|
22
|
+
# - Missing `### Framework attribution` section defaults to Path B
|
|
23
|
+
# (conservative — human review required).
|
|
16
24
|
#
|
|
17
25
|
# Operator setup (one-time):
|
|
18
26
|
# gh label create incident --color 'B60205' --description 'Operational or test incident; close to auto-archive as portal evidence'
|
|
19
27
|
#
|
|
20
|
-
# DevAudit-Installer#98 WS4.
|
|
28
|
+
# DevAudit-Installer#98 WS4, #200 Fix 1.
|
|
21
29
|
|
|
22
30
|
name: Incident Export
|
|
23
31
|
|
|
@@ -43,6 +51,77 @@ jobs:
|
|
|
43
51
|
fetch-depth: 0
|
|
44
52
|
token: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
|
|
45
53
|
|
|
54
|
+
- name: Classify routing (Path A vs Path B)
|
|
55
|
+
id: route
|
|
56
|
+
env:
|
|
57
|
+
ISSUE_BODY: ${{ github.event.issue.body }}
|
|
58
|
+
ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
|
|
59
|
+
run: |
|
|
60
|
+
# DevAudit-Installer#200 Fix 1 — Parse the `### Framework attribution`
|
|
61
|
+
# section from the issue body to determine routing.
|
|
62
|
+
#
|
|
63
|
+
# Path A (baseline-only): only ISO29119.3.5.4 is ticked → direct
|
|
64
|
+
# commit to develop, no PR, pre-filled GDPR triage as N/A.
|
|
65
|
+
#
|
|
66
|
+
# Path B (GDPR/SOC2/AI Act): any of SOC2.CC7.2, GDPR.Art-33,
|
|
67
|
+
# GDPR.Art-34, or EUAIA.* are ticked → open PR with REPLACE markers
|
|
68
|
+
# (current behaviour preserved).
|
|
69
|
+
#
|
|
70
|
+
# Missing `### Framework attribution` section → default to Path B
|
|
71
|
+
# (conservative — human review required).
|
|
72
|
+
|
|
73
|
+
# Extract the Framework attribution section from the issue body.
|
|
74
|
+
# Look for `- [x]` lines after the `### Framework attribution` header.
|
|
75
|
+
ATTRIB_SECTION=$(printf '%s' "$ISSUE_BODY" | \
|
|
76
|
+
sed -n '/^### *Framework attribution/,/^### /p' | \
|
|
77
|
+
grep -oP '\[x\] *`?\K[^`]+(?=`)?' || true)
|
|
78
|
+
|
|
79
|
+
# Check which frameworks are ticked.
|
|
80
|
+
HAS_ISO_ONLY=false
|
|
81
|
+
HAS_GDPR=false
|
|
82
|
+
HAS_SOC2=false
|
|
83
|
+
HAS_EUAIA=false
|
|
84
|
+
|
|
85
|
+
if printf '%s' "$ATTRIB_SECTION" | grep -qi 'ISO29119.3.5.4'; then
|
|
86
|
+
HAS_ISO_ONLY=true
|
|
87
|
+
fi
|
|
88
|
+
if printf '%s' "$ATTRIB_SECTION" | grep -qi 'SOC2.CC7.2'; then
|
|
89
|
+
HAS_SOC2=true
|
|
90
|
+
fi
|
|
91
|
+
if printf '%s' "$ATTRIB_SECTION" | grep -qi 'GDPR.Art-33\|GDPR.Art-34'; then
|
|
92
|
+
HAS_GDPR=true
|
|
93
|
+
fi
|
|
94
|
+
if printf '%s' "$ATTRIB_SECTION" | grep -qi 'EUAIA'; then
|
|
95
|
+
HAS_EUAIA=true
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# If no attribution section found at all, default to Path B.
|
|
99
|
+
if [ -z "$ATTRIB_SECTION" ]; then
|
|
100
|
+
echo "No Framework attribution section found — defaulting to Path B (PR with REPLACE markers)"
|
|
101
|
+
echo "path=B" >> "$GITHUB_OUTPUT"
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Path A: only ISO29119.3.5.4 ticked, no GDPR/SOC2/EUAIA.
|
|
106
|
+
if [ "$HAS_ISO_ONLY" = true ] && [ "$HAS_SOC2" = false ] && [ "$HAS_GDPR" = false ] && [ "$HAS_EUAIA" = false ]; then
|
|
107
|
+
echo "Path A: baseline-only (ISO29119.3.5.4) — direct commit to develop"
|
|
108
|
+
echo "path=A" >> "$GITHUB_OUTPUT"
|
|
109
|
+
else
|
|
110
|
+
echo "Path B: GDPR/SOC2/EUAIA attribution — PR with REPLACE markers"
|
|
111
|
+
echo "path=B" >> "$GITHUB_OUTPUT"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Derive severity from issue labels if available.
|
|
115
|
+
SEVERITY="low"
|
|
116
|
+
if printf '%s' "$ISSUE_LABELS" | grep -qi 'severity:critical\|critical'; then
|
|
117
|
+
SEVERITY="critical"
|
|
118
|
+
elif printf '%s' "$ISSUE_LABELS" | grep -qi 'severity:high\|high'; then
|
|
119
|
+
SEVERITY="high"
|
|
120
|
+
elif printf '%s' "$ISSUE_LABELS" | grep -qi 'severity:medium\|medium'; then
|
|
121
|
+
SEVERITY="medium"
|
|
122
|
+
fi
|
|
123
|
+
echo "severity=${SEVERITY}" >> "$GITHUB_OUTPUT"
|
|
124
|
+
|
|
46
125
|
- name: Compute paths
|
|
47
126
|
id: paths
|
|
48
127
|
env:
|
|
@@ -53,11 +132,211 @@ jobs:
|
|
|
53
132
|
echo "out=${OUT}" >> "$GITHUB_OUTPUT"
|
|
54
133
|
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
|
55
134
|
|
|
56
|
-
- name: Export issue to markdown
|
|
135
|
+
- name: Export issue to markdown (Path A — baseline-only)
|
|
136
|
+
if: steps.route.outputs.path == 'A'
|
|
57
137
|
env:
|
|
58
138
|
GH_TOKEN: ${{ github.token }}
|
|
59
139
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
60
140
|
OUT: ${{ steps.paths.outputs.out }}
|
|
141
|
+
SEVERITY: ${{ steps.route.outputs.severity }}
|
|
142
|
+
ISSUE_BODY: ${{ github.event.issue.body }}
|
|
143
|
+
ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
|
|
144
|
+
run: |
|
|
145
|
+
mkdir -p compliance/governance
|
|
146
|
+
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \
|
|
147
|
+
--json title,body,labels,author,createdAt,closedAt,comments,url,assignees,number,state)
|
|
148
|
+
|
|
149
|
+
# DevAudit-Installer#210 §4 — enrich the auto-export with
|
|
150
|
+
# structured sections parsed from the issue body + linked PRs.
|
|
151
|
+
# This populates §4 (Root cause), §5 (Impact), §6 (Containment),
|
|
152
|
+
# the test case from the Evidence line, disposition, and
|
|
153
|
+
# sign-off with REPLACE markers (not auto/auto).
|
|
154
|
+
|
|
155
|
+
# §4a — Root cause: find the PR that closed this issue.
|
|
156
|
+
CLOSING_PRS=$(gh pr list --state merged --search "linked:issue $ISSUE_NUMBER" --json number,title,body,mergeCommit --jq '.[]' 2>/dev/null || true)
|
|
157
|
+
ROOT_CAUSE="REPLACE — root cause not documented in fix PR"
|
|
158
|
+
if [ -n "$CLOSING_PRS" ]; then
|
|
159
|
+
# Try to extract a root cause from the first linked PR's body.
|
|
160
|
+
PR_BODY=$(echo "$CLOSING_PRS" | jq -r '.body' 2>/dev/null || true)
|
|
161
|
+
if echo "$PR_BODY" | grep -qi '## Root cause'; then
|
|
162
|
+
ROOT_CAUSE=$(echo "$PR_BODY" | sed -n '/^## Root cause/,/^## /p' | head -n -1 | tail -n +2 | sed 's/^/ /' | tr -d '\r')
|
|
163
|
+
elif echo "$PR_BODY" | grep -qiE '^fix\(' ; then
|
|
164
|
+
# Conventional-commit format: extract the description.
|
|
165
|
+
ROOT_CAUSE=$(echo "$PR_BODY" | head -1 | sed -E 's/^fix\([^)]*\): //')
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# §4d — Test case that found it: parse the Evidence line from the issue body.
|
|
170
|
+
TEST_CASE=$(printf '%s' "$ISSUE_BODY" | grep -oP '\*\*Evidence\*\*.*?—\s*\K.*' | head -1 || true)
|
|
171
|
+
[ -z "$TEST_CASE" ] && TEST_CASE="REPLACE — extract from issue body Evidence line"
|
|
172
|
+
|
|
173
|
+
# §4e — Disposition: from issue close state + linked PRs.
|
|
174
|
+
DISPOSITION="Fixed"
|
|
175
|
+
if [ -z "$CLOSING_PRS" ]; then
|
|
176
|
+
DISPOSITION="REPLACE — issue closed without a merged PR; verify disposition (won't fix / duplicate / other)"
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# §4b — Impact: map severity to impact tier.
|
|
180
|
+
IMPACT_TIER="low"
|
|
181
|
+
case "$SEVERITY" in
|
|
182
|
+
critical|blocker) IMPACT_TIER="high — users affected, potential data impact" ;;
|
|
183
|
+
high|major) IMPACT_TIER="medium — significant user impact" ;;
|
|
184
|
+
*) IMPACT_TIER="low — limited user impact" ;;
|
|
185
|
+
esac
|
|
186
|
+
# Check for additional impact labels.
|
|
187
|
+
IMPACT_NOTES=""
|
|
188
|
+
if printf '%s' "$ISSUE_LABELS" | grep -qi 'gdpr\|security'; then
|
|
189
|
+
IMPACT_NOTES="- **Regulatory:** REPLACE — potential GDPR / security implications; verify scope"
|
|
190
|
+
fi
|
|
191
|
+
if printf '%s' "$ISSUE_LABELS" | grep -qi 'ops-impact'; then
|
|
192
|
+
IMPACT_NOTES="${IMPACT_NOTES}- **Operational:** REPLACE — production impact documented in incident"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# §4c — Containment: list linked PRs.
|
|
196
|
+
CONTAINMENT=""
|
|
197
|
+
if [ -n "$CLOSING_PRS" ]; then
|
|
198
|
+
MERGE_SHA=$(echo "$CLOSING_PRS" | jq -r '.mergeCommit.oid // .mergeCommit // "unknown"' 2>/dev/null | head -1)
|
|
199
|
+
PR_TITLE=$(echo "$CLOSING_PRS" | jq -r '.title' 2>/dev/null | head -1)
|
|
200
|
+
PR_NUM=$(echo "$CLOSING_PRS" | jq -r '.number' 2>/dev/null | head -1)
|
|
201
|
+
CONTAINMENT="- **Containment actions:** auto-derived from PR #${PR_NUM}: ${PR_TITLE}
|
|
202
|
+
- **Mitigation deployed:** [PR #${PR_NUM}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUM}) (${MERGE_SHA})
|
|
203
|
+
- **Recovery actions:** REPLACE — if additional recovery steps were taken
|
|
204
|
+
- **Verification that the incident is resolved:** REPLACE — how was the fix verified (re-run E2E, manual check, etc.)"
|
|
205
|
+
else
|
|
206
|
+
CONTAINMENT="- **Containment actions:** REPLACE — no linked PR found
|
|
207
|
+
- **Mitigation deployed:** REPLACE
|
|
208
|
+
- **Recovery actions:** REPLACE
|
|
209
|
+
- **Verification that the incident is resolved:** REPLACE"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# §4g — Lessons learned: scan comments for follow-up issue references.
|
|
213
|
+
FOLLOW_UPS=$(echo "$ISSUE_JSON" | jq -r '.comments[].body' | grep -oP '#\d+' | sort -u | head -5 || true)
|
|
214
|
+
LESSONS=""
|
|
215
|
+
if [ -n "$FOLLOW_UPS" ]; then
|
|
216
|
+
LESSONS="- **Follow-up actions:** ${FOLLOW_UPS}"
|
|
217
|
+
else
|
|
218
|
+
LESSONS="- **Follow-up actions:** REPLACE — file GitHub issues for any preventive actions; one row per action"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
echo "$ISSUE_JSON" | jq -r '
|
|
223
|
+
"---",
|
|
224
|
+
("title: " + (.title | @json)),
|
|
225
|
+
("incident_id: \"INC-" + (.createdAt[:10] | gsub("-";"")) + "-" + (.number|tostring) + "\""),
|
|
226
|
+
("severity: \"'"${SEVERITY}"'\""),
|
|
227
|
+
("detected_at: " + (.createdAt | @json)),
|
|
228
|
+
("resolved_at: " + (.closedAt | @json)),
|
|
229
|
+
("involves_personal_data: \"false\""),
|
|
230
|
+
("reported_to_supervisory_authority: \"n/a\""),
|
|
231
|
+
("notification_window_72h: \"n/a\""),
|
|
232
|
+
("last_reviewed_at: " + (.closedAt[:10] | @json)),
|
|
233
|
+
("source_issue: " + (.url | @json)),
|
|
234
|
+
("source_issue_number: " + (.number|tostring)),
|
|
235
|
+
"---",
|
|
236
|
+
"",
|
|
237
|
+
"> ℹ️ Auto-exported by Incident Export workflow on issue close (Path A — baseline-only).",
|
|
238
|
+
"> The narrative below is the original issue body + comments.",
|
|
239
|
+
"> No GDPR triage required — framework attribution confirmed baseline-only.",
|
|
240
|
+
"> DevAudit-Installer#210: structured sections (§4-§9) auto-populated from linked PRs + issue body.",
|
|
241
|
+
"> Sign-off section has REPLACE markers — complete before pushing to develop.",
|
|
242
|
+
"",
|
|
243
|
+
"# Incident Report — " + .title,
|
|
244
|
+
"",
|
|
245
|
+
"**Framework coverage:**",
|
|
246
|
+
"",
|
|
247
|
+
"- `ISO29119.3.5.4` (Test incident report)",
|
|
248
|
+
"",
|
|
249
|
+
"**Source:** [#" + (.number|tostring) + "](" + .url + ") ",
|
|
250
|
+
"**Detected:** " + .createdAt + " ",
|
|
251
|
+
"**Closed:** " + .closedAt + " ",
|
|
252
|
+
"**Reporter:** @" + .author.login + " ",
|
|
253
|
+
"**Assignees:** " + (if .assignees == [] then "_unassigned_" else (.assignees | map("@" + .login) | join(", ")) end) + " ",
|
|
254
|
+
"**Labels:** " + (.labels | map("`" + .name + "`") | join(", ")),
|
|
255
|
+
"",
|
|
256
|
+
"## 1. Summary",
|
|
257
|
+
"",
|
|
258
|
+
"- **Incident ID:** INC-" + (.createdAt[:10] | gsub("-";"")) + "-" + (.number|tostring),
|
|
259
|
+
"- **Severity:** '"${SEVERITY}"'",
|
|
260
|
+
"- **One-line description:** " + (.title | gsub("^\\[.*?\\]\\s*"; "")),
|
|
261
|
+
"- **Detected at:** " + .createdAt,
|
|
262
|
+
"- **Resolved at:** " + .closedAt,
|
|
263
|
+
"- **Test case that detected this incident:** '"${TEST_CASE}"'",
|
|
264
|
+
"- **Disposition:** '"${DISPOSITION}"'",
|
|
265
|
+
"",
|
|
266
|
+
"## 2. Personal data scope (GDPR triage)",
|
|
267
|
+
"",
|
|
268
|
+
"| Question | Answer |",
|
|
269
|
+
"| --- | --- |",
|
|
270
|
+
"| Did the incident involve personal data? | No |",
|
|
271
|
+
"| Notify supervisory authority (Art. 33)? | N/A — no personal data scope |",
|
|
272
|
+
"| Notify data subjects (Art. 34)? | N/A — no personal data scope |",
|
|
273
|
+
"| 72-hour notification window | N/A |",
|
|
274
|
+
"",
|
|
275
|
+
"## 3. Timeline (from issue comments)",
|
|
276
|
+
"",
|
|
277
|
+
.body,
|
|
278
|
+
""
|
|
279
|
+
'
|
|
280
|
+
echo ""
|
|
281
|
+
# Comments — one section each, ordered by createdAt.
|
|
282
|
+
echo "$ISSUE_JSON" | jq -r '
|
|
283
|
+
.comments | sort_by(.createdAt) | .[] |
|
|
284
|
+
"### " + .createdAt + " — @" + .author.login + "\n\n" + .body + "\n"
|
|
285
|
+
'
|
|
286
|
+
cat <<TAIL
|
|
287
|
+
|
|
288
|
+
## 4. Root cause
|
|
289
|
+
|
|
290
|
+
- **What happened:** ${ROOT_CAUSE}
|
|
291
|
+
- **Why it happened:** REPLACE — 5-whys or equivalent
|
|
292
|
+
- **Why it wasn't caught earlier:** REPLACE — gap in monitoring / testing / review
|
|
293
|
+
|
|
294
|
+
## 5. Impact
|
|
295
|
+
|
|
296
|
+
- **Users affected:** REPLACE — count + segment
|
|
297
|
+
- **Impact tier:** ${IMPACT_TIER}
|
|
298
|
+
- **Data confidentiality / integrity / availability impact:** REPLACE
|
|
299
|
+
- **Financial / reputational:** REPLACE
|
|
300
|
+
${IMPACT_NOTES}
|
|
301
|
+
|
|
302
|
+
## 6. Containment, mitigation, and recovery
|
|
303
|
+
|
|
304
|
+
${CONTAINMENT}
|
|
305
|
+
|
|
306
|
+
## 7. Communications
|
|
307
|
+
|
|
308
|
+
- **Internal:** REPLACE — who was notified, when
|
|
309
|
+
- **Customer / data subjects:** N/A — no personal data scope (Path A baseline-only)
|
|
310
|
+
- **Supervisory authority:** N/A
|
|
311
|
+
- **Public statement:** REPLACE — link if any
|
|
312
|
+
|
|
313
|
+
## 8. Lessons learned and follow-ups
|
|
314
|
+
|
|
315
|
+
- **What worked well:** REPLACE
|
|
316
|
+
- **What didn't:** REPLACE
|
|
317
|
+
${LESSONS}
|
|
318
|
+
|
|
319
|
+
## 9. Sign-off
|
|
320
|
+
|
|
321
|
+
| Role | Name | Date |
|
|
322
|
+
| ----------------------------------- | ------- | ------- |
|
|
323
|
+
| Incident Commander | REPLACE | REPLACE |
|
|
324
|
+
| Engineering lead | REPLACE | REPLACE |
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
_Source: auto-exported by `.github/workflows/incident-export.yml` when the originating issue was closed (Path A — baseline-only, direct commit). Structured sections auto-populated from linked PRs + issue body (DevAudit-Installer#210). Sign-off requires human attestation._
|
|
329
|
+
TAIL
|
|
330
|
+
} > "$OUT"
|
|
331
|
+
echo "Wrote $OUT (Path A — enriched)"
|
|
332
|
+
|
|
333
|
+
- name: Export issue to markdown (Path B — PR with REPLACE markers)
|
|
334
|
+
if: steps.route.outputs.path == 'B'
|
|
335
|
+
env:
|
|
336
|
+
GH_TOKEN: ${{ github.token }}
|
|
337
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
338
|
+
OUT: ${{ steps.paths.outputs.out }}
|
|
339
|
+
SEVERITY: ${{ steps.route.outputs.severity }}
|
|
61
340
|
run: |
|
|
62
341
|
mkdir -p compliance/governance
|
|
63
342
|
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \
|
|
@@ -142,9 +421,28 @@ jobs:
|
|
|
142
421
|
_Source: auto-exported by `.github/workflows/incident-export.yml` when the originating issue was closed._
|
|
143
422
|
TAIL
|
|
144
423
|
} > "$OUT"
|
|
145
|
-
echo "Wrote $OUT"
|
|
424
|
+
echo "Wrote $OUT (Path B)"
|
|
425
|
+
|
|
426
|
+
- name: Direct commit to develop (Path A)
|
|
427
|
+
if: steps.route.outputs.path == 'A'
|
|
428
|
+
env:
|
|
429
|
+
GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
|
|
430
|
+
OUT: ${{ steps.paths.outputs.out }}
|
|
431
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
432
|
+
ISSUE_URL: ${{ github.event.issue.html_url }}
|
|
433
|
+
run: |
|
|
434
|
+
git config user.name 'devaudit-bot'
|
|
435
|
+
git config user.email 'devaudit-bot@users.noreply.github.com'
|
|
436
|
+
git add "$OUT"
|
|
437
|
+
if git diff --cached --quiet; then
|
|
438
|
+
echo "No change — already exported."
|
|
439
|
+
exit 0
|
|
440
|
+
fi
|
|
441
|
+
git commit -m "chore(compliance): export incident #${ISSUE_NUMBER} (baseline-only, Path A)" -m "Auto-exported by Incident Export workflow." -m "" -m "Source: ${ISSUE_URL}" -m "" -m "Framework attribution: ISO29119.3.5.4 only — no GDPR/SOC2/EUAIA. Direct commit (no PR required)."
|
|
442
|
+
git push origin HEAD:develop
|
|
146
443
|
|
|
147
|
-
- name: Open export PR
|
|
444
|
+
- name: Open export PR (Path B)
|
|
445
|
+
if: steps.route.outputs.path == 'B'
|
|
148
446
|
env:
|
|
149
447
|
GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
|
|
150
448
|
BRANCH: ${{ steps.paths.outputs.branch }}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Label Retention — enforce the `incident` label survives to issue close
|
|
2
|
+
#
|
|
3
|
+
# Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
|
|
4
|
+
# Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists:
|
|
7
|
+
# - The `incident` label on a GitHub issue is what triggers
|
|
8
|
+
# `incident-export.yml` on close → `compliance-evidence.yml` uploads
|
|
9
|
+
# `incident_report` evidence → portal flips ISO29119.3.5.4 to COVERED.
|
|
10
|
+
# - Without enforcement, a collaborator can remove the label (intentionally
|
|
11
|
+
# or accidentally) and the incident is silently lost from the audit trail.
|
|
12
|
+
# - This workflow re-applies the label if it's removed from an issue that
|
|
13
|
+
# has a `### Framework attribution` section (the signal that the issue
|
|
14
|
+
# was filed as an incident).
|
|
15
|
+
#
|
|
16
|
+
# Opt-in via sdlc-config.json: `incident_label_retention` (default: true).
|
|
17
|
+
# Repos that don't use the incident framework can disable it.
|
|
18
|
+
#
|
|
19
|
+
# DevAudit-Installer#210.
|
|
20
|
+
|
|
21
|
+
name: Label Retention
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
issues:
|
|
25
|
+
types: [labeled, unlabeled]
|
|
26
|
+
schedule:
|
|
27
|
+
# Daily fallback scan at 09:00 UTC — catches issues where the label
|
|
28
|
+
# was removed and the unlabeled event was missed (e.g. the workflow
|
|
29
|
+
# was disabled at the time, or the issue was closed without the label).
|
|
30
|
+
- cron: '0 9 * * *'
|
|
31
|
+
|
|
32
|
+
permissions:
|
|
33
|
+
issues: write
|
|
34
|
+
contents: read
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
enforce:
|
|
38
|
+
name: Enforce incident label retention
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
if: github.repository == github.event.repository.full_name || github.event_name == 'schedule'
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v6
|
|
43
|
+
with:
|
|
44
|
+
fetch-depth: 1
|
|
45
|
+
|
|
46
|
+
- name: Check sdlc-config for opt-out
|
|
47
|
+
id: opt
|
|
48
|
+
run: |
|
|
49
|
+
if [ -f sdlc-config.json ]; then
|
|
50
|
+
ENABLED=$(jq -r '.incident_label_retention // true' sdlc-config.json)
|
|
51
|
+
else
|
|
52
|
+
ENABLED="true"
|
|
53
|
+
fi
|
|
54
|
+
echo "enabled=${ENABLED}" >> "$GITHUB_OUTPUT"
|
|
55
|
+
|
|
56
|
+
- name: Enforce label retention
|
|
57
|
+
if: steps.opt.outputs.enabled == 'true'
|
|
58
|
+
env:
|
|
59
|
+
GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
|
|
60
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
61
|
+
EVENT_ACTION: ${{ github.event.action }}
|
|
62
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
63
|
+
LABEL_NAME: ${{ github.event.label.name }}
|
|
64
|
+
ISSUE_BODY: ${{ github.event.issue.body }}
|
|
65
|
+
run: |
|
|
66
|
+
# --- Event: unlabeled — re-apply if issue has Framework attribution ---
|
|
67
|
+
if [ "$EVENT_NAME" = "issues" ] && [ "$EVENT_ACTION" = "unlabeled" ]; then
|
|
68
|
+
if [ "$LABEL_NAME" != "incident" ]; then
|
|
69
|
+
echo "Label '${LABEL_NAME}' was removed — not 'incident', no action."
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Check if the issue body has a ### Framework attribution section.
|
|
74
|
+
if printf '%s' "$ISSUE_BODY" | grep -q '### *Framework attribution'; then
|
|
75
|
+
echo "Issue #$ISSUE_NUMBER: 'incident' label removed but issue has"
|
|
76
|
+
echo "### Framework attribution section — re-applying label."
|
|
77
|
+
|
|
78
|
+
# Re-apply the incident label.
|
|
79
|
+
gh issue edit "$ISSUE_NUMBER" --add-label incident
|
|
80
|
+
|
|
81
|
+
# Post a warning comment.
|
|
82
|
+
gh issue comment "$ISSUE_NUMBER" --body "⚠️ The \`incident\` label was removed from this issue, but the issue has a \`### Framework attribution\` section — this label is required for compliance evidence generation (\`incident-export.yml\`). It has been re-applied. If this issue is **NOT** an incident, remove the \`### Framework attribution\` section first, then remove the label."
|
|
83
|
+
else
|
|
84
|
+
echo "Issue #$ISSUE_NUMBER: 'incident' label removed and no"
|
|
85
|
+
echo "### Framework attribution section found — no action."
|
|
86
|
+
fi
|
|
87
|
+
exit 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# --- Event: labeled — prompt if incident label without attribution ---
|
|
91
|
+
if [ "$EVENT_NAME" = "issues" ] && [ "$EVENT_ACTION" = "labeled" ]; then
|
|
92
|
+
if [ "$LABEL_NAME" != "incident" ]; then
|
|
93
|
+
echo "Label '${LABEL_NAME}' added — not 'incident', no action."
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Check if the issue body has a ### Framework attribution section.
|
|
98
|
+
if printf '%s' "$ISSUE_BODY" | grep -q '### *Framework attribution'; then
|
|
99
|
+
echo "Issue #$ISSUE_NUMBER: 'incident' label added and issue"
|
|
100
|
+
echo "already has ### Framework attribution — no action needed."
|
|
101
|
+
else
|
|
102
|
+
echo "Issue #$ISSUE_NUMBER: 'incident' label added but no"
|
|
103
|
+
echo "### Framework attribution section — prompting author."
|
|
104
|
+
|
|
105
|
+
gh issue comment "$ISSUE_NUMBER" --body "ℹ️ The \`incident\` label was added to this issue, but no \`### Framework attribution\` section was found in the issue body. This section is required for compliance evidence generation. Please add a \`### Framework attribution\` section with at least \`- [x] ISO29119.3.5.4\` ticked. See the \`e2e-test-engineer\` skill's filing convention for the template."
|
|
106
|
+
fi
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# --- Event: schedule — daily fallback scan ---
|
|
111
|
+
if [ "$EVENT_NAME" = "schedule" ]; then
|
|
112
|
+
echo "Running daily fallback scan for closed issues with"
|
|
113
|
+
echo "### Framework attribution but no incident label..."
|
|
114
|
+
|
|
115
|
+
# List recently closed issues (last 30 days) without the incident label.
|
|
116
|
+
CLOSED_ISSUES=$(gh issue list --state closed --limit 50 --json number,body,labels --jq '.[] | select(.labels | map(.name) | index("incident") | not)')
|
|
117
|
+
|
|
118
|
+
if [ -z "$CLOSED_ISSUES" ]; then
|
|
119
|
+
echo "No closed issues without incident label found — nothing to scan."
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
FOUND=0
|
|
124
|
+
echo "$CLOSED_ISSUES" | jq -c '.' | while read -r issue; do
|
|
125
|
+
NUM=$(echo "$issue" | jq -r '.number')
|
|
126
|
+
BODY=$(echo "$issue" | jq -r '.body')
|
|
127
|
+
if printf '%s' "$BODY" | grep -q '### *Framework attribution'; then
|
|
128
|
+
echo "Issue #$NUM: has ### Framework attribution but no incident label!"
|
|
129
|
+
# Check if we already posted a warning (avoid spam).
|
|
130
|
+
EXISTING=$(gh issue view "$NUM" --json comments --jq '[.comments[].body] | any(. | contains("Label Retention daily scan"))')
|
|
131
|
+
if [ "$EXISTING" = "false" ]; then
|
|
132
|
+
gh issue comment "$NUM" --body "⚠️ **Label Retention daily scan:** This closed issue has a \`### Framework attribution\` section but is missing the \`incident\` label. Without the label, \`incident-export.yml\` will not export this issue as \`incident_report\` evidence, and \`ISO29119.3.5.4\` will not flip to COVERED on the portal. If this is genuinely an incident, re-apply the \`incident\` label. If it is not, remove the \`### Framework attribution\` section."
|
|
133
|
+
FOUND=$((FOUND + 1))
|
|
134
|
+
else
|
|
135
|
+
echo "Issue #$NUM: warning already posted — skipping."
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
echo "Daily scan complete. Warnings posted: ${FOUND}"
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
echo "Unrecognised event: $EVENT_NAME / $EVENT_ACTION — no action."
|
|
@@ -148,6 +148,81 @@ jobs:
|
|
|
148
148
|
}
|
|
149
149
|
RESULTS_EOF
|
|
150
150
|
|
|
151
|
+
- name: File incident on smoke failure
|
|
152
|
+
if: failure()
|
|
153
|
+
env:
|
|
154
|
+
GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
|
|
155
|
+
PROD_URL: ${{ secrets.{{PRODUCTION_URL_SECRET}} }}
|
|
156
|
+
GIT_SHA: ${{ github.sha }}
|
|
157
|
+
CI_RUN: ${{ github.run_id }}
|
|
158
|
+
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
159
|
+
run: |
|
|
160
|
+
# DevAudit-Installer#210 §10a — file an incident issue when the
|
|
161
|
+
# production smoke tests fail. The `incident` label ensures
|
|
162
|
+
# incident-export.yml fires on close → incident_report evidence
|
|
163
|
+
# lands on the portal → ISO29119.3.5.4 + SOC2.CC7.2 flip to COVERED.
|
|
164
|
+
#
|
|
165
|
+
# testCycleId (CI run ID) cross-references #209 so the incident
|
|
166
|
+
# report traces back to the specific test cycle that detected
|
|
167
|
+
# the production failure.
|
|
168
|
+
#
|
|
169
|
+
# Ensure the incident label exists (idempotent).
|
|
170
|
+
gh label list --json name --jq '.[].name' | grep -qx incident || \
|
|
171
|
+
gh label create incident --color 'B60205' \
|
|
172
|
+
--description 'Operational, test, or compliance incident; close to auto-archive as portal evidence'
|
|
173
|
+
|
|
174
|
+
SMOKE_RESULTS="No prod-smoke-results.json file found (smoke tests failed before producing output)."
|
|
175
|
+
if [ -f prod-smoke-results.json ]; then
|
|
176
|
+
SMOKE_RESULTS=$(cat prod-smoke-results.json)
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
DATE=$(date -u +%Y-%m-%d)
|
|
180
|
+
ISSUE_BODY=$(cat <<BODY_EOF
|
|
181
|
+
## Production Smoke Test Failure
|
|
182
|
+
|
|
183
|
+
**Production URL:** ${PROD_URL}
|
|
184
|
+
**Git SHA:** ${GIT_SHA}
|
|
185
|
+
**testCycleId:** ${CI_RUN}
|
|
186
|
+
**Workflow run:** ${WORKFLOW_URL}
|
|
187
|
+
**Date:** ${DATE}
|
|
188
|
+
|
|
189
|
+
### Smoke Results
|
|
190
|
+
|
|
191
|
+
\`\`\`json
|
|
192
|
+
${SMOKE_RESULTS}
|
|
193
|
+
\`\`\`
|
|
194
|
+
|
|
195
|
+
This incident was detected by the autonomous post-deploy production smoke workflow. The production health check failed after deploying ${GIT_SHA}. Page the on-call per the project's incident playbook. The \`incident\` label ensures an \`incident_report\` will be generated on close.
|
|
196
|
+
|
|
197
|
+
### Framework attribution
|
|
198
|
+
|
|
199
|
+
This defect, once closed with the \`incident\` label, will be auto-exported as \`incident_report\` evidence and attribute to:
|
|
200
|
+
|
|
201
|
+
- [x] \`ISO29119.3.5.4\` (baseline — every incident_report)
|
|
202
|
+
- [x] \`SOC2.CC7.2\` — ops impact: production health check failure means the app is down or degraded
|
|
203
|
+
- [ ] \`GDPR.Art-33\` — personal data scope: <REPLACE — yes/no>
|
|
204
|
+
- [ ] \`GDPR.Art-34\` — data-subject notification required: <REPLACE — yes/no>
|
|
205
|
+
- [ ] \`EUAIA.Art-9 / Art-14 / Art-15\` — AI failure: <REPLACE — yes/no, which article(s)>
|
|
206
|
+
|
|
207
|
+
Once closed, the \`incident-export.yml\` workflow exports this issue's body to \`compliance/governance/incident-report-<N>.md\`. Routing depends on the Framework attribution ticks (DevAudit-Installer#200 Fix 1):
|
|
208
|
+
- **Path A (baseline-only — only \`ISO29119.3.5.4\` ticked):** direct-committed to \`develop\`, no PR. GDPR triage pre-filled as N/A. Next \`compliance-evidence.yml\` run uploads as \`incident_report\`.
|
|
209
|
+
- **Path B (any of SOC2/GDPR/EUAIA ticked):** auto-files a PR with the GDPR triage + sign-off sections to fill in. Merge that PR → \`compliance-evidence.yml\` uploads as \`incident_report\`.
|
|
210
|
+
BODY_EOF
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Check for existing open incident issue for this smoke failure (dedup).
|
|
214
|
+
EXISTING=$(gh issue list --label incident --state open --search "[PROD-SMOKE] ${DATE}" --json number --jq '.[0].number' || true)
|
|
215
|
+
if [ -n "$EXISTING" ]; then
|
|
216
|
+
echo "Existing incident #$EXISTING already open for this date — posting comment instead of filing duplicate."
|
|
217
|
+
gh issue comment "$EXISTING" --body "Additional smoke failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA})."
|
|
218
|
+
else
|
|
219
|
+
gh issue create \
|
|
220
|
+
--title "[PROD-SMOKE] Production health check failed — ${DATE}" \
|
|
221
|
+
--label "incident,blocker" \
|
|
222
|
+
--body "$ISSUE_BODY"
|
|
223
|
+
echo "Incident issue filed for production smoke failure."
|
|
224
|
+
fi
|
|
225
|
+
|
|
151
226
|
- name: Promote in-scope releases (evidence + status)
|
|
152
227
|
run: |
|
|
153
228
|
chmod +x scripts/upload-evidence.sh 2>/dev/null || true
|
|
@@ -162,9 +237,10 @@ jobs:
|
|
|
162
237
|
# Production smoke evidence (whole-app health) attached to this release.
|
|
163
238
|
if [ -f prod-smoke-results.json ]; then
|
|
164
239
|
bash scripts/upload-evidence.sh \
|
|
165
|
-
"${PROJECT_SLUG}" "${VERSION}"
|
|
240
|
+
"${PROJECT_SLUG}" "${VERSION}" smoke_test prod-smoke-results.json \
|
|
166
241
|
--release "${VERSION}" --create-release-if-missing --environment production \
|
|
167
|
-
--category
|
|
242
|
+
--category smoke_test --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
|
|
243
|
+
--test-cycle "${CI_RUN}" \
|
|
168
244
|
|| echo "Warning: smoke upload failed for ${VERSION}"
|
|
169
245
|
fi
|
|
170
246
|
# Carry the release ticket into the production environment so the
|
|
@@ -177,9 +253,10 @@ jobs:
|
|
|
177
253
|
done
|
|
178
254
|
if [ -n "$TICKET" ]; then
|
|
179
255
|
bash scripts/upload-evidence.sh \
|
|
180
|
-
"${PROJECT_SLUG}" "${VERSION}"
|
|
256
|
+
"${PROJECT_SLUG}" "${VERSION}" release_ticket "$TICKET" \
|
|
181
257
|
--release "${VERSION}" --create-release-if-missing --environment production \
|
|
182
258
|
--category release_artifact --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
|
|
259
|
+
--test-cycle "${CI_RUN}" \
|
|
183
260
|
|| echo "Warning: ticket upload failed for ${VERSION}"
|
|
184
261
|
else
|
|
185
262
|
echo "No RELEASE-TICKET-${VERSION}.md found — skipping ticket (date-versioned or archived)."
|