@metasession.co/devaudit-cli 0.3.12 → 0.3.14

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 (67) hide show
  1. package/dist/index.js +53 -4
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +55 -1
  5. package/sdlc/files/_common/0-project-setup.md +10 -4
  6. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  7. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  8. package/sdlc/files/_common/Test_Architecture.md +15 -15
  9. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  10. package/sdlc/files/_common/Test_Policy.md +2 -2
  11. package/sdlc/files/_common/Test_Strategy.md +10 -10
  12. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  13. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  14. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  15. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  16. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  17. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  18. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  19. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  20. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  21. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  22. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  24. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  25. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  26. package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
  28. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
  30. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  32. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  33. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  34. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  35. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  36. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  37. package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
  38. package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
  39. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  41. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  42. package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
  45. package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
  46. package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
  47. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
  48. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
  49. package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
  50. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
  51. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
  52. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
  53. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
  54. package/sdlc/files/ci/ci.yml.template +119 -28
  55. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  56. package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
  57. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  58. package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
  59. package/sdlc/files/ci/python/ci.yml.template +60 -12
  60. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  61. package/sdlc/package.json +1 -1
  62. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  63. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  64. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  65. package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
  66. package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
  67. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -11,10 +11,11 @@
11
11
  # - workflow_dispatch — manual catch-up for an already-released requirement.
12
12
  #
13
13
  # It merges origin/main into a branch from develop (devaudit#281), runs
14
- # scripts/close-out-release.sh for the requirement (flip ticket + RTM, move
15
- # ticket to approved-releases/), then opens one PR to develop containing both
16
- # the main→develop sync and the compliance-tree close-out. The script is
17
- # idempotent, so a no-op produces no PR.
14
+ # scripts/close-out-release.sh for the requirement (flip the governing ticket +
15
+ # RTM, move the governing ticket to approved-releases/, and move any absorbed
16
+ # predecessor tickets to superseded-releases/), then opens one PR to develop
17
+ # containing both the main→develop sync and the compliance-tree close-out. The
18
+ # script is idempotent, so a no-op produces no PR.
18
19
 
19
20
  name: Release Close-out
20
21
 
@@ -133,6 +134,7 @@ jobs:
133
134
  - release ticket moved to approved-releases/
134
135
  - release ticket status → RELEASED
135
136
  - RTM row → RELEASED
137
+ - absorbed predecessor tickets → superseded-releases/ (if the bundle manifest listed any)
136
138
 
137
139
  Ref: ${REQ}"
138
140
  git push -u origin "$BRANCH" --force-with-lease
@@ -144,6 +146,7 @@ jobs:
144
146
  - **main → develop reconciliation** — merges released code back into develop
145
147
  - **release ticket** moved to \`approved-releases/\`, status \`RELEASED\`
146
148
  - **RTM row** flipped to \`RELEASED\`
149
+ - **absorbed predecessor tickets** moved to \`superseded-releases/\` when the release bundle lists them
147
150
 
148
151
  Review + merge to develop."
149
152
  gh pr create --base develop --head "$BRANCH" \
@@ -49,13 +49,16 @@ jobs:
49
49
  # double-upload every compliance doc.
50
50
  if: github.event_name != 'workflow_run'
51
51
  runs-on: {{RUNNER}}
52
- # Permissions are needed because the "Auto-generate housekeeping stubs"
53
- # step pushes a new branch + opens a PR via `gh pr create` when the
54
- # current push triggers a housekeeping release (vYYYY.MM.DD) that
55
- # doesn't yet have its stub release-ticket on disk. Without these
56
- # the github-actions[bot] token gets HTTP 403 on the push + on the PR
57
- # create, and the auto-housekeeping path silently leaves a red badge
58
- # on every docs-only / chore push.
52
+ # Permissions are needed because the housekeeping path now does two
53
+ # GitHub mutations:
54
+ # 1. pushes a stub branch + opens a PR via `gh pr create`
55
+ # 2. dispatches `ci.yml` on develop after that stub PR merges, so
56
+ # compliance-only housekeeping releases still get the gate evidence
57
+ # the portal approval path currently requires (#361)
58
+ #
59
+ # Without these the github-actions[bot] token gets HTTP 403 on the push,
60
+ # PR create, or workflow dispatch and the housekeeping path leaves a red
61
+ # badge or an unapprovable release behind.
59
62
  #
60
63
  # `DEVAUDIT_USER_TOKEN` is no longer consulted by this workflow (the
61
64
  # auto-stub step now uses `github.token` directly — see the env block
@@ -63,6 +66,7 @@ jobs:
63
66
  # needs; preferring a (potentially stale) PAT was the cause of the
64
67
  # 2026-06-07 401 chain on this issue.
65
68
  permissions:
69
+ actions: write # gh workflow run ci.yml --ref develop
66
70
  contents: write # push the auto-PR branch
67
71
  pull-requests: write # gh pr create
68
72
  env:
@@ -237,6 +241,43 @@ jobs:
237
241
  echo "PR #${EXISTING} already open for ${VERSION} — branch updated in place."
238
242
  fi
239
243
 
244
+ - name: Dispatch ci.yml for merged housekeeping stub
245
+ if: github.event_name == 'push' && steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
246
+ env:
247
+ GH_TOKEN: ${{ github.token }}
248
+ VERSION: ${{ steps.version.outputs.version }}
249
+ run: |
250
+ # devaudit-installer#361 — compliance-only housekeeping pushes
251
+ # intentionally skip the heavy quality-gates workflow, but the
252
+ # portal approval path still requires gate evidence before a
253
+ # housekeeping release can be approved. When the current develop
254
+ # push actually carries the housekeeping release-ticket / security-
255
+ # summary stubs, dispatch ci.yml once so the gates run on that
256
+ # exact HEAD and upload the missing evidence automatically.
257
+ if ! [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
258
+ echo "Version ${VERSION} is not housekeeping; skipping ci.yml dispatch."
259
+ exit 0
260
+ fi
261
+
262
+ TICKET_PATH="compliance/pending-releases/RELEASE-TICKET-${VERSION}.md"
263
+ SECSUM_PATH="compliance/security-summary-${VERSION}.md"
264
+ CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r "${{ github.sha }}" || true)
265
+
266
+ if ! echo "$CHANGED_FILES" | grep -Fxq "$TICKET_PATH" \
267
+ && ! echo "$CHANGED_FILES" | grep -Fxq "$SECSUM_PATH"; then
268
+ echo "Current push did not modify housekeeping stubs for ${VERSION}; skipping ci.yml dispatch."
269
+ exit 0
270
+ fi
271
+
272
+ if [ ! -f "$TICKET_PATH" ] || [ ! -f "$SECSUM_PATH" ]; then
273
+ echo "Housekeeping stubs for ${VERSION} are not both present on develop; skipping ci.yml dispatch."
274
+ exit 0
275
+ fi
276
+
277
+ echo "Dispatching ci.yml on develop so housekeeping release ${VERSION} gets gate evidence."
278
+ gh workflow run ci.yml --ref develop
279
+ echo "Triggered ci.yml workflow_dispatch for housekeeping release ${VERSION}."
280
+
240
281
  - name: Upload compliance documents
241
282
  if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
242
283
  run: |
@@ -250,7 +291,6 @@ jobs:
250
291
  # release. Note: upload-evidence.sh keeps the LAST --release seen.
251
292
  FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
252
293
  FLAGS="${FLAGS} --create-release-if-missing --environment uat --sdlc-stage 3"
253
- FLAGS="${FLAGS} --test-cycle ${{ github.run_id }}"
254
294
  DERIVED_RELEASE="${{ steps.version.outputs.version }}"
255
295
 
256
296
  # Derive change_type for the bare-date (housekeeping) DERIVED_RELEASE:
@@ -463,6 +503,30 @@ jobs:
463
503
  if [ -n "$CT" ]; then printf -- '--change-type %q ' "$CT"; fi
464
504
  }
465
505
 
506
+ # Upload root-level housekeeping security summaries:
507
+ # compliance/security-summary-vYYYY.MM.DD.md
508
+ # compliance/security-summary-vYYYY.MM.DD.N.md
509
+ #
510
+ # These are generated by the housekeeping stub path and belong to
511
+ # the bare-date housekeeping release encoded in the filename, not
512
+ # to whichever release the current commit happened to derive.
513
+ shopt -s nullglob
514
+ for SECSUM in compliance/security-summary-*.md; do
515
+ [ -f "$SECSUM" ] || continue
516
+ SECSUM_VER=$(basename "$SECSUM" .md | sed 's/^security-summary-//')
517
+ if ! echo "$SECSUM_VER" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
518
+ echo "::warning::Skipping $(basename "$SECSUM"): filename does not encode a housekeeping release version."
519
+ continue
520
+ fi
521
+ echo "Uploading housekeeping security summary: $(basename "$SECSUM") -> release ${SECSUM_VER}"
522
+ bash scripts/upload-evidence.sh \
523
+ {{PROJECT_SLUG}} _compliance-docs security_summary "$SECSUM" \
524
+ --category security ${FLAGS} --release "${SECSUM_VER}" \
525
+ "${DERIVED_META[@]}" \
526
+ || echo "Warning: Failed to upload $(basename "$SECSUM")"
527
+ done
528
+ shopt -u nullglob
529
+
466
530
  # Upload release tickets (pending only)
467
531
  if [ -d "compliance/pending-releases" ]; then
468
532
  for TICKET in compliance/pending-releases/*.md; do
@@ -704,6 +768,7 @@ jobs:
704
768
  if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
705
769
  run: |
706
770
  set -euo pipefail
771
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
707
772
  DERIVED_RELEASE="${{ steps.version.outputs.version }}"
708
773
 
709
774
  # Tier metadata for the portal (the operator filters/groups by
@@ -724,7 +789,6 @@ jobs:
724
789
  --ci-run-id ${{ github.event.workflow_run.id }} \
725
790
  --branch ${PRIOR_BRANCH}"
726
791
  [ -n "$STAGE" ] && FLAGS="${FLAGS} --sdlc-stage ${STAGE}"
727
- FLAGS="${FLAGS} --test-cycle ${{ github.event.workflow_run.id }}"
728
792
 
729
793
  # In-scope REQs from pending release tickets at the triggering
730
794
  # SHA. If the ticket was created after the triggering E2E run,
@@ -793,6 +857,30 @@ jobs:
793
857
 
794
858
  UPLOAD_FAILURES=0
795
859
  for REQ in "${REQS[@]}"; do
860
+ REQ_FAILURES=0
861
+ LINEAGE_FLAGS=(--test-cycle "${{ github.event.workflow_run.id }}")
862
+ CYCLE_OUT="$(mktemp)"
863
+ bash scripts/report-test-cycle.sh start \
864
+ --project-slug {{PROJECT_SLUG}} \
865
+ --release "${DERIVED_RELEASE}" \
866
+ --sdlc-stage "${STAGE:-2}" \
867
+ --environment uat \
868
+ --cycle-kind e2e \
869
+ --provider github_actions \
870
+ --external-run-id "${{ github.event.workflow_run.id }}" \
871
+ --external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
872
+ --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
873
+ --commit-sha "${{ github.event.workflow_run.head_sha }}" \
874
+ --branch "${PRIOR_BRANCH}" \
875
+ --workflow-name "E2E Regression" \
876
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
877
+ --started-at "${{ github.event.workflow_run.run_started_at }}" \
878
+ --output-file "$CYCLE_OUT"
879
+ . "$CYCLE_OUT"
880
+ rm -f "$CYCLE_OUT"
881
+ if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
882
+ LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
883
+ fi
796
884
  # 1. JSON reporter output → e2e_result. The single canonical
797
885
  # machine-readable artefact for the run.
798
886
  if [ -f e2e-artifacts/e2e-regression-results.json ]; then
@@ -800,12 +888,14 @@ jobs:
800
888
  {{PROJECT_SLUG}} "$REQ" e2e_result \
801
889
  e2e-artifacts/e2e-regression-results.json \
802
890
  --category e2e_result ${FLAGS} --release "${DERIVED_RELEASE}" \
891
+ "${LINEAGE_FLAGS[@]}" \
803
892
  --meta-key "tier=${TIER}"
804
893
  then
805
894
  :
806
895
  else
807
896
  echo "::warning::e2e_result upload failed for ${REQ}"
808
897
  UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
898
+ REQ_FAILURES=$((REQ_FAILURES + 1))
809
899
  fi
810
900
  fi
811
901
  # 2. Playwright HTML report zip → e2e_report. Operator-
@@ -830,13 +920,60 @@ jobs:
830
920
  {{PROJECT_SLUG}} "$REQ" e2e_report \
831
921
  e2e-artifacts/playwright-report.zip \
832
922
  --category e2e_report ${FLAGS} --release "${DERIVED_RELEASE}" \
923
+ "${LINEAGE_FLAGS[@]}" \
833
924
  --meta-key "tier=${TIER}"
834
925
  then
835
926
  :
836
927
  else
837
928
  echo "::warning::playwright HTML report upload failed for ${REQ} (non-blocking — JSON results are canonical evidence)"
929
+ REQ_FAILURES=$((REQ_FAILURES + 1))
838
930
  fi
839
931
  fi
932
+ case "${{ github.event.workflow_run.conclusion }}" in
933
+ success) REQ_OUTCOME=passed ;;
934
+ failure) REQ_OUTCOME=failed ;;
935
+ cancelled) REQ_OUTCOME=cancelled ;;
936
+ skipped) REQ_OUTCOME=skipped ;;
937
+ timed_out) REQ_OUTCOME=timed_out ;;
938
+ action_required) REQ_OUTCOME=action_required ;;
939
+ *) REQ_OUTCOME=unknown ;;
940
+ esac
941
+ bash scripts/report-test-cycle.sh complete \
942
+ --project-slug {{PROJECT_SLUG}} \
943
+ --release "${DERIVED_RELEASE}" \
944
+ --sdlc-stage "${STAGE:-2}" \
945
+ --environment uat \
946
+ --cycle-kind e2e \
947
+ --provider github_actions \
948
+ --external-run-id "${{ github.event.workflow_run.id }}" \
949
+ --external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
950
+ --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
951
+ --commit-sha "${{ github.event.workflow_run.head_sha }}" \
952
+ --branch "${PRIOR_BRANCH}" \
953
+ --workflow-name "E2E Regression" \
954
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
955
+ --started-at "${{ github.event.workflow_run.run_started_at }}" \
956
+ --completed-at "${{ github.event.workflow_run.updated_at }}" \
957
+ --outcome-reason "authoritative workflow_run conclusion: ${{ github.event.workflow_run.conclusion }}" \
958
+ --outcome "${REQ_OUTCOME}"
959
+ case "$REQ_OUTCOME" in
960
+ passed) CHECK_STATUS=successful ;;
961
+ *) CHECK_STATUS="$REQ_OUTCOME" ;;
962
+ esac
963
+ CHECK_DETAILS=$(jq -cn \
964
+ --arg conclusion "${{ github.event.workflow_run.conclusion }}" \
965
+ --argjson artifactUploadFailures "$REQ_FAILURES" \
966
+ '{executionSource:"workflow_run.conclusion",conclusion:$conclusion,artifactUploadFailures:$artifactUploadFailures}')
967
+ bash scripts/report-release-check.sh \
968
+ --project-slug {{PROJECT_SLUG}} --release "${DERIVED_RELEASE}" \
969
+ --check-key "e2e-regression:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${REQ}" \
970
+ --label "E2E Regression (${REQ})" --provider github_actions --status "$CHECK_STATUS" \
971
+ --external-run-id "${{ github.event.workflow_run.id }}" \
972
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
973
+ --commit-sha "${{ github.event.workflow_run.head_sha }}" --branch "${PRIOR_BRANCH}" \
974
+ --started-at "${{ github.event.workflow_run.run_started_at }}" \
975
+ --completed-at "${{ github.event.workflow_run.updated_at }}" \
976
+ --details-json "$CHECK_DETAILS"
840
977
  done
841
978
 
842
979
  # Per-spec failure screenshots from test-results/ are NOT
@@ -894,19 +1031,88 @@ jobs:
894
1031
  APP_DEFECT_COUNT=0
895
1032
 
896
1033
  # Parse the Playwright JSON reporter output and classify each failing test.
897
- # Using jq to extract failing tests.
898
- FAILING_TESTS=$(jq -r '
899
- [.suites[]? | .specs[]? | select(.status == "failed" or .status == "timedOut" or .status == "interrupted" or .status == "flaky")]
900
- | .[]
901
- | [.title, .status, (.tests[0].results[-1].error.message // "no error message")] | @tsv
902
- ' e2e-artifacts/e2e-regression-results.json 2>/dev/null || true)
1034
+ # Walk suites/specs/tests/results recursively so nested suite structure,
1035
+ # multi-test specs, and richer error payloads are handled reliably.
1036
+ FAILING_TESTS=$(python3 - <<'PY'
1037
+ import json
1038
+ from pathlib import Path
1039
+
1040
+ path = Path("e2e-artifacts/e2e-regression-results.json")
1041
+ if not path.exists():
1042
+ raise SystemExit(0)
1043
+
1044
+ try:
1045
+ payload = json.loads(path.read_text(encoding="utf-8"))
1046
+ except Exception:
1047
+ raise SystemExit(0)
1048
+
1049
+ failing_states = {"failed", "timedOut", "interrupted", "flaky"}
1050
+ rows = []
1051
+
1052
+ def first_error_message(result):
1053
+ error = result.get("error") or {}
1054
+ if isinstance(error, dict):
1055
+ message = str(error.get("message") or "").strip()
1056
+ if message:
1057
+ return message
1058
+ stack = str(error.get("stack") or "").strip()
1059
+ if stack:
1060
+ for line in stack.splitlines():
1061
+ line = line.strip()
1062
+ if line:
1063
+ return line
1064
+ return "no error message"
1065
+
1066
+ def visit_suite(suite, parent_titles):
1067
+ local_title = str(suite.get("title") or "").strip()
1068
+ titles = parent_titles + ([local_title] if local_title else [])
1069
+ for child in suite.get("suites") or []:
1070
+ if isinstance(child, dict):
1071
+ visit_suite(child, titles)
1072
+ for spec in suite.get("specs") or []:
1073
+ if not isinstance(spec, dict):
1074
+ continue
1075
+ spec_file = (
1076
+ str((spec.get("location") or {}).get("file") or "").strip()
1077
+ or str(spec.get("file") or "").strip()
1078
+ or "(unknown spec file)"
1079
+ )
1080
+ spec_title = str(spec.get("title") or "").strip()
1081
+ title_parts = titles + ([spec_title] if spec_title else [])
1082
+ for test in spec.get("tests") or []:
1083
+ if not isinstance(test, dict):
1084
+ continue
1085
+ test_title = str(test.get("title") or "").strip()
1086
+ full_title = " › ".join(part for part in (title_parts + ([test_title] if test_title else [])) if part)
1087
+ results = [r for r in (test.get("results") or []) if isinstance(r, dict)]
1088
+ final_result = next((r for r in reversed(results) if (r.get("status") or "") in failing_states), None)
1089
+ status = str((final_result or {}).get("status") or test.get("status") or spec.get("status") or "").strip()
1090
+ if status not in failing_states:
1091
+ continue
1092
+ rows.append((spec_file, full_title or spec_title or test_title or "(untitled test)", status, first_error_message(final_result or {})))
1093
+
1094
+ for root_suite in payload.get("suites") or []:
1095
+ if isinstance(root_suite, dict):
1096
+ visit_suite(root_suite, [])
1097
+
1098
+ seen = set()
1099
+ for spec_file, title, status, message in rows:
1100
+ key = (spec_file, title, status, message)
1101
+ if key in seen:
1102
+ continue
1103
+ seen.add(key)
1104
+ print("\t".join([spec_file, title.replace("\t", " "), status, message.replace("\t", " ")]))
1105
+ if len(seen) >= 20:
1106
+ break
1107
+ PY
1108
+ )
903
1109
 
904
1110
  if [ -z "$FAILING_TESTS" ]; then
905
1111
  echo "No failing tests found in JSON report — nothing to triage."
906
1112
  exit 0
907
1113
  fi
908
1114
 
909
- while IFS=$'\t' read -r TEST_NAME TEST_STATUS ERROR_MSG; do
1115
+ while IFS=$'\t' read -r SPEC_FILE TEST_NAME TEST_STATUS ERROR_MSG; do
910
1116
  [ -z "$TEST_NAME" ] && continue
911
1117
 
912
1118
  # --- Classification ---
@@ -962,10 +1168,10 @@ jobs:
962
1168
  # --- File issue ---
963
1169
  if [ "$FILE_LABEL" = "incident" ]; then
964
1170
  # Dedup: check for existing open incident issue for the same test name.
965
- EXISTING=$(gh issue list --label incident --state open --search "$TEST_NAME" --json number --jq '.[0].number' || true)
1171
+ EXISTING=$(gh issue list --label incident --state open --search "$SPEC_FILE $TEST_NAME" --json number --jq '.[0].number' || true)
966
1172
  if [ -n "$EXISTING" ]; then
967
1173
  echo " [DEDUP] $TEST_NAME — existing incident #$EXISTING already open. Posting comment."
968
- gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
1174
+ gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Spec: ${SPEC_FILE}. Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
969
1175
  continue
970
1176
  fi
971
1177
 
@@ -978,7 +1184,9 @@ jobs:
978
1184
  ISSUE_BODY=$(cat <<EOF
979
1185
  ## E2E Regression Failure — ${CLASSIFICATION}
980
1186
 
1187
+ **Spec file:** ${SPEC_FILE}
981
1188
  **Test name:** ${TEST_NAME}
1189
+ **Final status:** ${TEST_STATUS}
982
1190
  **Error message:**
983
1191
  \`\`\`
984
1192
  ${ERROR_MSG}
@@ -1010,7 +1218,7 @@ jobs:
1010
1218
  ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
1011
1219
 
1012
1220
  gh issue create \
1013
- --title "[REGRESSION] ${TEST_NAME}" \
1221
+ --title "[REGRESSION] ${SPEC_FILE} :: ${TEST_NAME}" \
1014
1222
  --label "incident,${CLASSIFICATION}" \
1015
1223
  --body "$ISSUE_BODY"
1016
1224
 
@@ -1021,7 +1229,9 @@ jobs:
1021
1229
  ISSUE_BODY=$(cat <<EOF
1022
1230
  ## E2E Regression Failure — ${CLASSIFICATION}
1023
1231
 
1232
+ **Spec file:** ${SPEC_FILE}
1024
1233
  **Test name:** ${TEST_NAME}
1234
+ **Final status:** ${TEST_STATUS}
1025
1235
  **Error message:**
1026
1236
  \`\`\`
1027
1237
  ${ERROR_MSG}
@@ -1040,7 +1250,7 @@ jobs:
1040
1250
  ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
1041
1251
 
1042
1252
  gh issue create \
1043
- --title "[TEST-BUG] ${TEST_NAME}" \
1253
+ --title "[TEST-BUG] ${SPEC_FILE} :: ${TEST_NAME}" \
1044
1254
  --label "test-bug" \
1045
1255
  --body "$ISSUE_BODY"
1046
1256
  fi
@@ -15,6 +15,7 @@ on:
15
15
  jobs:
16
16
  detect-req:
17
17
  name: Detect REQ from branch
18
+ if: ${{ !startsWith(github.head_ref, 'chore/close-out-') }}
18
19
  runs-on: {{RUNNER}}
19
20
  outputs:
20
21
  req_id: ${{ steps.detect.outputs.req_id }}
@@ -108,6 +109,46 @@ jobs:
108
109
  echo "No new e2e specs detected — all captures will tag origin=regression."
109
110
  fi
110
111
 
112
+ - name: Register feature release and start feature E2E cycle
113
+ id: cycle
114
+ if: always()
115
+ run: |
116
+ REQ_ID="${{ needs.detect-req.outputs.req_id }}"
117
+ : > "$GITHUB_OUTPUT"
118
+ CONFIG_URL=""
119
+ if [ -f sdlc-config.json ]; then
120
+ CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
121
+ fi
122
+ BASE="${CONFIG_URL:-$DEVAUDIT_BASE_URL_VAR}"
123
+ if [ -z "$BASE" ] || [ -z "$DEVAUDIT_API_KEY" ]; then
124
+ echo "::warning::DevAudit not configured — skipping feature E2E cycle lifecycle."
125
+ echo "cycle_supported=false" >> "$GITHUB_OUTPUT"
126
+ exit 0
127
+ fi
128
+ export DEVAUDIT_BASE_URL="${BASE%/}"
129
+ chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
130
+ bash scripts/upload-evidence.sh \
131
+ {{PROJECT_SLUG}} _compliance-docs release_registration README.md \
132
+ --release "$REQ_ID" --create-release-if-missing \
133
+ --environment uat --category planning \
134
+ --git-sha "${{ github.event.pull_request.head.sha }}" \
135
+ --branch "${{ github.head_ref }}" || true
136
+ bash scripts/report-test-cycle.sh start \
137
+ --project-slug {{PROJECT_SLUG}} \
138
+ --release "$REQ_ID" \
139
+ --sdlc-stage 2 \
140
+ --environment uat \
141
+ --cycle-kind e2e \
142
+ --provider github_actions \
143
+ --external-run-id "${{ github.run_id }}" \
144
+ --external-run-attempt "${{ github.run_attempt }}" \
145
+ --idempotency-key "github:${{ github.repository }}:feature-in-scope-e2e.e2e:${{ github.run_id }}:${{ github.run_attempt }}:2:${REQ_ID}" \
146
+ --commit-sha "${{ github.event.pull_request.head.sha }}" \
147
+ --branch "${{ github.head_ref }}" \
148
+ --workflow-name "Feature In-Scope E2E" \
149
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
150
+ --output-file "$GITHUB_OUTPUT"
151
+
111
152
  {{E2E_FEATURE_TEST_STEP}}
112
153
  - name: Upload feature E2E evidence (stage 2, origin=feature)
113
154
  if: always()
@@ -127,13 +168,55 @@ jobs:
127
168
  zip -qr playwright-report.zip playwright-report/ 2>/dev/null || true
128
169
  fi
129
170
  if [ -f playwright-report.zip ]; then
171
+ LINEAGE_FLAGS=(--test-cycle "${{ github.run_id }}")
172
+ if [ "${{ steps.cycle.outputs.cycle_supported }}" = "true" ] && [ -n "${{ steps.cycle.outputs.cycle_record_id }}" ]; then
173
+ LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${{ steps.cycle.outputs.cycle_record_id }}")
174
+ fi
130
175
  bash scripts/upload-evidence.sh \
131
176
  {{PROJECT_SLUG}} "$REQ_ID" feature_e2e_report playwright-report.zip \
132
177
  --category e2e_report --release "$REQ_ID" --create-release-if-missing \
133
178
  --environment uat --sdlc-stage 2 \
134
179
  --git-sha "${{ github.event.pull_request.head.sha }}" \
135
180
  --ci-run-id "${{ github.run_id }}" --branch "${{ github.head_ref }}" \
136
- --test-cycle "${{ github.run_id }}" \
181
+ "${LINEAGE_FLAGS[@]}" \
137
182
  --meta-key "origin=feature" \
138
183
  || echo "::warning::feature E2E report upload failed"
139
184
  fi
185
+
186
+ - name: Complete feature E2E cycle
187
+ if: always()
188
+ run: |
189
+ if [ "${{ steps.cycle.outputs.cycle_supported }}" != "true" ] || [ -z "${{ steps.cycle.outputs.cycle_record_id }}" ]; then
190
+ exit 0
191
+ fi
192
+ CONFIG_URL=""
193
+ if [ -f sdlc-config.json ]; then
194
+ CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
195
+ fi
196
+ BASE="${CONFIG_URL:-$DEVAUDIT_BASE_URL_VAR}"
197
+ if [ -z "$BASE" ] || [ -z "$DEVAUDIT_API_KEY" ]; then
198
+ exit 0
199
+ fi
200
+ case "${{ job.status }}" in
201
+ success) OUTCOME=passed ;;
202
+ cancelled) OUTCOME=cancelled ;;
203
+ *) OUTCOME=failed ;;
204
+ esac
205
+ export DEVAUDIT_BASE_URL="${BASE%/}"
206
+ chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
207
+ bash scripts/report-test-cycle.sh complete \
208
+ --project-slug {{PROJECT_SLUG}} \
209
+ --release "${{ needs.detect-req.outputs.req_id }}" \
210
+ --sdlc-stage 2 \
211
+ --environment uat \
212
+ --cycle-kind e2e \
213
+ --provider github_actions \
214
+ --external-run-id "${{ github.run_id }}" \
215
+ --external-run-attempt "${{ github.run_attempt }}" \
216
+ --idempotency-key "github:${{ github.repository }}:feature-in-scope-e2e.e2e:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.detect-req.outputs.req_id }}" \
217
+ --commit-sha "${{ github.event.pull_request.head.sha }}" \
218
+ --branch "${{ github.head_ref }}" \
219
+ --workflow-name "Feature In-Scope E2E" \
220
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
221
+ --started-at "${{ steps.cycle.outputs.cycle_started_at }}" \
222
+ --outcome "$OUTCOME"