@metasession.co/devaudit-cli 0.3.18 → 0.3.19

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 (36) hide show
  1. package/dist/index.js +35 -34
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +23 -19
  5. package/sdlc/files/_common/governance/incident-report.md.template +6 -1
  6. package/sdlc/files/_common/governance/nil-incident-report.md.template +5 -2
  7. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +8 -1
  8. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +58 -10
  9. package/sdlc/files/_common/scripts/check-self-hosted-runner.sh +103 -0
  10. package/sdlc/files/_common/scripts/check-self-hosted-runner.test.sh +74 -0
  11. package/sdlc/files/_common/scripts/record-uat-execution.sh +195 -0
  12. package/sdlc/files/_common/scripts/record-uat-execution.test.sh +144 -0
  13. package/sdlc/files/_common/scripts/render-test-executions.sh +146 -0
  14. package/sdlc/files/_common/scripts/{render-test-cycles.test.sh → render-test-executions.test.sh} +12 -18
  15. package/sdlc/files/_common/scripts/{report-test-cycle.sh → report-test-execution.sh} +66 -74
  16. package/sdlc/files/_common/scripts/report-test-execution.test.sh +177 -0
  17. package/sdlc/files/_common/scripts/submit-for-uat-review.sh +21 -0
  18. package/sdlc/files/_common/scripts/upload-evidence.sh +23 -19
  19. package/sdlc/files/_common/scripts/validate-commits.sh +6 -4
  20. package/sdlc/files/_common/scripts/validate-commits.test.sh +15 -0
  21. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +9 -7
  22. package/sdlc/files/_common/skills/e2e-test-engineer/references/e2e-regression-3-tier.yml +4 -1
  23. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +16 -14
  24. package/sdlc/files/ci/ci.yml.template +54 -44
  25. package/sdlc/files/ci/compliance-evidence.yml.template +135 -46
  26. package/sdlc/files/ci/feature-e2e.yml.template +17 -17
  27. package/sdlc/files/ci/incident-export.yml.template +18 -4
  28. package/sdlc/files/ci/post-deploy-prod.yml.template +33 -30
  29. package/sdlc/files/ci/python/ci.yml.template +14 -14
  30. package/sdlc/files/ci/quality-gates-provenance.yml.template +3 -0
  31. package/sdlc/package.json +1 -1
  32. package/sdlc/src/blueprints/3-compile-evidence.raw.md +10 -10
  33. package/sdlc/src/blueprints/4-submit-for-review.raw.md +1 -1
  34. package/sdlc/src/blueprints/5-deploy-main.raw.md +1 -1
  35. package/sdlc/src/blueprints/implementing-an-sdlc-issue.raw.md +3 -3
  36. package/sdlc/files/_common/scripts/render-test-cycles.sh +0 -227
@@ -57,7 +57,18 @@ jobs:
57
57
  # tag each captured screenshot as feature vs regression.
58
58
  fetch-depth: 0
59
59
 
60
- - name: Start authoritative quality-gate cycle
60
+ - name: Validate self-hosted runner prerequisites
61
+ env:
62
+ DEVAUDIT_RUNNER_ENVIRONMENT: ${{ runner.environment }}
63
+ run: |
64
+ if [ -f scripts/check-self-hosted-runner.sh ]; then
65
+ chmod +x scripts/check-self-hosted-runner.sh
66
+ bash scripts/check-self-hosted-runner.sh
67
+ else
68
+ echo "::warning::scripts/check-self-hosted-runner.sh missing; run devaudit update to sync runner preflight."
69
+ fi
70
+
71
+ - name: Start authoritative quality-gate execution
61
72
  if: github.event_name != 'pull_request' && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.outputs.version != 'skip'
62
73
  run: |
63
74
  BASE=""
@@ -65,13 +76,13 @@ jobs:
65
76
  BASE=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
66
77
  fi
67
78
  [ -n "$BASE" ] || BASE="$DEVAUDIT_BASE_URL_VAR"
68
- [ -n "$BASE" ] || { echo "::warning::No DevAudit URL; cycle lifecycle disabled"; exit 0; }
79
+ [ -n "$BASE" ] || { echo "::warning::No DevAudit URL; test execution lifecycle disabled"; exit 0; }
69
80
  echo "DEVAUDIT_BASE_URL=${BASE%/}" >> "$GITHUB_ENV"
70
- chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
71
- bash scripts/report-test-cycle.sh start \
81
+ chmod +x scripts/report-test-execution.sh scripts/report-release-check.sh 2>/dev/null || true
82
+ bash scripts/report-test-execution.sh start \
72
83
  --project-slug {{PROJECT_SLUG}} \
73
84
  --release "${{ needs.register-release.outputs.version }}" \
74
- --sdlc-stage 2 --environment ci --cycle-kind quality_gate \
85
+ --sdlc-stage 2 --environment ci --suite-kind quality_gate \
75
86
  --provider github_actions --external-run-id "${{ github.run_id }}" \
76
87
  --external-run-attempt "${{ github.run_attempt }}" \
77
88
  --external-job-id "quality-gates" \
@@ -323,11 +334,11 @@ jobs:
323
334
  echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
324
335
  echo "Release version: ${VERSION}"
325
336
 
326
- - name: Ensure release exists for gate-cycle lineage
337
+ - name: Ensure release exists for gate-execution lineage
327
338
  if: env.DEVAUDIT_BASE_URL != '' && steps.version.outputs.version != 'skip'
328
339
  run: |
329
- # First-class cycle APIs need an exact release row before the
330
- # stage-2 cycle can start. This keeps failed gate runs visible even
340
+ # First-class test execution APIs need an exact release row before the
341
+ # stage-2 execution can start. This keeps failed gate runs visible even
331
342
  # when later evidence generation fails.
332
343
  chmod +x scripts/upload-evidence.sh 2>/dev/null || true
333
344
  bash scripts/upload-evidence.sh \
@@ -501,17 +512,17 @@ jobs:
501
512
  - name: Generate and upload gate evidence
502
513
  if: env.DEVAUDIT_BASE_URL != ''
503
514
  run: |
504
- chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
515
+ chmod +x scripts/upload-evidence.sh scripts/report-test-execution.sh 2>/dev/null || true
505
516
  FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
506
517
  FLAGS="${FLAGS} --release ${{ needs.register-release.outputs.version }} --create-release-if-missing"
507
518
  FLAGS="${FLAGS} --environment uat"
508
519
  FLAGS="${FLAGS} --sdlc-stage 2"
509
- PRIMARY_LINEAGE_FLAGS=(--test-cycle "${{ github.run_id }}")
510
- CYCLE_OUT="$(mktemp)"
511
- bash scripts/report-test-cycle.sh start \
520
+ PRIMARY_LINEAGE_FLAGS=(--test-execution "${{ github.run_id }}")
521
+ EXECUTION_OUT="$(mktemp)"
522
+ bash scripts/report-test-execution.sh start \
512
523
  --project-slug {{PROJECT_SLUG}} \
513
524
  --release "${{ needs.register-release.outputs.version }}" \
514
- --sdlc-stage 2 --environment ci --cycle-kind quality_gate \
525
+ --sdlc-stage 2 --environment ci --suite-kind quality_gate \
515
526
  --provider github_actions --external-run-id "${{ github.run_id }}" \
516
527
  --external-run-attempt "${{ github.run_attempt }}" \
517
528
  --external-job-id "quality-gates" \
@@ -519,11 +530,11 @@ jobs:
519
530
  --commit-sha "${{ github.sha }}" --branch "${{ github.ref_name }}" \
520
531
  --workflow-name "Quality Gates" \
521
532
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
522
- --output-file "$CYCLE_OUT"
523
- . "$CYCLE_OUT"
524
- rm -f "$CYCLE_OUT"
525
- if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
526
- PRIMARY_LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
533
+ --output-file "$EXECUTION_OUT"
534
+ . "$EXECUTION_OUT"
535
+ rm -f "$EXECUTION_OUT"
536
+ if [ "${execution_supported:-false}" = "true" ] && [ -n "${execution_record_id:-}" ]; then
537
+ PRIMARY_LINEAGE_FLAGS+=(--evidence-scope execution --test-execution-record-id "${execution_record_id}")
527
538
  fi
528
539
 
529
540
  # Track failures across all uploads so we can fail the job at the
@@ -646,17 +657,20 @@ jobs:
646
657
  --environment uat --sdlc-stage 2
647
658
  fi
648
659
 
649
- # Per-REQ gate evidence fan-out.
650
- # The gate evidence above was uploaded to the single commit-derived
651
- # release (FLAGS carries --release <derived>). When multiple REQs
652
- # have pending release tickets, only that one REQ gets gate evidence
653
- # on the portal other in-scope REQs show "no gate evidence" and
654
- # "no tests tagged with this REQ" even though the gates ran and
655
- # passed. Gate outcomes can be attached to every active REQ, but an
656
- # E2E JSON result is REQ-scoped proof only when it actually contains
657
- # an executed test tagged for that REQ. A smoke run must never
658
- # overwrite or obscure a prior targeted feature execution merely
659
- # because a pending ticket exists (DevAudit-Installer#435).
660
+ # Per-REQ E2E JSON attachment only.
661
+ # Gate outcomes above are intentionally uploaded only to the single
662
+ # commit-derived release in FLAGS. A generic integration Quality
663
+ # Gates run is run-level evidence; pending release tickets alone do
664
+ # not make that gate current approval evidence for every REQ. Bundle
665
+ # membership must be represented through an explicit bundle manifest
666
+ # and portal lineage, not by fanning out gate-outcomes.json rows
667
+ # (DevAudit-Installer#438).
668
+ #
669
+ # E2E JSON is different: it may be attached to a REQ only when the
670
+ # report itself contains an executed test tagged for that REQ. A smoke
671
+ # run must never overwrite or obscure a prior targeted feature
672
+ # execution merely because a pending ticket exists
673
+ # (DevAudit-Installer#435).
660
674
  has_req_tagged_e2e_result() {
661
675
  python3 - "$1" "$2" <<'PY'
662
676
  import json, re, sys
@@ -687,7 +701,7 @@ jobs:
687
701
  PY
688
702
  }
689
703
  COMMIT_REQ="${{ needs.register-release.outputs.version }}"
690
- FANOUT_REQS=()
704
+ E2E_REQS=()
691
705
  if [ -d compliance/pending-releases ]; then
692
706
  for TICKET in compliance/pending-releases/RELEASE-TICKET-REQ-*.md; do
693
707
  [ -f "$TICKET" ] || continue
@@ -701,21 +715,17 @@ jobs:
701
715
  if [ "$REQ_ID" = "$COMMIT_REQ" ]; then
702
716
  continue
703
717
  fi
704
- FANOUT_REQS+=("$REQ_ID")
718
+ E2E_REQS+=("$REQ_ID")
705
719
  done
706
720
  fi
707
- if [ "${#FANOUT_REQS[@]}" -gt 0 ]; then
708
- echo "Fanning out gate evidence to in-scope REQs: ${FANOUT_REQS[*]}"
709
- for REQ_ID in "${FANOUT_REQS[@]}"; do
721
+ if [ "${#E2E_REQS[@]}" -gt 0 ]; then
722
+ echo "Checking tagged E2E JSON attachment candidates: ${E2E_REQS[*]}"
723
+ echo "::notice::Not fanning out gate-outcomes.json to pending REQs; gate evidence remains on ${COMMIT_REQ} unless represented through explicit release lineage."
724
+ for REQ_ID in "${E2E_REQS[@]}"; do
710
725
  FANOUT_FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
711
726
  FANOUT_FLAGS="${FANOUT_FLAGS} --release ${REQ_ID} --create-release-if-missing"
712
727
  FANOUT_FLAGS="${FANOUT_FLAGS} --environment uat --sdlc-stage 2"
713
- FANOUT_FLAGS="${FANOUT_FLAGS} --test-cycle ${{ github.run_id }}"
714
- if [ -f ci-evidence/gate-outcomes.json ]; then
715
- upload "gate-outcomes.json -> ${REQ_ID}" \
716
- {{PROJECT_SLUG}} "${REQ_ID}" gate_outcome ci-evidence/gate-outcomes.json \
717
- --category ci_pipeline ${FANOUT_FLAGS}
718
- fi
728
+ FANOUT_FLAGS="${FANOUT_FLAGS} --test-execution ${{ github.run_id }}"
719
729
  if [ -f ci-evidence/e2e-results.json ] && has_req_tagged_e2e_result "$REQ_ID" ci-evidence/e2e-results.json; then
720
730
  upload "e2e-results.json -> ${REQ_ID}" \
721
731
  {{PROJECT_SLUG}} "${REQ_ID}" e2e_result ci-evidence/e2e-results.json \
@@ -975,7 +985,7 @@ jobs:
975
985
  exit 1
976
986
  fi
977
987
 
978
- - name: Complete primary quality-gate cycle
988
+ - name: Complete primary quality-gate execution
979
989
  if: always() && env.DEVAUDIT_BASE_URL != ''
980
990
  run: |
981
991
  case "${{ needs.quality-gates.result }}" in
@@ -983,13 +993,13 @@ jobs:
983
993
  cancelled) OUTCOME=cancelled ;;
984
994
  *) OUTCOME=failed ;;
985
995
  esac
986
- chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
987
- bash scripts/report-test-cycle.sh complete \
996
+ chmod +x scripts/report-test-execution.sh scripts/report-release-check.sh 2>/dev/null || true
997
+ bash scripts/report-test-execution.sh complete \
988
998
  --project-slug {{PROJECT_SLUG}} \
989
999
  --release "${{ needs.register-release.outputs.version }}" \
990
1000
  --sdlc-stage 2 \
991
1001
  --environment ci \
992
- --cycle-kind quality_gate \
1002
+ --suite-kind quality_gate \
993
1003
  --provider github_actions \
994
1004
  --external-run-id "${{ github.run_id }}" \
995
1005
  --external-run-attempt "${{ github.run_attempt }}" \
@@ -345,28 +345,99 @@ jobs:
345
345
  # (operator's choice — both layouts are common).
346
346
  upload_governance compliance/periodic-review.md periodic_review
347
347
  upload_governance compliance/governance/periodic-review.md periodic_review
348
- # Incident reports: glob `incident-report*.md` so per-incident
349
- # files (e.g. `incident-report-2026-001.md`, written by
350
- # incident-export.yml from labelled GitHub issues) all
351
- # upload as real evidence. The unedited starter
352
- # `incident-report.md` matches the glob too but is skipped
353
- # by upload-evidence.sh's central stub guard so the stub
354
- # can never flip ISO29119.3.5.4 to COVERED on its own
355
- # (devaudit#133). `*.md` does not match `*.md.template`.
356
- #
357
- # DevAudit-Installer#210 §8a: also glob `nil-incident-report*.md`
358
- # — per-release "no incidents" attestations generated by
359
- # sdlc-implementer Phase 3 when no incident-labelled issues
360
- # were closed. Upload as `incident_report` evidence so the
361
- # portal flips ISO29119.3.5.4 to COVERED for clean releases.
362
- #
363
- # DevAudit-Installer#210 §5: completeness gate — skip upload
364
- # if the file contains REPLACE markers in §4 (Root cause),
365
- # §5 (Impact), or §9 (Sign-off). Incomplete incident reports
366
- # must not flip ISO29119.3.5.4 to COVERED on the portal.
348
+ frontmatter_value() {
349
+ local FILE="$1" KEY="$2"
350
+ awk -v key="$KEY" '
351
+ NR == 1 && $0 == "---" { in_fm=1; next }
352
+ in_fm && $0 == "---" { exit }
353
+ in_fm && $0 ~ "^[[:space:]]*" key ":" {
354
+ sub("^[[:space:]]*" key ":[[:space:]]*", "")
355
+ gsub(/^["'\'' ]+|["'\'' ]+$/, "")
356
+ print
357
+ exit
358
+ }
359
+ ' "$FILE" 2>/dev/null || true
360
+ }
361
+
362
+ bundle_manifest_allows_source_release() {
363
+ local SOURCE_RELEASE="$1" MANIFEST=""
364
+ [ -n "$SOURCE_RELEASE" ] || return 1
365
+ for MANIFEST in \
366
+ "compliance/pending-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json" \
367
+ "compliance/approved-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json" \
368
+ "compliance/superseded-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json"; do
369
+ [ -f "$MANIFEST" ] || continue
370
+ if jq -e --arg source "$SOURCE_RELEASE" '
371
+ any(.members[]?;
372
+ .version == $source
373
+ and (
374
+ .evidenceInheritancePolicy.mode == "all_eligible"
375
+ or any(.evidenceInheritancePolicy.scopes[]?; . == "release" or . == "stage" or . == "approval")
376
+ )
377
+ )
378
+ ' "$MANIFEST" >/dev/null 2>&1; then
379
+ return 0
380
+ fi
381
+ done
382
+ return 1
383
+ }
384
+
385
+ # Incident reports are selected by frontmatter ownership, not by
386
+ # repository-wide glob attribution. Historical files may stay on disk,
387
+ # but only the incident/no-incident artefact whose source release is
388
+ # the current derived release is uploaded to that release. If the
389
+ # current release has an explicit bundle manifest that grants
390
+ # inheritance for a predecessor, upload the file to the SOURCE release
391
+ # so the portal can inherit by lineage without relabelling ownership.
367
392
  upload_incident_report() {
368
393
  local FILE="$1"
369
394
  if [ ! -f "$FILE" ]; then return 0; fi
395
+ local KIND SOURCE_RELEASE RELEASE_SCOPE SOURCE_ISSUE SEMANTIC_ID CONTENT_HASH TARGET_RELEASE
396
+ KIND="$(frontmatter_value "$FILE" incident_kind)"
397
+ SOURCE_RELEASE="$(frontmatter_value "$FILE" source_release)"
398
+ RELEASE_SCOPE="$(frontmatter_value "$FILE" release_scope)"
399
+ SOURCE_ISSUE="$(frontmatter_value "$FILE" source_issue)"
400
+ SEMANTIC_ID="$(frontmatter_value "$FILE" semantic_id)"
401
+ [ -n "$SEMANTIC_ID" ] || SEMANTIC_ID="$(frontmatter_value "$FILE" incident_id)"
402
+
403
+ case "$KIND" in
404
+ incident|no_incidents_attestation) ;;
405
+ "")
406
+ echo "::error::$(basename "$FILE"): missing required frontmatter incident_kind. Expected incident or no_incidents_attestation."
407
+ return 1 ;;
408
+ *)
409
+ echo "::error::$(basename "$FILE"): invalid incident_kind '${KIND}'. Expected incident or no_incidents_attestation."
410
+ return 1 ;;
411
+ esac
412
+ if [ "$KIND" = "no_incidents_attestation" ] && [ -z "$SOURCE_RELEASE" ]; then
413
+ echo "::error::$(basename "$FILE"): nil incident reports require source_release frontmatter."
414
+ return 1
415
+ fi
416
+ if [ "$KIND" = "incident" ]; then
417
+ if [ -z "$SOURCE_RELEASE" ] && [ -z "$RELEASE_SCOPE" ]; then
418
+ echo "::error::$(basename "$FILE"): incident reports require source_release or release_scope frontmatter."
419
+ return 1
420
+ fi
421
+ if [ -z "$SOURCE_ISSUE" ]; then
422
+ echo "::error::$(basename "$FILE"): incident reports require source_issue frontmatter."
423
+ return 1
424
+ fi
425
+ fi
426
+ if [ -z "$SEMANTIC_ID" ]; then
427
+ echo "::error::$(basename "$FILE"): incident reports require semantic_id or incident_id frontmatter."
428
+ return 1
429
+ fi
430
+
431
+ TARGET_RELEASE="${SOURCE_RELEASE:-$RELEASE_SCOPE}"
432
+ if [ "$TARGET_RELEASE" != "$DERIVED_RELEASE" ]; then
433
+ if bundle_manifest_allows_source_release "$TARGET_RELEASE"; then
434
+ echo "Incident $(basename "$FILE") is inherited by ${DERIVED_RELEASE}; preserving source release ${TARGET_RELEASE}."
435
+ else
436
+ echo "::notice::Skipping $(basename "$FILE"): source release ${TARGET_RELEASE} is not ${DERIVED_RELEASE} and no explicit bundle manifest grants inheritance."
437
+ return 0
438
+ fi
439
+ fi
440
+
370
441
  # Completeness gate — check for REPLACE markers in structured sections.
371
442
  # §4 Root cause, §5 Impact, §9 Sign-off (populated reports).
372
443
  # For nil reports, only check the Sign-off section.
@@ -399,10 +470,17 @@ jobs:
399
470
  return 0
400
471
  fi
401
472
  fi
402
- echo "Uploading governance: $(basename "$FILE") (type=incident_report)"
473
+ CONTENT_HASH="sha256:$(sha256sum "$FILE" | awk '{print $1}')"
474
+ echo "Uploading governance: $(basename "$FILE") (type=incident_report, source_release=${TARGET_RELEASE})"
403
475
  bash scripts/upload-evidence.sh \
404
476
  {{PROJECT_SLUG}} _compliance-docs incident_report "$FILE" \
405
- --category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
477
+ --category planning ${FLAGS} --release "${TARGET_RELEASE}" \
478
+ --evidence-scope release \
479
+ --meta-key "incident_kind=${KIND}" \
480
+ --meta-key "source_release=${TARGET_RELEASE}" \
481
+ --meta-key "source_issue=${SOURCE_ISSUE:-none}" \
482
+ --meta-key "semantic_id=${SEMANTIC_ID}" \
483
+ --meta-key "content_hash=${CONTENT_HASH}" \
406
484
  "${DERIVED_META[@]}" \
407
485
  || echo "Warning: Failed to upload $(basename "$FILE")"
408
486
  }
@@ -738,7 +816,7 @@ jobs:
738
816
  if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
739
817
  run: |
740
818
  set -euo pipefail
741
- chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
819
+ chmod +x scripts/report-test-execution.sh scripts/report-release-check.sh 2>/dev/null || true
742
820
  DERIVED_RELEASE="${{ steps.version.outputs.version }}"
743
821
 
744
822
  # Tier metadata for the portal (the operator filters/groups by
@@ -746,18 +824,24 @@ jobs:
746
824
  # push to main → regression; otherwise dispatch/schedule.
747
825
  PRIOR_EVENT="${{ github.event.workflow_run.event }}"
748
826
  PRIOR_BRANCH="${{ github.event.workflow_run.head_branch }}"
827
+ E2E_ENVIRONMENT=uat
749
828
  case "$PRIOR_EVENT" in
750
- pull_request) TIER=critical; STAGE=2 ;;
751
- push) TIER=regression; STAGE=5 ;;
752
- *) TIER="${PRIOR_EVENT}"; STAGE="" ;;
829
+ pull_request) TIER=critical; STAGE=2 ;;
830
+ push) TIER=regression; STAGE=5 ;;
831
+ deployment_status) TIER=regression; STAGE=5; E2E_ENVIRONMENT=production ;;
832
+ *) TIER="${PRIOR_EVENT}"; STAGE="" ;;
753
833
  esac
754
834
 
755
835
  # Common flags for upload-evidence.sh. Branch + SHA come from
756
836
  # the triggering run, not from this workflow_run dispatch.
757
- FLAGS="--create-release-if-missing --environment uat \
837
+ FLAGS="--create-release-if-missing --environment ${E2E_ENVIRONMENT} \
758
838
  --git-sha ${{ github.event.workflow_run.head_sha }} \
759
839
  --ci-run-id ${{ github.event.workflow_run.id }} \
760
- --branch ${PRIOR_BRANCH}"
840
+ --branch ${PRIOR_BRANCH} \
841
+ --meta-key source_event=${PRIOR_EVENT} \
842
+ --meta-key source_workflow=E2E_Regression \
843
+ --meta-key source_run_id=${{ github.event.workflow_run.id }} \
844
+ --meta-key source_run_attempt=${{ github.event.workflow_run.run_attempt }}"
761
845
  [ -n "$STAGE" ] && FLAGS="${FLAGS} --sdlc-stage ${STAGE}"
762
846
 
763
847
  # In-scope REQs from pending release tickets at the triggering
@@ -820,6 +904,11 @@ jobs:
820
904
  DERIVED_RELEASE="${REQS[0]}"
821
905
  fi
822
906
 
907
+ if [ "$PRIOR_EVENT" = "deployment_status" ] && [ "${#REQS[@]}" -eq 0 ]; then
908
+ echo "::error::Deployment-origin E2E evidence requires tagged or in-scope REQ attribution; refusing _compliance-docs fallback."
909
+ exit 1
910
+ fi
911
+
823
912
  if [ "${#REQS[@]}" -eq 0 ]; then
824
913
  REQS=(_compliance-docs)
825
914
  fi
@@ -839,28 +928,28 @@ jobs:
839
928
  UPLOAD_FAILURES=0
840
929
  for REQ in "${REQS[@]}"; do
841
930
  REQ_FAILURES=0
842
- LINEAGE_FLAGS=(--test-cycle "${{ github.event.workflow_run.id }}")
843
- CYCLE_OUT="$(mktemp)"
844
- bash scripts/report-test-cycle.sh start \
931
+ LINEAGE_FLAGS=(--test-execution "${{ github.event.workflow_run.id }}")
932
+ EXECUTION_OUT="$(mktemp)"
933
+ bash scripts/report-test-execution.sh start \
845
934
  --project-slug {{PROJECT_SLUG}} \
846
935
  --release "${DERIVED_RELEASE}" \
847
936
  --sdlc-stage "${STAGE:-2}" \
848
- --environment uat \
849
- --cycle-kind e2e \
937
+ --environment "${E2E_ENVIRONMENT}" \
938
+ --suite-kind e2e \
850
939
  --provider github_actions \
851
940
  --external-run-id "${{ github.event.workflow_run.id }}" \
852
941
  --external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
853
- --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
942
+ --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${E2E_ENVIRONMENT}:${DERIVED_RELEASE}" \
854
943
  --commit-sha "${{ github.event.workflow_run.head_sha }}" \
855
944
  --branch "${PRIOR_BRANCH}" \
856
945
  --workflow-name "E2E Regression" \
857
946
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
858
947
  --started-at "${{ github.event.workflow_run.run_started_at }}" \
859
- --output-file "$CYCLE_OUT"
860
- . "$CYCLE_OUT"
861
- rm -f "$CYCLE_OUT"
862
- if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
863
- LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
948
+ --output-file "$EXECUTION_OUT"
949
+ . "$EXECUTION_OUT"
950
+ rm -f "$EXECUTION_OUT"
951
+ if [ "${execution_supported:-false}" = "true" ] && [ -n "${execution_record_id:-}" ]; then
952
+ LINEAGE_FLAGS+=(--evidence-scope execution --test-execution-record-id "${execution_record_id}")
864
953
  fi
865
954
  # 1. JSON reporter output → e2e_result. The single canonical
866
955
  # machine-readable artefact for the run.
@@ -942,16 +1031,16 @@ jobs:
942
1031
  else
943
1032
  OUTCOME_REASON="authoritative workflow_run conclusion: ${{ github.event.workflow_run.conclusion }}"
944
1033
  fi
945
- bash scripts/report-test-cycle.sh complete \
1034
+ bash scripts/report-test-execution.sh complete \
946
1035
  --project-slug {{PROJECT_SLUG}} \
947
1036
  --release "${DERIVED_RELEASE}" \
948
1037
  --sdlc-stage "${STAGE:-2}" \
949
- --environment uat \
950
- --cycle-kind e2e \
1038
+ --environment "${E2E_ENVIRONMENT}" \
1039
+ --suite-kind e2e \
951
1040
  --provider github_actions \
952
1041
  --external-run-id "${{ github.event.workflow_run.id }}" \
953
1042
  --external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
954
- --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
1043
+ --idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${E2E_ENVIRONMENT}:${DERIVED_RELEASE}" \
955
1044
  --commit-sha "${{ github.event.workflow_run.head_sha }}" \
956
1045
  --branch "${PRIOR_BRANCH}" \
957
1046
  --workflow-name "E2E Regression" \
@@ -1012,8 +1101,8 @@ jobs:
1012
1101
  # evidence). Classification rationale is included in every filed
1013
1102
  # issue body so a human can verify and reclassify.
1014
1103
  #
1015
- # testCycleId (CI run ID) cross-references #209 so the incident
1016
- # report traces back to the specific test cycle that found the defect.
1104
+ # testExecutionId (CI run ID) cross-references #209 so the incident
1105
+ # report traces back to the specific test execution that found the defect.
1017
1106
 
1018
1107
  if [ ! -f e2e-artifacts/e2e-regression-results.json ]; then
1019
1108
  echo "No e2e-regression-results.json found — cannot triage. Skipping incident filing."
@@ -1200,7 +1289,7 @@ jobs:
1200
1289
  **Classification:** ${CLASSIFICATION}
1201
1290
  **Classification rationale:** ${RATIONALE}
1202
1291
 
1203
- **testCycleId:** ${CI_RUN_ID}
1292
+ **testExecutionId:** ${CI_RUN_ID}
1204
1293
  **Workflow run:** ${WORKFLOW_URL}
1205
1294
  **Git SHA:** ${GIT_SHA}
1206
1295
 
@@ -1245,7 +1334,7 @@ jobs:
1245
1334
  **Classification:** ${CLASSIFICATION}
1246
1335
  **Classification rationale:** ${RATIONALE}
1247
1336
 
1248
- **testCycleId:** ${CI_RUN_ID}
1337
+ **testExecutionId:** ${CI_RUN_ID}
1249
1338
  **Workflow run:** ${WORKFLOW_URL}
1250
1339
  **Git SHA:** ${GIT_SHA}
1251
1340
 
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
4
4
  # Runs the E2E specs tagged with the branch's REQ on PRs to the integration
5
- # branch, so regression-tier bugs surface during the feature cycle instead of
5
+ # branch, so regression-tier bugs surface during the feature execution instead of
6
6
  # at the release gate. Uploads the report as stage-2, origin=feature evidence
7
7
  # so it does NOT co-mingle with the stage-3/5 regression runs and is NOT
8
8
  # counted as release/UAT evidence by the portal.
@@ -109,8 +109,8 @@ jobs:
109
109
  echo "No new e2e specs detected — all captures will tag origin=regression."
110
110
  fi
111
111
 
112
- - name: Register feature release and start feature E2E cycle
113
- id: cycle
112
+ - name: Register feature release and start feature E2E execution
113
+ id: exec
114
114
  if: always()
115
115
  run: |
116
116
  REQ_ID="${{ needs.detect-req.outputs.req_id }}"
@@ -121,24 +121,24 @@ jobs:
121
121
  fi
122
122
  BASE="${CONFIG_URL:-$DEVAUDIT_BASE_URL_VAR}"
123
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"
124
+ echo "::warning::DevAudit not configured — skipping feature E2E test execution lifecycle."
125
+ echo "execution_supported=false" >> "$GITHUB_OUTPUT"
126
126
  exit 0
127
127
  fi
128
128
  export DEVAUDIT_BASE_URL="${BASE%/}"
129
- chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
129
+ chmod +x scripts/upload-evidence.sh scripts/report-test-execution.sh 2>/dev/null || true
130
130
  bash scripts/upload-evidence.sh \
131
131
  {{PROJECT_SLUG}} _compliance-docs release_registration README.md \
132
132
  --release "$REQ_ID" --create-release-if-missing \
133
133
  --environment uat --category planning \
134
134
  --git-sha "${{ github.event.pull_request.head.sha }}" \
135
135
  --branch "${{ github.head_ref }}" || true
136
- bash scripts/report-test-cycle.sh start \
136
+ bash scripts/report-test-execution.sh start \
137
137
  --project-slug {{PROJECT_SLUG}} \
138
138
  --release "$REQ_ID" \
139
139
  --sdlc-stage 2 \
140
140
  --environment uat \
141
- --cycle-kind e2e \
141
+ --suite-kind e2e \
142
142
  --provider github_actions \
143
143
  --external-run-id "${{ github.run_id }}" \
144
144
  --external-run-attempt "${{ github.run_attempt }}" \
@@ -168,9 +168,9 @@ jobs:
168
168
  zip -qr playwright-report.zip playwright-report/ 2>/dev/null || true
169
169
  fi
170
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 }}")
171
+ LINEAGE_FLAGS=(--test-execution "${{ github.run_id }}")
172
+ if [ "${{ steps.exec.outputs.execution_supported }}" = "true" ] && [ -n "${{ steps.exec.outputs.execution_record_id }}" ]; then
173
+ LINEAGE_FLAGS+=(--evidence-scope execution --test-execution-record-id "${{ steps.exec.outputs.execution_record_id }}")
174
174
  fi
175
175
  bash scripts/upload-evidence.sh \
176
176
  {{PROJECT_SLUG}} "$REQ_ID" feature_e2e_report playwright-report.zip \
@@ -183,10 +183,10 @@ jobs:
183
183
  || echo "::warning::feature E2E report upload failed"
184
184
  fi
185
185
 
186
- - name: Complete feature E2E cycle
186
+ - name: Complete feature E2E execution
187
187
  if: always()
188
188
  run: |
189
- if [ "${{ steps.cycle.outputs.cycle_supported }}" != "true" ] || [ -z "${{ steps.cycle.outputs.cycle_record_id }}" ]; then
189
+ if [ "${{ steps.exec.outputs.execution_supported }}" != "true" ] || [ -z "${{ steps.exec.outputs.execution_record_id }}" ]; then
190
190
  exit 0
191
191
  fi
192
192
  CONFIG_URL=""
@@ -203,13 +203,13 @@ jobs:
203
203
  *) OUTCOME=failed ;;
204
204
  esac
205
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 \
206
+ chmod +x scripts/report-test-execution.sh 2>/dev/null || true
207
+ bash scripts/report-test-execution.sh complete \
208
208
  --project-slug {{PROJECT_SLUG}} \
209
209
  --release "${{ needs.detect-req.outputs.req_id }}" \
210
210
  --sdlc-stage 2 \
211
211
  --environment uat \
212
- --cycle-kind e2e \
212
+ --suite-kind e2e \
213
213
  --provider github_actions \
214
214
  --external-run-id "${{ github.run_id }}" \
215
215
  --external-run-attempt "${{ github.run_attempt }}" \
@@ -218,5 +218,5 @@ jobs:
218
218
  --branch "${{ github.head_ref }}" \
219
219
  --workflow-name "Feature In-Scope E2E" \
220
220
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
221
- --started-at "${{ steps.cycle.outputs.cycle_started_at }}" \
221
+ --started-at "${{ steps.exec.outputs.execution_started_at }}" \
222
222
  --outcome "$OUTCOME"
@@ -145,6 +145,11 @@ jobs:
145
145
  ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
146
146
  run: |
147
147
  mkdir -p compliance/governance
148
+ SOURCE_RELEASE=$(printf '%s\n' "$ISSUE_BODY" | grep -oE 'REQ-[0-9]+|v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?' | head -1 || true)
149
+ if [ -z "$SOURCE_RELEASE" ]; then
150
+ echo "::error::Baseline-only incident export requires the issue body to reference its owning REQ-XXX or housekeeping release. Reopen or route through Path B with human frontmatter review."
151
+ exit 1
152
+ fi
148
153
  ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \
149
154
  --json title,body,labels,author,createdAt,closedAt,comments,url,assignees,number,state)
150
155
 
@@ -235,6 +240,11 @@ jobs:
235
240
  "---",
236
241
  ("title: " + (.title | @json)),
237
242
  ("incident_id: \"INC-" + (.createdAt[:10] | gsub("-";"")) + "-" + (.number|tostring) + "\""),
243
+ "incident_kind: \"incident\"",
244
+ ("source_release: \"'"${SOURCE_RELEASE}"'\""),
245
+ ("source_issue: \"" + (.number|tostring) + "\""),
246
+ ("source_issue_url: " + (.url | @json)),
247
+ ("semantic_id: \"INC-" + (.createdAt[:10] | gsub("-";"")) + "-" + (.number|tostring) + "\""),
238
248
  ("severity: \"'"${SEVERITY}"'\""),
239
249
  ("detected_at: " + (.createdAt | @json)),
240
250
  ("resolved_at: " + (.closedAt | @json)),
@@ -242,8 +252,6 @@ jobs:
242
252
  ("reported_to_supervisory_authority: \"n/a\""),
243
253
  ("notification_window_72h: \"n/a\""),
244
254
  ("last_reviewed_at: " + (.closedAt[:10] | @json)),
245
- ("source_issue: " + (.url | @json)),
246
- ("source_issue_number: " + (.number|tostring)),
247
255
  "---",
248
256
  "",
249
257
  "> ℹ️ Auto-exported by Incident Export workflow on issue close (Path A — baseline-only).",
@@ -349,8 +357,11 @@ jobs:
349
357
  ISSUE_NUMBER: ${{ github.event.issue.number }}
350
358
  OUT: ${{ steps.paths.outputs.out }}
351
359
  SEVERITY: ${{ steps.route.outputs.severity }}
360
+ ISSUE_BODY: ${{ github.event.issue.body }}
352
361
  run: |
353
362
  mkdir -p compliance/governance
363
+ SOURCE_RELEASE=$(printf '%s\n' "$ISSUE_BODY" | grep -oE 'REQ-[0-9]+|v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?' | head -1 || true)
364
+ [ -n "$SOURCE_RELEASE" ] || SOURCE_RELEASE="REPLACE — owning REQ-XXX or vYYYY.MM.DD.N"
354
365
  ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \
355
366
  --json title,body,labels,author,createdAt,closedAt,comments,url,assignees,number,state)
356
367
  {
@@ -358,6 +369,11 @@ jobs:
358
369
  "---",
359
370
  ("title: " + (.title | @json)),
360
371
  ("incident_id: \"INC-" + (.createdAt[:10] | gsub("-";"")) + "-" + (.number|tostring) + "\""),
372
+ "incident_kind: \"incident\"",
373
+ ("source_release: \"'"${SOURCE_RELEASE}"'\""),
374
+ ("source_issue: \"" + (.number|tostring) + "\""),
375
+ ("source_issue_url: " + (.url | @json)),
376
+ ("semantic_id: \"INC-" + (.createdAt[:10] | gsub("-";"")) + "-" + (.number|tostring) + "\""),
361
377
  ("severity: \"REPLACE — low | medium | high | critical\""),
362
378
  ("detected_at: " + (.createdAt | @json)),
363
379
  ("resolved_at: " + (.closedAt | @json)),
@@ -365,8 +381,6 @@ jobs:
365
381
  ("reported_to_supervisory_authority: \"REPLACE — true | false | n/a\""),
366
382
  ("notification_window_72h: \"REPLACE — within | outside | n/a\""),
367
383
  ("last_reviewed_at: " + (.closedAt[:10] | @json)),
368
- ("source_issue: " + (.url | @json)),
369
- ("source_issue_number: " + (.number|tostring)),
370
384
  "---",
371
385
  "",
372
386
  "> ℹ️ Auto-exported by Incident Export workflow on issue close.",