Skip to content

Commit 116d823

Browse files
anakrishCopilot
andcommitted
refactor: extract review logic into standalone script
Move all review logic from inline YAML to .github/scripts/perspective-review.sh. Workflow YAML is now minimal — just triggers, permissions, and script invocation. This fixes YAML parsing issues with complex heredocs and inline bash. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7de4177 commit 116d823

2 files changed

Lines changed: 214 additions & 244 deletions

File tree

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License.
4+
#
5+
# Multi-perspective PR review using GitHub Models API.
6+
# Called by the perspective-review.yml workflow.
7+
#
8+
# Usage: perspective-review.sh <repo> <pr_number>
9+
# Requires: GITHUB_TOKEN env var, jq, gh CLI
10+
11+
set -euo pipefail
12+
13+
REPO="$1"
14+
PR_NUMBER="$2"
15+
16+
echo "=== Perspective Review for ${REPO}#${PR_NUMBER} ==="
17+
18+
# Step 1: Get changed files
19+
echo "Fetching changed files..."
20+
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" \
21+
--jq '.[].filename' > /tmp/changed_files.txt
22+
echo "Changed files: $(wc -l < /tmp/changed_files.txt)"
23+
24+
# Step 2: Get the diff (truncate to ~60KB for token limits)
25+
echo "Fetching diff..."
26+
gh api "repos/${REPO}/pulls/${PR_NUMBER}" \
27+
-H "Accept: application/vnd.github.v3.diff" \
28+
| head -c 60000 > /tmp/pr_diff.txt
29+
echo "Diff size: $(wc -c < /tmp/pr_diff.txt) bytes"
30+
31+
# Step 3: Select perspectives based on changed paths
32+
PERSPECTIVES="reliability-engineer,test-engineer"
33+
34+
if grep -qE 'src/builtins/' /tmp/changed_files.txt 2>/dev/null; then
35+
PERSPECTIVES="${PERSPECTIVES},semantics-expert,red-teamer"
36+
fi
37+
if grep -qE 'src/(value|number)' /tmp/changed_files.txt 2>/dev/null; then
38+
PERSPECTIVES="${PERSPECTIVES},semantics-expert"
39+
fi
40+
if grep -qE 'bindings/|src/.*ffi' /tmp/changed_files.txt 2>/dev/null; then
41+
PERSPECTIVES="${PERSPECTIVES},architect,api-steward"
42+
fi
43+
if grep -qE 'Cargo\.(toml|lock)' /tmp/changed_files.txt 2>/dev/null; then
44+
PERSPECTIVES="${PERSPECTIVES},security-auditor,architect"
45+
fi
46+
if grep -qE 'src/(interpreter|rvm|compiler|scheduler)' /tmp/changed_files.txt 2>/dev/null; then
47+
PERSPECTIVES="${PERSPECTIVES},semantics-expert,performance-engineer"
48+
fi
49+
50+
# Deduplicate
51+
PERSPECTIVES=$(echo "$PERSPECTIVES" | tr ',' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
52+
echo "Selected perspectives: ${PERSPECTIVES}"
53+
54+
# Step 4: Build context
55+
echo "Building context..."
56+
KNOWLEDGE_CTX=""
57+
for f in builtin-system value-semantics policy-evaluation-security ffi-boundary; do
58+
kf="docs/knowledge/${f}.md"
59+
if [ -f "$kf" ]; then
60+
KNOWLEDGE_CTX="${KNOWLEDGE_CTX}
61+
--- ${f}.md ---
62+
$(head -c 4000 "$kf")
63+
"
64+
fi
65+
done
66+
67+
DIFF_CONTENT=$(cat /tmp/pr_diff.txt)
68+
69+
# Step 5: Run each perspective
70+
ALL_FINDINGS=""
71+
72+
for perspective in $(echo "$PERSPECTIVES" | tr ',' ' '); do
73+
echo "--- Reviewing: ${perspective} ---"
74+
75+
# Load agent instructions
76+
AGENT_INSTRUCTIONS=""
77+
agent_file=".github/agents/${perspective}.agent.md"
78+
if [ -f "$agent_file" ]; then
79+
AGENT_INSTRUCTIONS=$(head -c 4000 "$agent_file")
80+
fi
81+
82+
# Format display name
83+
DISPLAY_NAME=$(echo "$perspective" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
84+
85+
# Build prompt as a temp file to avoid heredoc/quoting issues
86+
cat > /tmp/review_prompt.txt <<EOF
87+
You are reviewing a pull request from the perspective of a ${DISPLAY_NAME}.
88+
89+
Your agent instructions:
90+
${AGENT_INSTRUCTIONS}
91+
92+
Relevant knowledge context:
93+
${KNOWLEDGE_CTX}
94+
95+
PR Diff:
96+
${DIFF_CONTENT}
97+
98+
Respond with a JSON array of findings. Each finding must have these fields:
99+
- "severity": one of "critical", "important", "suggestion"
100+
- "summary": a single sentence suitable as a GitHub issue title
101+
- "file": the file path (from the diff)
102+
- "line": approximate line number (from the diff), or null
103+
- "explanation": 2-4 sentences explaining the issue
104+
105+
If you find no issues from this perspective, return an empty array: []
106+
107+
Return ONLY valid JSON — no markdown fences, no extra text.
108+
EOF
109+
110+
PROMPT_CONTENT=$(cat /tmp/review_prompt.txt)
111+
112+
# Call GitHub Models API using jq for safe JSON encoding
113+
RESPONSE=$(jq -n \
114+
--arg model "openai/gpt-4o-mini" \
115+
--arg prompt "$PROMPT_CONTENT" \
116+
'{
117+
model: $model,
118+
messages: [
119+
{role: "system", content: "You are a code reviewer. Return findings as a JSON array only."},
120+
{role: "user", content: $prompt}
121+
],
122+
temperature: 0.1
123+
}' | curl -s -X POST "https://models.github.ai/inference/chat/completions" \
124+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
125+
-H "Content-Type: application/json" \
126+
-d @- 2>/dev/null || echo '{"error": "API call failed"}')
127+
128+
# Extract content
129+
CONTENT=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty' 2>/dev/null || true)
130+
131+
if [ -z "$CONTENT" ]; then
132+
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error // .message // "Unknown error"' 2>/dev/null || echo "Unknown error")
133+
echo " API error: ${ERROR_MSG}"
134+
continue
135+
fi
136+
137+
# Try to parse as JSON array and render as tagged markdown
138+
RENDERED=$(echo "$CONTENT" | jq -r --arg perspective "$DISPLAY_NAME" '
139+
if type == "array" then
140+
.[] |
141+
"**[\($perspective)]** " +
142+
(if .severity == "critical" then "🔴 critical" elif .severity == "important" then "🟠 important" else "🔵 suggestion" end) +
143+
"\n> " + .summary + "\n\n" + .explanation +
144+
(if .file then "\n\n📁 `" + .file + "`" + (if .line then ":" + (.line | tostring) else "" end) else "" end) +
145+
"\n\n---\n"
146+
else
147+
empty
148+
end
149+
' 2>/dev/null || true)
150+
151+
if [ -n "$RENDERED" ]; then
152+
FINDING_COUNT=$(echo "$CONTENT" | jq 'if type == "array" then length else 0 end' 2>/dev/null || echo 0)
153+
echo " Found ${FINDING_COUNT} findings"
154+
ALL_FINDINGS="${ALL_FINDINGS}${RENDERED}"
155+
else
156+
echo " No findings (or unparseable response)"
157+
fi
158+
done
159+
160+
# Step 6: Build the final comment
161+
MARKER="<!-- perspective-review-bot -->"
162+
163+
if [ -n "$ALL_FINDINGS" ]; then
164+
cat > /tmp/review_comment.md <<EOF
165+
${MARKER}
166+
## 🔍 Perspective Review
167+
168+
Automated multi-perspective review of this PR.
169+
Each finding is tagged with the perspective that identified it.
170+
171+
---
172+
173+
$(echo -e "$ALL_FINDINGS")
174+
175+
<sub>Generated by perspective-review workflow • Perspectives: ${PERSPECTIVES}</sub>
176+
EOF
177+
else
178+
cat > /tmp/review_comment.md <<EOF
179+
${MARKER}
180+
## 🔍 Perspective Review
181+
182+
✅ No significant findings from the selected perspectives.
183+
184+
<sub>Generated by perspective-review workflow • Perspectives: ${PERSPECTIVES}</sub>
185+
EOF
186+
fi
187+
188+
# Step 7: Upsert the comment (update existing or create new)
189+
echo "Posting review comment..."
190+
EXISTING_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
191+
--jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" \
192+
| head -1 || true)
193+
194+
COMMENT_BODY=$(cat /tmp/review_comment.md)
195+
196+
if [ -n "$EXISTING_ID" ]; then
197+
gh api "repos/${REPO}/issues/comments/${EXISTING_ID}" \
198+
-X PATCH \
199+
-f body="$COMMENT_BODY"
200+
echo "Updated existing comment ${EXISTING_ID}"
201+
else
202+
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
203+
-f body="$COMMENT_BODY"
204+
echo "Created new review comment"
205+
fi
206+
207+
echo "=== Review complete ==="

0 commit comments

Comments
 (0)