@metasession.co/devaudit-cli 0.3.12 → 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 (65) 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 +302 -64
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -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-test-cycle.sh +344 -0
  38. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  39. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  40. package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
  41. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
  42. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
  43. package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
  44. package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
  45. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
  46. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
  47. package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
  48. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
  49. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
  50. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
  51. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
  52. package/sdlc/files/ci/ci.yml.template +82 -28
  53. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  54. package/sdlc/files/ci/compliance-evidence.yml.template +204 -20
  55. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  56. package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
  57. package/sdlc/files/ci/python/ci.yml.template +60 -12
  58. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  59. package/sdlc/package.json +1 -1
  60. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  61. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  62. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  63. package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
  64. package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
  65. 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,
@@ -793,6 +856,29 @@ jobs:
793
856
 
794
857
  UPLOAD_FAILURES=0
795
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
796
882
  # 1. JSON reporter output → e2e_result. The single canonical
797
883
  # machine-readable artefact for the run.
798
884
  if [ -f e2e-artifacts/e2e-regression-results.json ]; then
@@ -800,12 +886,14 @@ jobs:
800
886
  {{PROJECT_SLUG}} "$REQ" e2e_result \
801
887
  e2e-artifacts/e2e-regression-results.json \
802
888
  --category e2e_result ${FLAGS} --release "${DERIVED_RELEASE}" \
889
+ "${LINEAGE_FLAGS[@]}" \
803
890
  --meta-key "tier=${TIER}"
804
891
  then
805
892
  :
806
893
  else
807
894
  echo "::warning::e2e_result upload failed for ${REQ}"
808
895
  UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
896
+ REQ_FAILURES=$((REQ_FAILURES + 1))
809
897
  fi
810
898
  fi
811
899
  # 2. Playwright HTML report zip → e2e_report. Operator-
@@ -830,13 +918,36 @@ jobs:
830
918
  {{PROJECT_SLUG}} "$REQ" e2e_report \
831
919
  e2e-artifacts/playwright-report.zip \
832
920
  --category e2e_report ${FLAGS} --release "${DERIVED_RELEASE}" \
921
+ "${LINEAGE_FLAGS[@]}" \
833
922
  --meta-key "tier=${TIER}"
834
923
  then
835
924
  :
836
925
  else
837
926
  echo "::warning::playwright HTML report upload failed for ${REQ} (non-blocking — JSON results are canonical evidence)"
927
+ REQ_FAILURES=$((REQ_FAILURES + 1))
838
928
  fi
839
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}"
840
951
  done
841
952
 
842
953
  # Per-spec failure screenshots from test-results/ are NOT
@@ -894,19 +1005,88 @@ jobs:
894
1005
  APP_DEFECT_COUNT=0
895
1006
 
896
1007
  # 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)
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
+ )
903
1083
 
904
1084
  if [ -z "$FAILING_TESTS" ]; then
905
1085
  echo "No failing tests found in JSON report — nothing to triage."
906
1086
  exit 0
907
1087
  fi
908
1088
 
909
- 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
910
1090
  [ -z "$TEST_NAME" ] && continue
911
1091
 
912
1092
  # --- Classification ---
@@ -962,10 +1142,10 @@ jobs:
962
1142
  # --- File issue ---
963
1143
  if [ "$FILE_LABEL" = "incident" ]; then
964
1144
  # 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)
1145
+ EXISTING=$(gh issue list --label incident --state open --search "$SPEC_FILE $TEST_NAME" --json number --jq '.[0].number' || true)
966
1146
  if [ -n "$EXISTING" ]; then
967
1147
  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}"
1148
+ gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Spec: ${SPEC_FILE}. Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
969
1149
  continue
970
1150
  fi
971
1151
 
@@ -978,7 +1158,9 @@ jobs:
978
1158
  ISSUE_BODY=$(cat <<EOF
979
1159
  ## E2E Regression Failure — ${CLASSIFICATION}
980
1160
 
1161
+ **Spec file:** ${SPEC_FILE}
981
1162
  **Test name:** ${TEST_NAME}
1163
+ **Final status:** ${TEST_STATUS}
982
1164
  **Error message:**
983
1165
  \`\`\`
984
1166
  ${ERROR_MSG}
@@ -1010,7 +1192,7 @@ jobs:
1010
1192
  ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
1011
1193
 
1012
1194
  gh issue create \
1013
- --title "[REGRESSION] ${TEST_NAME}" \
1195
+ --title "[REGRESSION] ${SPEC_FILE} :: ${TEST_NAME}" \
1014
1196
  --label "incident,${CLASSIFICATION}" \
1015
1197
  --body "$ISSUE_BODY"
1016
1198
 
@@ -1021,7 +1203,9 @@ jobs:
1021
1203
  ISSUE_BODY=$(cat <<EOF
1022
1204
  ## E2E Regression Failure — ${CLASSIFICATION}
1023
1205
 
1206
+ **Spec file:** ${SPEC_FILE}
1024
1207
  **Test name:** ${TEST_NAME}
1208
+ **Final status:** ${TEST_STATUS}
1025
1209
  **Error message:**
1026
1210
  \`\`\`
1027
1211
  ${ERROR_MSG}
@@ -1040,7 +1224,7 @@ jobs:
1040
1224
  ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
1041
1225
 
1042
1226
  gh issue create \
1043
- --title "[TEST-BUG] ${TEST_NAME}" \
1227
+ --title "[TEST-BUG] ${SPEC_FILE} :: ${TEST_NAME}" \
1044
1228
  --label "test-bug" \
1045
1229
  --body "$ISSUE_BODY"
1046
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" \