@metasession.co/devaudit-cli 0.3.8 → 0.3.10

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.
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # validate-commits.test.sh — Fixture-based tests for validate-commits.sh.
3
+ #
4
+ # Covers the relaxed "exactly one active tracked release" behavior:
5
+ # implementation commits without REQ tags downgrade to warnings only when
6
+ # release context is unambiguous; otherwise they remain hard errors.
7
+ #
8
+ # Usage:
9
+ # ./scripts/validate-commits.test.sh
10
+
11
+ set -euo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ VALIDATOR="$SCRIPT_DIR/validate-commits.sh"
15
+ [ -x "$VALIDATOR" ] || chmod +x "$VALIDATOR"
16
+
17
+ PASS=0
18
+ FAIL=0
19
+
20
+ make_fixture() {
21
+ local dir="$1" subject="$2" body="${3:-}"
22
+ rm -rf "$dir"
23
+ mkdir -p "$dir"
24
+ cd "$dir"
25
+ git init -q --initial-branch=main
26
+ git config user.email "test@example.com"
27
+ git config user.name "test"
28
+ echo "base" > base.txt
29
+ git add base.txt
30
+ git commit -q -m "chore: base"
31
+
32
+ git checkout -q -b feature
33
+ echo "feature" > app.ts
34
+ git add app.ts
35
+ if [ -n "$body" ]; then
36
+ git commit -q -m "$subject" -m "$body"
37
+ else
38
+ git commit -q -m "$subject"
39
+ fi
40
+ }
41
+
42
+ run_validator() {
43
+ set +e
44
+ OUT_FILE=$(mktemp)
45
+ bash "$VALIDATOR" main > "$OUT_FILE" 2>&1
46
+ LAST_EXIT=$?
47
+ set -e
48
+ }
49
+
50
+ assert_exit() {
51
+ local desc="$1" want="$2"
52
+ if [ "$LAST_EXIT" = "$want" ]; then
53
+ echo " PASS: $desc (exit=$LAST_EXIT)"
54
+ PASS=$((PASS + 1))
55
+ else
56
+ echo " FAIL: $desc (want=$want, got=$LAST_EXIT)"
57
+ sed 's/^/ /' "$OUT_FILE"
58
+ FAIL=$((FAIL + 1))
59
+ fi
60
+ }
61
+
62
+ assert_grep() {
63
+ local desc="$1" pattern="$2" want_match="$3"
64
+ local found=0
65
+ if grep -qE "$pattern" "$OUT_FILE"; then
66
+ found=1
67
+ fi
68
+ if [ "$found" = "$want_match" ]; then
69
+ echo " PASS: $desc"
70
+ PASS=$((PASS + 1))
71
+ else
72
+ echo " FAIL: $desc (want match=$want_match, got=$found, pattern=$pattern)"
73
+ sed 's/^/ /' "$OUT_FILE"
74
+ FAIL=$((FAIL + 1))
75
+ fi
76
+ }
77
+
78
+ WORKDIR=$(mktemp -d -t validate-commits-test-XXXX)
79
+ trap 'rm -rf "$WORKDIR"' EXIT
80
+
81
+ echo "=== validate-commits.sh tests ==="
82
+
83
+ # Case 1: one active pending ticket makes a missing-REQ implementation
84
+ # commit a warning, not a hard failure.
85
+ echo "Case 1: single active pending release downgrades missing REQ to warning"
86
+ make_fixture "$WORKDIR/case1" "feat: implement kitchen workflow" "Co-Authored-By: Test <test@example.com>"
87
+ mkdir -p compliance/pending-releases
88
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-123.md <<'EOF'
89
+ # Release Ticket: REQ-123
90
+ EOF
91
+ git add compliance/pending-releases
92
+ git commit -q --amend --no-edit
93
+ run_validator
94
+ assert_exit "single active pending release exits 0" 0
95
+ assert_grep "warning emitted for missing REQ with one active release" 'WARNING .*one active tracked release \(REQ-123\)' 1
96
+ assert_grep "no hard error for missing requirement" "ERROR .*implementation commit but cites no requirement" 0
97
+
98
+ # Case 2: ambiguous pending release context keeps the missing-REQ path as
99
+ # a hard error.
100
+ echo "Case 2: two active pending releases keep missing REQ as hard error"
101
+ make_fixture "$WORKDIR/case2" "feat: implement kitchen workflow" "Co-Authored-By: Test <test@example.com>"
102
+ mkdir -p compliance/pending-releases
103
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-123.md <<'EOF'
104
+ # Release Ticket: REQ-123
105
+ EOF
106
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-124.md <<'EOF'
107
+ # Release Ticket: REQ-124
108
+ EOF
109
+ git add compliance/pending-releases
110
+ git commit -q --amend --no-edit
111
+ run_validator
112
+ assert_exit "ambiguous pending releases exits 1" 1
113
+ assert_grep "hard error emitted when active context is ambiguous" "ERROR .*implementation commit but cites no requirement" 1
114
+
115
+ # Case 3: no pending ticket, but exactly one active RTM row in the
116
+ # accepted statuses also downgrades to a warning.
117
+ echo "Case 3: RTM fallback with one active row downgrades missing REQ to warning"
118
+ make_fixture "$WORKDIR/case3" "fix: recover already-merged history" "Co-Authored-By: Test <test@example.com>"
119
+ mkdir -p compliance
120
+ cat > compliance/RTM.md <<'EOF'
121
+ # RTM
122
+ | REQ-ID | Title | Status |
123
+ | ------- | ----- | ------------------------- |
124
+ | REQ-200 | Done | APPROVED - DEPLOYED |
125
+ | REQ-201 | Live | TESTED - PENDING SIGN-OFF |
126
+ EOF
127
+ git add compliance/RTM.md
128
+ git commit -q --amend --no-edit
129
+ run_validator
130
+ assert_exit "single active RTM row exits 0" 0
131
+ assert_grep "warning emitted for RTM fallback context" 'WARNING .*one active tracked release \(REQ-201\)' 1
132
+ assert_grep "no hard error under single RTM fallback context" "ERROR .*implementation commit but cites no requirement" 0
133
+
134
+ # Case 4: zero active context remains a hard error.
135
+ echo "Case 4: no active release context keeps missing REQ as hard error"
136
+ make_fixture "$WORKDIR/case4" "perf: tune kitchen workflow" "Co-Authored-By: Test <test@example.com>"
137
+ run_validator
138
+ assert_exit "no active context exits 1" 1
139
+ assert_grep "hard error emitted without any active release context" "ERROR .*implementation commit but cites no requirement" 1
140
+
141
+ echo
142
+ echo "Result: $PASS passed, $FAIL failed"
143
+ [ "$FAIL" = "0" ]
@@ -553,7 +553,17 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
553
553
 
554
554
  ### Phase 4 — Submit for UAT review (SDLC stage 4)
555
555
 
556
- 1. **Open the release PR** — the PR that carries the UAT four-eyes approval gate (`check-release-approval.yml`), always into `$RELEASE_BRANCH`: `gh pr create --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH"` (e.g. `develop → main`). The implementation already landed on `$INTEGRATION_BRANCH` in Phase 2; this promotes it. (Note: if other work is also waiting on `$INTEGRATION_BRANCH`, this is a bundled release — every in-scope REQ keeps its own release record and Production approval.)
556
+ 1. **Prepare the truthful release PR** — the PR that carries the UAT four-eyes approval gate (`check-release-approval.yml`), always into `$RELEASE_BRANCH` from `$INTEGRATION_BRANCH` (e.g. `develop → main`). The implementation already landed on `$INTEGRATION_BRANCH` in Phase 2; this promotes it. (Note: if other work is also waiting on `$INTEGRATION_BRANCH`, this is a bundled release — every in-scope REQ keeps its own release record and Production approval.)
557
+
558
+ Before creating a PR manually, run the synced helper:
559
+
560
+ ```bash
561
+ ./scripts/prepare-release-pr.sh --apply --mode=update
562
+ ```
563
+
564
+ This is mandatory for devaudit-installer#312. It detects an existing open `$INTEGRATION_BRANCH → $RELEASE_BRANCH` PR, derives the current governing release from the current head, and ensures there is exactly one truthful release PR. If the existing PR is stale, the default `update` mode rewrites the title/body and comments with the refresh. Use `--mode=recreate` only when the operator explicitly wants the older PR closed and a new PR opened.
565
+
566
+ If the helper reports multiple open release PRs, halt with: "Multiple open release PRs for `$INTEGRATION_BRANCH → $RELEASE_BRANCH`; operator action — close duplicates or choose the authoritative PR, then resume REQ-XXX." Do not open another PR.
557
567
 
558
568
  PR body per the SDLC PR template (see [`.github/pull_request_template.md`](../../../../../.github/pull_request_template.md)):
559
569
  - Closes #N
@@ -571,8 +581,30 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
571
581
 
572
582
  3. **Apply labels** — `awaiting-uat-review`, `risk:<class>`.
573
583
  4. **Comment on the issue**: "Implementation complete. PR #M opened. Evidence on portal: <link>. UAT review requested. Resume with `resume REQ-XXX` once UAT approval is granted on the portal."
574
- 5. **Hard stop.** Phase 4 ends here. Do not proceed to merge; the human's next action is reviewing on the portal.
575
- 6. **Update SDLC status sticky** before halting: `bash scripts/update-sdlc-status.sh "$ISSUE_NUM" "Phase 4 release PR #<N> opened against $RELEASE_BRANCH; CI running" "Operator action — review PR #<N> + approve UAT release on the portal; sdlc-implementer halts until you ping resume REQ-XXX"`. This is a critical handoff — the sticky must reflect that the agent has stopped + the operator is on the hook.
584
+ 5. **Inspect PR blockers before handoff (devaudit-installer#304).** Run `gh pr view <N> --json mergeStateStatus,reviewDecision,statusCheckRollup,url` and categorize the state:
585
+ - `reviewDecision` not approved human approval blocker; hand off to the reviewer.
586
+ - Release Approval Gate failing while portal state is not `uat_approved` → portal approval blocker; hand off to portal UAT reviewer.
587
+ - Release Approval Gate failing while portal state is `uat_approved` → retry path below.
588
+ - Compliance validation / evidence completeness failing → real workflow or evidence defect; fix or create a follow-up PR before handoff.
589
+ - Checks pending → wait/poll until terminal before declaring the PR ready.
590
+ - Stale PR context detected by `prepare-release-pr.sh` → rerun the helper in `--apply` mode and repeat this blocker inspection.
591
+
592
+ **Executable loop (devaudit-installer#304).** Use the bundled watcher rather than a one-shot `gh` read when the PR is blocked or waiting:
593
+
594
+ ```bash
595
+ node SDLC/bin/devaudit-sdlc.js \
596
+ --watch-pr=<N> \
597
+ --repo <owner/name> \
598
+ --req XXX \
599
+ --release REQ-XXX \
600
+ --project-slug <project-slug> \
601
+ --base-url <devaudit-base-url>
602
+ ```
603
+
604
+ The watcher persists retry state in `.sdlc-pr-watch.json`, polls `gh pr view` + `gh pr checks`, re-runs likely flaky workflows automatically, and re-runs the Release Approval Gate when the portal is already approved but GitHub has not converged yet. Use `--once` when you only need a single classification pass.
605
+
606
+ 6. **Hard stop only after blocker classification.** Phase 4 ends here. Do not proceed to merge; the human's next action is reviewing on the portal or clearing the categorized blocker above.
607
+ 7. **Update SDLC status sticky** before halting: `bash scripts/update-sdlc-status.sh "$ISSUE_NUM" "Phase 4 — truthful release PR #<N> prepared against $RELEASE_BRANCH; blockers classified" "Operator action — review PR #<N> + approve UAT release on the portal; sdlc-implementer halts until you ping resume REQ-XXX"`. This is a critical handoff — the sticky must reflect that the agent has stopped + the operator is on the hook.
576
608
 
577
609
  **When an external gate hangs or fails for unrelated reasons.** A required gate may fail for reasons outside the change's scope — flaky infra, an unrelated regression test that hangs at hour-plus runtime with no log activity, a known-failing suite. When this happens:
578
610
 
@@ -685,6 +717,10 @@ The native agent must NOT continue to the next phase (Phase 3 → Phase 4, Phase
685
717
 
686
718
  **PR merged to main ≠ done.** After the release PR is merged to `main`, the native agent must re-invoke the skill for Phase 5 close-out (see Phase 5 step 0 below). The merge is not the end of the workflow — post-merge compliance steps are mandatory.
687
719
 
720
+ ### Commit-history hygiene recovery (devaudit-installer#315)
721
+
722
+ Commit-level `[REQ-XXX]` hygiene is preventive, not the strongest source of release truth after history has already merged to a protected integration branch. If CI reports a missing commit-level REQ after the branch has exactly one active tracked release ticket or one matching RTM row, treat the release ticket / RTM context as authoritative and add a follow-up compliance note instead of rewriting protected history. The generated `validate-commits.sh` implements this as a warning in the exactly-one-active-release case. If there are zero or multiple active tracked releases, the validation remains a hard error because the release context is ambiguous.
723
+
688
724
  ### Commit-scoping rule for SRS updates (devaudit-installer#226)
689
725
 
690
726
  When a commit is authored as part of REQ-XXX's SDLC flow, the commit subject must cite `[REQ-XXX]` — the **active REQ** driving the current SDLC flow — regardless of which other REQs the content touches. Other REQs may appear in the body for traceability, but never in the subject.
@@ -783,13 +783,13 @@ jobs:
783
783
  # "zero traceable evidence" even though well-written, REQ-tagged
784
784
  # specs exist on disk. If specs are found but weren't run in
785
785
  # this tier, emit a non-blocking warning instead of an error.
786
- SPECS_ON_DISK=$(grep -rl --include='*.spec.ts' -e "@requirement ${REQ}" -e "tagTest.*${REQ}" -e "'${REQ}'" e2e/ 2>/dev/null | wc -l)
786
+ SPECS_ON_DISK=$( { grep -rl --include='*.spec.ts' -e "@requirement ${REQ}" -e "tagTest.*${REQ}" -e "'${REQ}'" e2e/ 2>/dev/null || true; } | wc -l | tr -d ' ')
787
787
 
788
788
  # devaudit-installer#237 — also scan unit test files for
789
789
  # @requirement REQ-XXX annotations. A REQ with thorough unit
790
790
  # test coverage but no E2E specs (legitimate for API-only or
791
791
  # backend-only changes) should not be hard-blocked.
792
- UNIT_TESTS_ON_DISK=$(grep -rl --include='*.test.ts' --include='*.test.tsx' -e "@requirement ${REQ}" -e "'${REQ}'" e2e/ __tests__/ tests/ src/ 2>/dev/null | wc -l)
792
+ UNIT_TESTS_ON_DISK=$( { grep -rl --include='*.test.ts' --include='*.test.tsx' -e "@requirement ${REQ}" -e "'${REQ}'" e2e/ __tests__/ tests/ src/ 2>/dev/null || true; } | wc -l | tr -d ' ')
793
793
 
794
794
  # devaudit-installer#237 — check for test-execution-summary.md
795
795
  # as a secondary signal. This is a human-authored document and
@@ -638,6 +638,7 @@ jobs:
638
638
  permissions:
639
639
  contents: read
640
640
  actions: read
641
+ issues: write
641
642
  env:
642
643
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
643
644
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
@@ -682,7 +683,7 @@ jobs:
682
683
  name: e2e-regression-report
683
684
  path: e2e-artifacts/
684
685
  run-id: ${{ github.event.workflow_run.id }}
685
- github-token: ${{ secrets.GITHUB_TOKEN }}
686
+ github-token: ${{ github.token }}
686
687
  continue-on-error: true
687
688
 
688
689
  - name: Derive release version
@@ -726,8 +727,9 @@ jobs:
726
727
  FLAGS="${FLAGS} --test-cycle ${{ github.event.workflow_run.id }}"
727
728
 
728
729
  # In-scope REQs from pending release tickets at the triggering
729
- # SHA. Fall back to `_compliance-docs` if no tickets present so
730
- # the artefact at least lands somewhere visible.
730
+ # SHA. If the ticket was created after the triggering E2E run,
731
+ # recover REQs from the Playwright JSON artifact before falling
732
+ # back to `_compliance-docs` (DevAudit-Installer#311).
731
733
  REQS=()
732
734
  if [ -d compliance/pending-releases ]; then
733
735
  for TICKET in compliance/pending-releases/RELEASE-TICKET-REQ-*.md; do
@@ -742,6 +744,47 @@ jobs:
742
744
  REQS+=("$REQ_ID")
743
745
  done
744
746
  fi
747
+
748
+ if [ "${#REQS[@]}" -eq 0 ] && [ -f e2e-artifacts/e2e-regression-results.json ]; then
749
+ while IFS= read -r REQ_ID; do
750
+ [ -n "$REQ_ID" ] || continue
751
+ REQS+=("$REQ_ID")
752
+ done < <(python3 - <<'PY'
753
+ import json
754
+ import re
755
+ import sys
756
+
757
+ try:
758
+ with open('e2e-artifacts/e2e-regression-results.json', 'r', encoding='utf-8') as handle:
759
+ data = json.load(handle)
760
+ except Exception:
761
+ sys.exit(0)
762
+
763
+ seen = set()
764
+
765
+ def walk(value):
766
+ if isinstance(value, str):
767
+ for req in re.findall(r'REQ-[0-9]+', value):
768
+ seen.add(req)
769
+ elif isinstance(value, dict):
770
+ for item in value.values():
771
+ walk(item)
772
+ elif isinstance(value, list):
773
+ for item in value:
774
+ walk(item)
775
+
776
+ walk(data)
777
+ for req in sorted(seen):
778
+ print(req)
779
+ PY
780
+ )
781
+ fi
782
+
783
+ if [ "${#REQS[@]}" -eq 1 ] && echo "$DERIVED_RELEASE" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
784
+ echo "E2E upload recovered tracked release ${REQS[0]} from artifact metadata; overriding bare-date release ${DERIVED_RELEASE}."
785
+ DERIVED_RELEASE="${REQS[0]}"
786
+ fi
787
+
745
788
  if [ "${#REQS[@]}" -eq 0 ]; then
746
789
  REQS=(_compliance-docs)
747
790
  fi
@@ -812,7 +855,9 @@ jobs:
812
855
  - name: Triage and file incidents on E2E Regression failure
813
856
  if: steps.resolve.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'failure'
814
857
  env:
815
- GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
858
+ # GitHub issue/label mutations use the workflow token. DevAudit
859
+ # user tokens are portal PATs, not guaranteed GitHub PATs (#305).
860
+ GH_TOKEN: ${{ github.token }}
816
861
  CI_RUN_ID: ${{ github.event.workflow_run.id }}
817
862
  WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}
818
863
  GIT_SHA: ${{ github.event.workflow_run.head_sha }}
@@ -49,7 +49,9 @@ jobs:
49
49
  - uses: actions/checkout@v6
50
50
  with:
51
51
  fetch-depth: 0
52
- token: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
52
+ # GitHub repo mutations use the workflow token. The DevAudit
53
+ # user token is for portal attribution, not GitHub checkout/push.
54
+ token: ${{ github.token }}
53
55
 
54
56
  - name: Classify routing (Path A vs Path B)
55
57
  id: route
@@ -436,7 +438,8 @@ jobs:
436
438
  - name: Direct commit to develop (Path A)
437
439
  if: steps.route.outputs.path == 'A'
438
440
  env:
439
- GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
441
+ # GitHub push uses the workflow token plus job permissions.
442
+ GH_TOKEN: ${{ github.token }}
440
443
  OUT: ${{ steps.paths.outputs.out }}
441
444
  ISSUE_NUMBER: ${{ github.event.issue.number }}
442
445
  ISSUE_URL: ${{ github.event.issue.html_url }}
@@ -454,7 +457,8 @@ jobs:
454
457
  - name: Open export PR (Path B)
455
458
  if: steps.route.outputs.path == 'B'
456
459
  env:
457
- GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
460
+ # GitHub branch push + PR creation use the workflow token.
461
+ GH_TOKEN: ${{ github.token }}
458
462
  BRANCH: ${{ steps.paths.outputs.branch }}
459
463
  OUT: ${{ steps.paths.outputs.out }}
460
464
  ISSUE_NUMBER: ${{ github.event.issue.number }}
@@ -56,7 +56,9 @@ jobs:
56
56
  - name: Enforce label retention
57
57
  if: steps.opt.outputs.enabled == 'true'
58
58
  env:
59
- GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
59
+ # GitHub issue/comment mutations use the workflow token. The
60
+ # DevAudit user token is reserved for portal attribution flows.
61
+ GH_TOKEN: ${{ github.token }}
60
62
  EVENT_NAME: ${{ github.event_name }}
61
63
  EVENT_ACTION: ${{ github.event.action }}
62
64
  ISSUE_NUMBER: ${{ github.event.issue.number }}
@@ -37,8 +37,9 @@ jobs:
37
37
  - uses: actions/checkout@v6
38
38
  with:
39
39
  fetch-depth: 0
40
- # Need write access for the chore branch.
41
- token: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
40
+ # Need write access for the chore branch. GitHub repo mutations
41
+ # use the workflow token; the DevAudit user token is portal auth.
42
+ token: ${{ github.token }}
42
43
 
43
44
  - name: Compute review period
44
45
  id: period
@@ -188,7 +189,8 @@ jobs:
188
189
 
189
190
  - name: Open / update review PR
190
191
  env:
191
- GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
192
+ # GitHub branch push + PR creation use the workflow token.
193
+ GH_TOKEN: ${{ github.token }}
192
194
  BRANCH: ${{ steps.period.outputs.branch }}
193
195
  REVIEW_ID: ${{ steps.period.outputs.review_id }}
194
196
  run: |
@@ -34,6 +34,9 @@ jobs:
34
34
  production-evidence:
35
35
  name: Production Evidence
36
36
  runs-on: {{RUNNER}}
37
+ permissions:
38
+ contents: read
39
+ issues: write
37
40
  env:
38
41
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
39
42
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
@@ -151,7 +154,9 @@ jobs:
151
154
  - name: File incident on smoke failure
152
155
  if: failure()
153
156
  env:
154
- GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
157
+ # GitHub issue/label mutations use the workflow token. DevAudit
158
+ # user tokens are portal PATs, not guaranteed GitHub PATs (#305).
159
+ GH_TOKEN: ${{ github.token }}
155
160
  PROD_URL: ${{ secrets.{{PRODUCTION_URL_SECRET}} }}
156
161
  GIT_SHA: ${{ github.sha }}
157
162
  CI_RUN: ${{ github.run_id }}
@@ -31,7 +31,7 @@ jobs:
31
31
  steps:
32
32
  - name: Verify prior Quality Gates success on head SHA
33
33
  env:
34
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34
+ GH_TOKEN: ${{ github.token }}
35
35
  HEAD_SHA: ${{ github.event.pull_request.head.sha }}
36
36
  CURRENT_RUN_ID: ${{ github.run_id }}
37
37
  run: |
@@ -38,6 +38,9 @@ echo "Pre-push: TypeScript check passed."
38
38
  # Previous code had two `while read` loops; the second consumed nothing
39
39
  # because stdin was already exhausted by the first (devaudit-installer#278).
40
40
  PUSH_REFS="$(cat)"
41
+ PUSH_REFS_FILE=$(mktemp)
42
+ trap 'rm -f "$PUSH_REFS_FILE"' EXIT HUP INT TERM
43
+ printf '%s\n' "$PUSH_REFS" > "$PUSH_REFS_FILE"
41
44
 
42
45
  INTEGRATION_BRANCH=$(jq -r '.integration_branch // "develop"' sdlc-config.json 2>/dev/null || echo "develop")
43
46
 
@@ -49,7 +52,8 @@ INTEGRATION_BRANCH=$(jq -r '.integration_branch // "develop"' sdlc-config.json 2
49
52
  # (written by e2e-test-engineer Phase 5½ after validating tagTest/evidenceShot).
50
53
  # Scoped to integration-branch pushes (correct for the normal workflow).
51
54
 
52
- echo "$PUSH_REFS" | while read -r local_ref local_sha remote_ref remote_sha; do
55
+ while read -r local_ref local_sha remote_ref remote_sha; do
56
+ [ -n "${local_ref:-}" ] || continue
53
57
  # Check if pushing to the integration branch
54
58
  case "$remote_ref" in
55
59
  *"refs/heads/$INTEGRATION_BRANCH") ;;
@@ -103,7 +107,7 @@ echo "$PUSH_REFS" | while read -r local_ref local_sha remote_ref remote_sha; do
103
107
  fi
104
108
  echo "Pre-push: E2E evidence wiring check passed."
105
109
  fi
106
- done
110
+ done < "$PUSH_REFS_FILE"
107
111
 
108
112
  # ── 3. Skill-invocation sentinel (devaudit-installer#226) ────────────
109
113
  # If feat/fix/refactor/perf commits are in the push, verify the
@@ -115,11 +119,9 @@ done
115
119
  TRACKED_TYPES='^(feat|fix|refactor|perf)(\(.+\))?!?:'
116
120
  HAS_TRACKED=false
117
121
 
118
- # Parse stdin refs in the current shell (not a subshell) so HAS_TRACKED propagates.
119
- for ref_line in $PUSH_REFS; do
120
- set -- $ref_line
121
- local_sha="$2"
122
- remote_sha="$4"
122
+ # Parse captured refs in the current shell so HAS_TRACKED propagates.
123
+ while read -r local_ref local_sha remote_ref remote_sha; do
124
+ [ -n "${local_ref:-}" ] || continue
123
125
  if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
124
126
  RANGE="$local_sha"
125
127
  else
@@ -129,7 +131,7 @@ for ref_line in $PUSH_REFS; do
129
131
  if echo "$COMMITS" | grep -qE "$TRACKED_TYPES"; then
130
132
  HAS_TRACKED=true
131
133
  fi
132
- done
134
+ done < "$PUSH_REFS_FILE"
133
135
 
134
136
  if [ "$HAS_TRACKED" = "true" ]; then
135
137
  if [ ! -f .sdlc-implementer-invoked ]; then
@@ -147,10 +149,8 @@ if [ "$HAS_TRACKED" = "true" ]; then
147
149
  # If the commit touches Phase 3 artifacts (compliance/evidence/REQ-XXX/),
148
150
  # the sentinel must contain a phase "3" record. If it touches Phase 5
149
151
  # close-out (approved-releases/ or RTM RELEASED), sentinel must have phase "5".
150
- for ref_line in $PUSH_REFS; do
151
- set -- $ref_line
152
- local_sha="$2"
153
- remote_sha="$4"
152
+ while read -r local_ref local_sha remote_ref remote_sha; do
153
+ [ -n "${local_ref:-}" ] || continue
154
154
  if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
155
155
  RANGE="$local_sha"
156
156
  else
@@ -181,7 +181,7 @@ if [ "$HAS_TRACKED" = "true" ]; then
181
181
  exit 1
182
182
  fi
183
183
  fi
184
- done
184
+ done < "$PUSH_REFS_FILE"
185
185
  echo "Pre-push: phase-progression check passed."
186
186
  fi
187
187
 
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "DevAudit SDLC CLI Engine — cross-agent terminal-driven SDLC orchestrator.",
5
5
  "bin": {
6
6
  "devaudit-sdlc": "./src/bin/devaudit-sdlc.js"