2323 run : pip install --quiet git+https://github.com/profullstack/vu1nz-gh-actions.git
2424
2525 - name : Review PR
26+ id : review
2627 env :
2728 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
2829 ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
@@ -32,5 +33,144 @@ jobs:
3233 vu1nz review-pr main \
3334 ${{ github.repository }} \
3435 ${{ github.event.pull_request.number }} \
35- --ai auto \
36- --token "$GITHUB_TOKEN"
36+ --token "$GITHUB_TOKEN" \
37+ --json \
38+ | tee "$RUNNER_TEMP/vu1nz-review-raw.txt" || true
39+
40+ # Extract JSON from output (vu1nz prints progress lines before JSON)
41+ python3 -c "
42+ import json, re, sys
43+ raw = open('$RUNNER_TEMP/vu1nz-review-raw.txt').read()
44+ raw = re.sub(r'\x1b\[[0-9;]*m', '', raw)
45+ start = raw.find('{')
46+ if start >= 0:
47+ obj, _ = json.JSONDecoder(strict=False).raw_decode(raw, start)
48+ json.dump(obj, sys.stdout)
49+ else:
50+ print('{}')
51+ " > "$RUNNER_TEMP/vu1nz-review.json"
52+
53+ - name : Build PR comment
54+ id : comment
55+ run : |
56+ python3 << 'PYEOF'
57+ import json, os, sys
58+
59+ review_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-review.json"
60+ comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md"
61+
62+ try:
63+ with open(review_file) as f:
64+ data = json.loads(f.read(), strict=False)
65+ except Exception as e:
66+ print(f"::warning::Could not parse review results: {e}")
67+ with open(comment_file, "w") as f:
68+ f.write("## vu1nz Security Review\n\nCould not parse review results.\n")
69+ sys.exit(0)
70+
71+ findings = data.get("findings", [])
72+ analysis = data.get("analysis", "")
73+ pr = data.get("pr_number", "?")
74+ total = len(findings)
75+
76+ counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
77+ for finding in findings:
78+ sev = finding.get("severity", "").lower()
79+ if sev in counts:
80+ counts[sev] += 1
81+
82+ has_hc = counts["critical"] > 0 or counts["high"] > 0
83+
84+ lines = ["## vu1nz Security Review", ""]
85+ lines.append(f"**{total}** finding(s) in PR #{pr}")
86+ lines.append("")
87+
88+ badge_parts = []
89+ for sev in ("critical", "high", "medium", "low"):
90+ if counts[sev] > 0:
91+ badge_parts.append(f"**{sev.upper()}**: {counts[sev]}")
92+ if badge_parts:
93+ lines.append(" | ".join(badge_parts))
94+ lines.append("")
95+
96+ if has_hc:
97+ lines.append("> **High or critical findings — review before merging.**")
98+ lines.append("")
99+
100+ if findings:
101+ lines.append("### Findings")
102+ lines.append("")
103+ lines.append("| Severity | File | Issue | Suggestion |")
104+ lines.append("|----------|------|-------|------------|")
105+ for f in findings:
106+ sev = f.get("severity", "?").upper()
107+ file = f.get("file", "N/A")
108+ issue = f.get("issue", "").replace("\n", " ")[:150]
109+ suggestion = f.get("suggestion", "").replace("\n", " ")[:150]
110+ lines.append(f"| {sev} | `{file}` | {issue} | {suggestion} |")
111+ lines.append("")
112+ else:
113+ lines.append("No security issues found.")
114+ lines.append("")
115+
116+ if analysis:
117+ lines.append("<details><summary>Full AI Analysis</summary>")
118+ lines.append("")
119+ lines.append(analysis)
120+ lines.append("")
121+ lines.append("</details>")
122+
123+ body = "\n".join(lines)
124+ with open(comment_file, "w") as f:
125+ f.write(body)
126+
127+ with open(os.environ.get("GITHUB_OUTPUT", ""), "a") as out:
128+ out.write(f"total={total}\n")
129+ out.write(f"has_high_critical={'true' if has_hc else 'false'}\n")
130+
131+ if has_hc:
132+ print(f"::error::vu1nz found high/critical vulnerabilities in PR code")
133+ sys.exit(1)
134+
135+ print(f"::notice::vu1nz review: {total} finding(s), no high/critical issues")
136+ PYEOF
137+
138+ - name : Comment on PR
139+ if : always()
140+ uses : actions/github-script@v7
141+ with :
142+ script : |
143+ const fs = require('fs');
144+ const commentFile = `${process.env.RUNNER_TEMP}/vu1nz-comment.md`;
145+ let body;
146+ try {
147+ body = fs.readFileSync(commentFile, 'utf8');
148+ } catch {
149+ body = '## vu1nz Security Review\n\nScan completed but could not read results.';
150+ }
151+
152+ const { data: comments } = await github.rest.issues.listComments({
153+ issue_number: context.issue.number,
154+ owner: context.repo.owner,
155+ repo: context.repo.repo,
156+ });
157+
158+ const existing = comments.find(c =>
159+ c.user.type === 'Bot' && c.body.includes('vu1nz Security Review')
160+ );
161+
162+ if (existing) {
163+ await github.rest.issues.updateComment({
164+ comment_id: existing.id,
165+ owner: context.repo.owner,
166+ repo: context.repo.repo,
167+ body: body,
168+ });
169+ } else {
170+ await github.rest.issues.createComment({
171+ issue_number: context.issue.number,
172+ owner: context.repo.owner,
173+ repo: context.repo.repo,
174+ body: body,
175+ });
176+ }
0 commit comments