@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.
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/files/_common/0-project-setup.md +10 -4
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
- package/sdlc/files/_common/README_TEMPLATE.md +8 -8
- package/sdlc/files/_common/Test_Architecture.md +15 -15
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
- package/sdlc/files/_common/Test_Policy.md +2 -2
- package/sdlc/files/_common/Test_Strategy.md +10 -10
- package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
- package/sdlc/files/_common/governance/dpia.md.template +3 -3
- package/sdlc/files/_common/governance/incident-report.md.template +3 -3
- package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
- package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
- package/sdlc/files/_common/governance/risk-register.md.template +1 -1
- package/sdlc/files/_common/governance/ropa.md.template +2 -2
- package/sdlc/files/_common/joining-an-existing-project.md +4 -4
- package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
- package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
- package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +302 -64
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -82
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
- package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
- package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
- package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
- package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
- package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
- package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
- package/sdlc/files/ci/ci.yml.template +82 -28
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +204 -20
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
- package/sdlc/files/ci/python/ci.yml.template +60 -12
- package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +44 -2
- package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
- 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
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
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
|
-
#
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" \
|