@metasession.co/devaudit-cli 0.3.11 → 0.3.13

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 (68) hide show
  1. package/dist/index.js +94 -6
  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/ai-rules/SDLC_RULES.md +27 -7
  6. package/sdlc/files/_common/0-project-setup.md +13 -7
  7. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  8. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  9. package/sdlc/files/_common/Test_Architecture.md +15 -15
  10. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  11. package/sdlc/files/_common/Test_Policy.md +2 -2
  12. package/sdlc/files/_common/Test_Strategy.md +10 -10
  13. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  14. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  15. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  16. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  17. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  18. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  19. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  20. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  21. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  22. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  24. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  25. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  26. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.sh +65 -0
  28. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +38 -0
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +307 -58
  30. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -78
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  32. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  33. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  34. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  35. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  36. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  37. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  38. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  39. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  41. package/sdlc/files/_common/scripts/upload-evidence.sh +55 -1
  42. package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +169 -2
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +169 -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 +57 -3
  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/check-release-approval.yml.template +18 -28
  55. package/sdlc/files/ci/ci.yml.template +95 -35
  56. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  57. package/sdlc/files/ci/compliance-evidence.yml.template +234 -49
  58. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  59. package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
  60. package/sdlc/files/ci/python/ci.yml.template +60 -12
  61. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  62. package/sdlc/package.json +1 -1
  63. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  64. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  65. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  66. package/sdlc/src/blueprints/3-compile-evidence.raw.md +102 -20
  67. package/sdlc/src/blueprints/4-submit-for-review.raw.md +3 -3
  68. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -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
@@ -724,7 +788,6 @@ jobs:
724
788
  --ci-run-id ${{ github.event.workflow_run.id }} \
725
789
  --branch ${PRIOR_BRANCH}"
726
790
  [ -n "$STAGE" ] && FLAGS="${FLAGS} --sdlc-stage ${STAGE}"
727
- FLAGS="${FLAGS} --test-cycle ${{ github.event.workflow_run.id }}"
728
791
 
729
792
  # In-scope REQs from pending release tickets at the triggering
730
793
  # SHA. If the ticket was created after the triggering E2E run,
@@ -746,38 +809,39 @@ jobs:
746
809
  fi
747
810
 
748
811
  if [ "${#REQS[@]}" -eq 0 ] && [ -f e2e-artifacts/e2e-regression-results.json ]; then
812
+ {
813
+ printf '%s\n' 'import json'
814
+ printf '%s\n' 'import re'
815
+ printf '%s\n' 'import sys'
816
+ printf '%s\n' ''
817
+ printf '%s\n' 'try:'
818
+ printf '%s\n' " with open('e2e-artifacts/e2e-regression-results.json', 'r', encoding='utf-8') as handle:"
819
+ printf '%s\n' ' data = json.load(handle)'
820
+ printf '%s\n' 'except Exception:'
821
+ printf '%s\n' ' sys.exit(0)'
822
+ printf '%s\n' ''
823
+ printf '%s\n' 'seen = set()'
824
+ printf '%s\n' ''
825
+ printf '%s\n' 'def walk(value):'
826
+ printf '%s\n' ' if isinstance(value, str):'
827
+ printf '%s\n' " for req in re.findall(r'REQ-[0-9]+', value):"
828
+ printf '%s\n' ' seen.add(req)'
829
+ printf '%s\n' ' elif isinstance(value, dict):'
830
+ printf '%s\n' ' for item in value.values():'
831
+ printf '%s\n' ' walk(item)'
832
+ printf '%s\n' ' elif isinstance(value, list):'
833
+ printf '%s\n' ' for item in value:'
834
+ printf '%s\n' ' walk(item)'
835
+ printf '%s\n' ''
836
+ printf '%s\n' 'walk(data)'
837
+ printf '%s\n' 'for req in sorted(seen):'
838
+ printf '%s\n' ' print(req)'
839
+ } > /tmp/devaudit-extract-e2e-reqs.py
749
840
  while IFS= read -r REQ_ID; do
750
841
  [ -n "$REQ_ID" ] || continue
751
842
  REQS+=("$REQ_ID")
752
- done < <(python3 - <<'PY'
753
- import json
754
- import re
755
- import sys
756
-
757
- try:
758
- with open('e2e-artifacts/e2e-regression-results.json', 'r', encoding='utf-8') as handle:
759
- data = json.load(handle)
760
- except Exception:
761
- sys.exit(0)
762
-
763
- seen = set()
764
-
765
- def walk(value):
766
- if isinstance(value, str):
767
- for req in re.findall(r'REQ-[0-9]+', value):
768
- seen.add(req)
769
- elif isinstance(value, dict):
770
- for item in value.values():
771
- walk(item)
772
- elif isinstance(value, list):
773
- for item in value:
774
- walk(item)
775
-
776
- walk(data)
777
- for req in sorted(seen):
778
- print(req)
779
- PY
780
- )
843
+ done < <(python3 /tmp/devaudit-extract-e2e-reqs.py)
844
+ rm -f /tmp/devaudit-extract-e2e-reqs.py
781
845
  fi
782
846
 
783
847
  if [ "${#REQS[@]}" -eq 1 ] && echo "$DERIVED_RELEASE" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
@@ -792,6 +856,29 @@ jobs:
792
856
 
793
857
  UPLOAD_FAILURES=0
794
858
  for REQ in "${REQS[@]}"; do
859
+ REQ_FAILURES=0
860
+ LINEAGE_FLAGS=(--test-cycle "${{ github.event.workflow_run.id }}")
861
+ CYCLE_OUT="$(mktemp)"
862
+ bash scripts/report-test-cycle.sh start \
863
+ --project-slug {{PROJECT_SLUG}} \
864
+ --release "${DERIVED_RELEASE}" \
865
+ --sdlc-stage "${STAGE:-2}" \
866
+ --environment uat \
867
+ --cycle-kind e2e \
868
+ --provider github_actions \
869
+ --external-run-id "${{ github.event.workflow_run.id }}" \
870
+ --external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
871
+ --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
872
+ --commit-sha "${{ github.event.workflow_run.head_sha }}" \
873
+ --branch "${PRIOR_BRANCH}" \
874
+ --workflow-name "E2E Regression" \
875
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
876
+ --output-file "$CYCLE_OUT"
877
+ . "$CYCLE_OUT"
878
+ rm -f "$CYCLE_OUT"
879
+ if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
880
+ LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
881
+ fi
795
882
  # 1. JSON reporter output → e2e_result. The single canonical
796
883
  # machine-readable artefact for the run.
797
884
  if [ -f e2e-artifacts/e2e-regression-results.json ]; then
@@ -799,12 +886,14 @@ jobs:
799
886
  {{PROJECT_SLUG}} "$REQ" e2e_result \
800
887
  e2e-artifacts/e2e-regression-results.json \
801
888
  --category e2e_result ${FLAGS} --release "${DERIVED_RELEASE}" \
889
+ "${LINEAGE_FLAGS[@]}" \
802
890
  --meta-key "tier=${TIER}"
803
891
  then
804
892
  :
805
893
  else
806
894
  echo "::warning::e2e_result upload failed for ${REQ}"
807
895
  UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
896
+ REQ_FAILURES=$((REQ_FAILURES + 1))
808
897
  fi
809
898
  fi
810
899
  # 2. Playwright HTML report zip → e2e_report. Operator-
@@ -829,13 +918,36 @@ jobs:
829
918
  {{PROJECT_SLUG}} "$REQ" e2e_report \
830
919
  e2e-artifacts/playwright-report.zip \
831
920
  --category e2e_report ${FLAGS} --release "${DERIVED_RELEASE}" \
921
+ "${LINEAGE_FLAGS[@]}" \
832
922
  --meta-key "tier=${TIER}"
833
923
  then
834
924
  :
835
925
  else
836
926
  echo "::warning::playwright HTML report upload failed for ${REQ} (non-blocking — JSON results are canonical evidence)"
927
+ REQ_FAILURES=$((REQ_FAILURES + 1))
837
928
  fi
838
929
  fi
930
+ if [ "$REQ_FAILURES" -gt 0 ]; then
931
+ REQ_OUTCOME=failed
932
+ else
933
+ REQ_OUTCOME=passed
934
+ fi
935
+ bash scripts/report-test-cycle.sh complete \
936
+ --project-slug {{PROJECT_SLUG}} \
937
+ --release "${DERIVED_RELEASE}" \
938
+ --sdlc-stage "${STAGE:-2}" \
939
+ --environment uat \
940
+ --cycle-kind e2e \
941
+ --provider github_actions \
942
+ --external-run-id "${{ github.event.workflow_run.id }}" \
943
+ --external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
944
+ --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
945
+ --commit-sha "${{ github.event.workflow_run.head_sha }}" \
946
+ --branch "${PRIOR_BRANCH}" \
947
+ --workflow-name "E2E Regression" \
948
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
949
+ --started-at "${cycle_started_at:-}" \
950
+ --outcome "${REQ_OUTCOME}"
839
951
  done
840
952
 
841
953
  # Per-spec failure screenshots from test-results/ are NOT
@@ -893,19 +1005,88 @@ jobs:
893
1005
  APP_DEFECT_COUNT=0
894
1006
 
895
1007
  # Parse the Playwright JSON reporter output and classify each failing test.
896
- # Using jq to extract failing tests.
897
- FAILING_TESTS=$(jq -r '
898
- [.suites[]? | .specs[]? | select(.status == "failed" or .status == "timedOut" or .status == "interrupted" or .status == "flaky")]
899
- | .[]
900
- | [.title, .status, (.tests[0].results[-1].error.message // "no error message")] | @tsv
901
- ' e2e-artifacts/e2e-regression-results.json 2>/dev/null || true)
1008
+ # Walk suites/specs/tests/results recursively so nested suite structure,
1009
+ # multi-test specs, and richer error payloads are handled reliably.
1010
+ FAILING_TESTS=$(python3 - <<'PY'
1011
+ import json
1012
+ from pathlib import Path
1013
+
1014
+ path = Path("e2e-artifacts/e2e-regression-results.json")
1015
+ if not path.exists():
1016
+ raise SystemExit(0)
1017
+
1018
+ try:
1019
+ payload = json.loads(path.read_text(encoding="utf-8"))
1020
+ except Exception:
1021
+ raise SystemExit(0)
1022
+
1023
+ failing_states = {"failed", "timedOut", "interrupted", "flaky"}
1024
+ rows = []
1025
+
1026
+ def first_error_message(result):
1027
+ error = result.get("error") or {}
1028
+ if isinstance(error, dict):
1029
+ message = str(error.get("message") or "").strip()
1030
+ if message:
1031
+ return message
1032
+ stack = str(error.get("stack") or "").strip()
1033
+ if stack:
1034
+ for line in stack.splitlines():
1035
+ line = line.strip()
1036
+ if line:
1037
+ return line
1038
+ return "no error message"
1039
+
1040
+ def visit_suite(suite, parent_titles):
1041
+ local_title = str(suite.get("title") or "").strip()
1042
+ titles = parent_titles + ([local_title] if local_title else [])
1043
+ for child in suite.get("suites") or []:
1044
+ if isinstance(child, dict):
1045
+ visit_suite(child, titles)
1046
+ for spec in suite.get("specs") or []:
1047
+ if not isinstance(spec, dict):
1048
+ continue
1049
+ spec_file = (
1050
+ str((spec.get("location") or {}).get("file") or "").strip()
1051
+ or str(spec.get("file") or "").strip()
1052
+ or "(unknown spec file)"
1053
+ )
1054
+ spec_title = str(spec.get("title") or "").strip()
1055
+ title_parts = titles + ([spec_title] if spec_title else [])
1056
+ for test in spec.get("tests") or []:
1057
+ if not isinstance(test, dict):
1058
+ continue
1059
+ test_title = str(test.get("title") or "").strip()
1060
+ full_title = " › ".join(part for part in (title_parts + ([test_title] if test_title else [])) if part)
1061
+ results = [r for r in (test.get("results") or []) if isinstance(r, dict)]
1062
+ final_result = next((r for r in reversed(results) if (r.get("status") or "") in failing_states), None)
1063
+ status = str((final_result or {}).get("status") or test.get("status") or spec.get("status") or "").strip()
1064
+ if status not in failing_states:
1065
+ continue
1066
+ rows.append((spec_file, full_title or spec_title or test_title or "(untitled test)", status, first_error_message(final_result or {})))
1067
+
1068
+ for root_suite in payload.get("suites") or []:
1069
+ if isinstance(root_suite, dict):
1070
+ visit_suite(root_suite, [])
1071
+
1072
+ seen = set()
1073
+ for spec_file, title, status, message in rows:
1074
+ key = (spec_file, title, status, message)
1075
+ if key in seen:
1076
+ continue
1077
+ seen.add(key)
1078
+ print("\t".join([spec_file, title.replace("\t", " "), status, message.replace("\t", " ")]))
1079
+ if len(seen) >= 20:
1080
+ break
1081
+ PY
1082
+ )
902
1083
 
903
1084
  if [ -z "$FAILING_TESTS" ]; then
904
1085
  echo "No failing tests found in JSON report — nothing to triage."
905
1086
  exit 0
906
1087
  fi
907
1088
 
908
- while IFS=$'\t' read -r TEST_NAME TEST_STATUS ERROR_MSG; do
1089
+ while IFS=$'\t' read -r SPEC_FILE TEST_NAME TEST_STATUS ERROR_MSG; do
909
1090
  [ -z "$TEST_NAME" ] && continue
910
1091
 
911
1092
  # --- Classification ---
@@ -961,10 +1142,10 @@ jobs:
961
1142
  # --- File issue ---
962
1143
  if [ "$FILE_LABEL" = "incident" ]; then
963
1144
  # Dedup: check for existing open incident issue for the same test name.
964
- EXISTING=$(gh issue list --label incident --state open --search "$TEST_NAME" --json number --jq '.[0].number' || true)
1145
+ EXISTING=$(gh issue list --label incident --state open --search "$SPEC_FILE $TEST_NAME" --json number --jq '.[0].number' || true)
965
1146
  if [ -n "$EXISTING" ]; then
966
1147
  echo " [DEDUP] $TEST_NAME — existing incident #$EXISTING already open. Posting comment."
967
- gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
1148
+ gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Spec: ${SPEC_FILE}. Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
968
1149
  continue
969
1150
  fi
970
1151
 
@@ -977,7 +1158,9 @@ jobs:
977
1158
  ISSUE_BODY=$(cat <<EOF
978
1159
  ## E2E Regression Failure — ${CLASSIFICATION}
979
1160
 
1161
+ **Spec file:** ${SPEC_FILE}
980
1162
  **Test name:** ${TEST_NAME}
1163
+ **Final status:** ${TEST_STATUS}
981
1164
  **Error message:**
982
1165
  \`\`\`
983
1166
  ${ERROR_MSG}
@@ -1009,7 +1192,7 @@ jobs:
1009
1192
  ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
1010
1193
 
1011
1194
  gh issue create \
1012
- --title "[REGRESSION] ${TEST_NAME}" \
1195
+ --title "[REGRESSION] ${SPEC_FILE} :: ${TEST_NAME}" \
1013
1196
  --label "incident,${CLASSIFICATION}" \
1014
1197
  --body "$ISSUE_BODY"
1015
1198
 
@@ -1020,7 +1203,9 @@ jobs:
1020
1203
  ISSUE_BODY=$(cat <<EOF
1021
1204
  ## E2E Regression Failure — ${CLASSIFICATION}
1022
1205
 
1206
+ **Spec file:** ${SPEC_FILE}
1023
1207
  **Test name:** ${TEST_NAME}
1208
+ **Final status:** ${TEST_STATUS}
1024
1209
  **Error message:**
1025
1210
  \`\`\`
1026
1211
  ${ERROR_MSG}
@@ -1039,7 +1224,7 @@ jobs:
1039
1224
  ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
1040
1225
 
1041
1226
  gh issue create \
1042
- --title "[TEST-BUG] ${TEST_NAME}" \
1227
+ --title "[TEST-BUG] ${SPEC_FILE} :: ${TEST_NAME}" \
1043
1228
  --label "test-bug" \
1044
1229
  --body "$ISSUE_BODY"
1045
1230
  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"
@@ -36,6 +36,7 @@ jobs:
36
36
  runs-on: {{RUNNER}}
37
37
  permissions:
38
38
  contents: read
39
+ deployments: read
39
40
  issues: write
40
41
  env:
41
42
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
@@ -128,6 +129,17 @@ jobs:
128
129
  sleep 10
129
130
  done
130
131
 
132
+ - name: Confirm terminal host deployment success
133
+ env:
134
+ GH_TOKEN: ${{ github.token }}
135
+ run: |
136
+ chmod +x scripts/check-host-deployment.sh 2>/dev/null || true
137
+ bash scripts/check-host-deployment.sh \
138
+ --repo="${{ github.repository }}" \
139
+ --sha="${GIT_SHA}" \
140
+ --max-attempts=30 \
141
+ --poll-seconds=10
142
+
131
143
  - name: Production smoke tests (read-only)
132
144
  run: |
133
145
  echo "=== Production Smoke Tests ==="
@@ -230,7 +242,7 @@ jobs:
230
242
 
231
243
  - name: Promote in-scope releases (evidence + status)
232
244
  run: |
233
- chmod +x scripts/upload-evidence.sh 2>/dev/null || true
245
+ chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
234
246
  PROMOTED=0
235
247
  for PREFIX in ${REQS}; do
236
248
  echo "=== Promoting ${PREFIX} ==="
@@ -239,14 +251,37 @@ jobs:
239
251
  VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
240
252
  [ -z "$VERSION" ] && VERSION="${PREFIX}"
241
253
  RELEASE_ID=$(echo "$RESP" | jq -r '.latest.id // empty')
254
+ REQ_FAILURES=0
255
+ LINEAGE_FLAGS=(--test-cycle "${CI_RUN}")
256
+ CYCLE_OUT="$(mktemp)"
257
+ bash scripts/report-test-cycle.sh start \
258
+ --project-slug "${PROJECT_SLUG}" \
259
+ --release "${VERSION}" \
260
+ --sdlc-stage 5 \
261
+ --environment production \
262
+ --cycle-kind smoke \
263
+ --provider github_actions \
264
+ --external-run-id "${CI_RUN}" \
265
+ --external-run-attempt "${{ github.run_attempt }}" \
266
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
267
+ --commit-sha "${GIT_SHA}" \
268
+ --branch main \
269
+ --workflow-name "Post-Deploy Production" \
270
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
271
+ --output-file "$CYCLE_OUT"
272
+ . "$CYCLE_OUT"
273
+ rm -f "$CYCLE_OUT"
274
+ if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
275
+ LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
276
+ fi
242
277
  # Production smoke evidence (whole-app health) attached to this release.
243
278
  if [ -f prod-smoke-results.json ]; then
244
279
  bash scripts/upload-evidence.sh \
245
280
  "${PROJECT_SLUG}" "${VERSION}" smoke_test prod-smoke-results.json \
246
281
  --release "${VERSION}" --create-release-if-missing --environment production \
247
282
  --category smoke_test --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
248
- --test-cycle "${CI_RUN}" \
249
- || echo "Warning: smoke upload failed for ${VERSION}"
283
+ "${LINEAGE_FLAGS[@]}" \
284
+ || { echo "Warning: smoke upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
250
285
  fi
251
286
  # Carry the release ticket into the production environment so the
252
287
  # prod-review gate is self-contained.
@@ -261,11 +296,31 @@ jobs:
261
296
  "${PROJECT_SLUG}" "${VERSION}" release_ticket "$TICKET" \
262
297
  --release "${VERSION}" --create-release-if-missing --environment production \
263
298
  --category release_artifact --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
264
- --test-cycle "${CI_RUN}" \
265
299
  || echo "Warning: ticket upload failed for ${VERSION}"
266
300
  else
267
301
  echo "No RELEASE-TICKET-${VERSION}.md found — skipping ticket (date-versioned or archived)."
268
302
  fi
303
+ if [ "$REQ_FAILURES" -gt 0 ]; then
304
+ REQ_OUTCOME=failed
305
+ else
306
+ REQ_OUTCOME=passed
307
+ fi
308
+ bash scripts/report-test-cycle.sh complete \
309
+ --project-slug "${PROJECT_SLUG}" \
310
+ --release "${VERSION}" \
311
+ --sdlc-stage 5 \
312
+ --environment production \
313
+ --cycle-kind smoke \
314
+ --provider github_actions \
315
+ --external-run-id "${CI_RUN}" \
316
+ --external-run-attempt "${{ github.run_attempt }}" \
317
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
318
+ --commit-sha "${GIT_SHA}" \
319
+ --branch main \
320
+ --workflow-name "Post-Deploy Production" \
321
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
322
+ --started-at "${cycle_started_at:-}" \
323
+ --outcome "${REQ_OUTCOME}"
269
324
  # Advance status (idempotent — re-PATCHing an already-promoted release is a no-op).
270
325
  if [ -n "$RELEASE_ID" ]; then
271
326
  curl -s -o /dev/null -w " ${VERSION} status patch: HTTP %{http_code}\n" \