11name : DiffScope Review
22on :
33 pull_request :
4- types : [opened, synchronize, reopened]
4+ types : [opened, synchronize, reopened, review_requested ]
55
66permissions :
77 contents : read
@@ -20,61 +20,109 @@ jobs:
2020 with :
2121 fetch-depth : 0
2222 ref : ${{ github.event.pull_request.head.sha }}
23-
23+
24+ - uses : actions/setup-node@v5
25+ with :
26+ node-version : 24
27+ cache : npm
28+ cache-dependency-path : web/package-lock.json
29+
30+ - uses : dtolnay/rust-toolchain@1.88.0
31+ - uses : Swatinem/rust-cache@v2
32+
33+ - name : Build frontend
34+ working-directory : web
35+ run : |
36+ npm ci
37+ npm run build
38+
39+ - name : Build current DiffScope binary
40+ run : cargo build --release
41+
2442 - name : Get PR diff
25- id : diff
2643 run : |
2744 git fetch origin ${{ github.base_ref }} --depth=1
2845 git diff origin/${{ github.base_ref }}...HEAD > pr.diff
29-
30- - name : Check API key
31- id : check_key
46+
47+ - name : Select review provider
48+ id : provider
49+ env :
50+ ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
51+ OPENROUTER_API_KEY : ${{ secrets.OPENROUTER_API_KEY }}
52+ OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
3253 run : |
33- if [ -z "${{ secrets.OPENAI_API_KEY }}" ]; then
34- echo "skip=true" >> "$GITHUB_OUTPUT"
35- echo "::notice::DiffScope review skipped — OPENAI_API_KEY secret not configured"
54+ if [ -n "${ANTHROPIC_API_KEY}" ]; then
55+ {
56+ echo "configured=true"
57+ echo "model=anthropic/claude-opus-4.5"
58+ echo "adapter=anthropic"
59+ } >> "$GITHUB_OUTPUT"
60+ elif [ -n "${OPENROUTER_API_KEY}" ]; then
61+ {
62+ echo "configured=true"
63+ echo "model=anthropic/claude-opus-4.5"
64+ echo "adapter=openrouter"
65+ } >> "$GITHUB_OUTPUT"
66+ elif [ -n "${OPENAI_API_KEY}" ]; then
67+ {
68+ echo "configured=true"
69+ echo "model=gpt-4o"
70+ echo "adapter=openai"
71+ } >> "$GITHUB_OUTPUT"
3672 else
37- echo "skip=false" >> "$GITHUB_OUTPUT"
73+ echo "configured=false" >> "$GITHUB_OUTPUT"
74+ echo "::notice::DiffScope review skipped; no ANTHROPIC_API_KEY, OPENROUTER_API_KEY, or OPENAI_API_KEY secret is configured"
3875 fi
3976
40- - name : Check image available
41- id : check_image
77+ - name : Run DiffScope from this branch
78+ if : steps.provider.outputs.configured == 'true'
79+ env :
80+ ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
81+ OPENROUTER_API_KEY : ${{ secrets.OPENROUTER_API_KEY }}
82+ OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
4283 run : |
43- docker pull ghcr.io/evalops/ diffscope:latest 2>/dev/null && echo "available=true" >> "$GITHUB_OUTPUT" || echo "available=false" >> "$GITHUB_OUTPUT"
44-
45- - name : Notice image unavailable
46- if : steps.check_image.outputs.available == 'false'
47- run : echo "::notice::DiffScope review skipped — image ghcr.io/evalops/diffscope:latest not available (merge to main publishes it)"
84+ ./target/release/ diffscope \
85+ --model "${{ steps.provider.outputs.model }}" \
86+ --adapter "${{ steps.provider.outputs.adapter }}" \
87+ --output-format json \
88+ review --diff pr.diff --output comments.json
4889
49- - name : Run DiffScope
50- if : steps.check_key.outputs.skip != 'true' && steps.check_image.outputs.available == 'true'
51- run : |
52- docker run --rm \
53- -e OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
54- -v "$PWD":/workspace -w /workspace \
55- ghcr.io/evalops/diffscope:latest \
56- review --diff pr.diff --output-format json --output comments.json
90+ - name : Upload review artifact
91+ if : always() && steps.provider.outputs.configured == 'true'
92+ uses : actions/upload-artifact@v4
93+ with :
94+ name : diffscope-review-${{ github.event.pull_request.number }}
95+ path : comments.json
96+ if-no-files-found : ignore
5797
5898 - name : Post comments
59- if : steps.check_key .outputs.skip != 'true' && steps.check_image.outputs.available == 'true'
99+ if : steps.provider .outputs.configured == 'true'
60100 uses : actions/github-script@v7
61101 with :
62102 script : |
63103 const fs = require('fs');
104+ if (!fs.existsSync('comments.json')) {
105+ core.notice('DiffScope did not produce comments.json');
106+ return;
107+ }
64108 const comments = JSON.parse(fs.readFileSync('comments.json', 'utf8'));
65109 const headSha = context.payload.pull_request.head.sha;
66110 const fallback = [];
67-
111+
68112 for (const comment of comments) {
69113 if (!comment.file_path || !comment.line_number || comment.line_number < 1) {
70114 continue;
71115 }
116+ const body = [
117+ `**${comment.severity}**: ${comment.content}`,
118+ comment.suggestion ? `\nSuggestion: ${comment.suggestion}` : ''
119+ ].join('');
72120 try {
73121 await github.rest.pulls.createReviewComment({
74122 owner: context.repo.owner,
75123 repo: context.repo.repo,
76124 pull_number: context.issue.number,
77- body: `**${comment.severity}**: ${comment.content}` ,
125+ body,
78126 commit_id: headSha,
79127 path: comment.file_path,
80128 line: comment.line_number,
90138 "Some review comments could not be placed inline and are listed below:",
91139 "",
92140 ...fallback.slice(0, 100)
93- ].join("\\ n");
141+ ].join("\n");
94142 await github.rest.issues.createComment({
95143 owner: context.repo.owner,
96144 repo: context.repo.repo,
0 commit comments