@metasession.co/devaudit-cli 0.1.68 → 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.
Files changed (27) hide show
  1. package/dist/index.js +11 -4
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +10 -1
  5. package/sdlc/CLAUDE.md +25 -2
  6. package/sdlc/SKILLS.md +1 -1
  7. package/sdlc/ai-rules/INSTRUCTIONS-SDLC.md +21 -5
  8. package/sdlc/files/_common/3-compile-evidence.md +63 -2
  9. package/sdlc/files/_common/4-submit-for-review.md +2 -1
  10. package/sdlc/files/_common/5-deploy-main.md +1 -0
  11. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +1 -1
  12. package/sdlc/files/_common/Test_Strategy.md +1 -1
  13. package/sdlc/files/_common/governance/nil-incident-report.md.template +38 -0
  14. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +13 -0
  15. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +12 -2
  16. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +6 -1
  17. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +137 -25
  18. package/sdlc/files/_common/skills/sdlc-implementer/references/call-graph.md +1 -1
  19. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +27 -2
  20. package/sdlc/files/ci/ci.yml.template +83 -13
  21. package/sdlc/files/ci/compliance-evidence.yml.template +353 -53
  22. package/sdlc/files/ci/feature-e2e.yml.template +3 -2
  23. package/sdlc/files/ci/incident-export.yml.template +306 -8
  24. package/sdlc/files/ci/label-retention.yml.template +144 -0
  25. package/sdlc/files/ci/post-deploy-prod.yml.template +80 -3
  26. package/sdlc/files/ci/python/ci.yml.template +60 -16
  27. package/sdlc/files/sdlc-config.example.json +1 -1
@@ -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
- # opens a PR adding it to compliance/governance/, and lets the next
14
- # push to develop pick it up via compliance-evidence.yml's
15
- # upload_governance helper (uploads as `incident_report`).
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}" test_report prod-smoke-results.json \
240
+ "${PROJECT_SLUG}" "${VERSION}" smoke_test prod-smoke-results.json \
166
241
  --release "${VERSION}" --create-release-if-missing --environment production \
167
- --category test_report --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
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}" compliance_document "$TICKET" \
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)."