-
Notifications
You must be signed in to change notification settings - Fork 2.3k
248 lines (214 loc) · 10.4 KB
/
Copy pathrepo--deepseek-review.yml
File metadata and controls
248 lines (214 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
name: DeepSeek PR Review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
jobs:
deepseek-review:
# Run automatically on non-draft PRs from the base repo, or when a trusted
# author comments @deepseek (which also covers fork PRs after maintainer review).
# SECURITY: both paths are gated so anonymous fork PRs cannot burn
# DEEPSEEK_API_KEY budget by opening/synchronizing PRs or spamming comments.
if: >-
(
github.event_name == 'pull_request'
&& github.event.pull_request.draft == false
&& github.event.pull_request.head.repo.fork == false
&& !startsWith(github.head_ref, 'release-please')
&& !startsWith(github.head_ref, 'dependabot')
)
|| (
github.event_name == 'issue_comment'
&& contains(github.event.comment.body, '@deepseek')
&& github.event.issue.pull_request != null
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
# For issue_comment events on a PR, github.event.issue.number IS the PR
# number (GitHub shares one numbering space for issues and PRs), so no
# extra `gh pr view` lookup is needed.
- name: Resolve PR number
id: pr
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
else
echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
fi
- name: Run DeepSeek Code Review
id: review
uses: actions/github-script@v8
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
DEEPSEEK_MODEL: ${{ vars.DEEPSEEK_MODEL }}
DEEPSEEK_MAX_TOKENS: ${{ vars.DEEPSEEK_MAX_TOKENS }}
DEEPSEEK_MAX_DIFF: ${{ vars.DEEPSEEK_MAX_DIFF }}
with:
script: |
const prNumber = parseInt('${{ steps.pr.outputs.number }}', 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
// Parse a positive integer from a repo variable, falling back to a
// default when unset or non-numeric (e.g. "64k") so a misconfigured
// variable can't serialize to null and break every review run.
const intFromEnv = (name, fallback) => {
const n = parseInt(process.env[name] || '', 10);
return Number.isFinite(n) && n > 0 ? n : fallback;
};
// Fetch PR details
const pr = await github.rest.pulls.get({
owner, repo, pull_number: prNumber
});
const title = pr.data.title;
const body = pr.data.body || '';
// Get the diff as raw text
const diffResponse = await github.request({
method: 'GET',
url: `/repos/${owner}/${repo}/pulls/${prNumber}`,
headers: { Accept: 'application/vnd.github.v3.diff' }
});
const diffText = typeof diffResponse.data === 'string'
? diffResponse.data
: JSON.stringify(diffResponse.data);
if (!diffText || diffText.length === 0) {
console.log('No diff found - skipping review');
return 'No diff to review.';
}
// Truncate diff if too large. deepseek-v4-pro has a large context
// window, so default high and allow overriding via repo variable.
const MAX_DIFF = intFromEnv('DEEPSEEK_MAX_DIFF', 2000000);
const truncatedDiff = diffText.length > MAX_DIFF
? diffText.substring(0, MAX_DIFF) + '\n\n... (diff truncated)'
: diffText;
// Build the prompt
const systemPrompt = `You are a senior software engineer performing a code review on the Taiko monorepo.
Taiko is a based rollup on Ethereum (type-1 ZK-EVM). The repo contains:
- Smart contracts (Solidity/Foundry) in packages/protocol
- Go services (taiko-client, relayer, eventindexer)
- Rust services (taiko-client-rs)
- Frontend apps (SvelteKit, TypeScript)
Provide a CONCISE code review in the following format. Skip sections that don't apply:
### 🔴 Critical Issues
Issues that could lead to security vulnerabilities, fund loss, or chain halts.
### 🟡 Warnings
Logic errors, edge cases, or potential bugs that should be addressed.
### 🔵 Suggestions
Performance improvements, style/convention fixes, better error handling.
### 🟢 What Looks Good
No need to comment on things that are already clear or don't need review.
### Review Guidelines
- **Solidity**: Check access control, reentrancy, overflow, storage layout, gas optimization, NatSpec docs
- **Go/Rust**: Check error handling, race conditions, resource leaks, proper use of contexts
- **TypeScript**: Check type safety, XSS, unsafe API calls
- Be direct and constructive. Don't be overly polite.
- If there are no issues in a category, skip it entirely.`;
const userPrompt = `## PR #${prNumber}: ${title}
**Description:**
${body || 'No description provided.'}
**Diff:**
\`\`\`diff
${truncatedDiff}
\`\`\`
Review this PR thoroughly. Be direct - flag only real issues.`;
// Model + token budget are configurable via repo variables.
// NOTE: deepseek-v4-pro is a reasoning model — reasoning tokens
// count against max_tokens, so a small budget (e.g. 4096) can be
// fully consumed before any visible content is produced, yielding
// an empty review. Default high (65536) to leave room for output.
const model = process.env.DEEPSEEK_MODEL || 'deepseek-v4-pro';
const maxTokens = intFromEnv('DEEPSEEK_MAX_TOKENS', 8192 * 8);
// Call DeepSeek API (OpenAI-compatible endpoint).
async function requestReview() {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`
},
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
max_tokens: maxTokens,
temperature: 0.3
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(`DeepSeek API error (${response.status}): ${JSON.stringify(data)}`);
}
const choice = data.choices && data.choices[0];
return {
reviewText: ((choice && choice.message && choice.message.content) || '').trim(),
finishReason: (choice && choice.finish_reason) || 'unknown',
usage: data.usage ? JSON.stringify(data.usage) : 'unknown'
};
}
// Empty content usually means the token budget was exhausted by
// reasoning or the model id is invalid/aliased. Retry once before
// surfacing a diagnostic instead of posting a blank review.
let { reviewText, finishReason, usage } = await requestReview();
if (!reviewText) {
core.warning(
`DeepSeek returned no content (model=${model}, finish_reason=${finishReason}, usage=${usage}); retrying once.`
);
({ reviewText, finishReason, usage } = await requestReview());
}
// Build comment body
const triggerInfo = context.eventName === 'pull_request'
? 'Automatically triggered on PR update'
: 'Triggered by `@deepseek` comment';
const footer = `*${triggerInfo} • model: \`${model}\`*`;
const commentBody = reviewText
? `## 🐋 DeepSeek Code Review\n\n${reviewText}\n\n---\n${footer}`
: `## 🐋 DeepSeek Code Review\n\n⚠️ The model returned no review content after a retry `
+ `(model: \`${model}\`, finish_reason: \`${finishReason}\`, usage: \`${usage}\`). `
+ `This usually means the configured model id is invalid/aliased or the token budget was `
+ `exhausted before any visible output. Override via the \`DEEPSEEK_MODEL\` / `
+ `\`DEEPSEEK_MAX_TOKENS\` repo variables.\n\n---\n${footer}`;
// Check for existing bot comment to update (sticky behavior)
const comments = await github.rest.issues.listComments({
owner, repo,
issue_number: prNumber,
per_page: 100
});
const existingComment = comments.data.find(c =>
c.body.includes('🐋 DeepSeek Code Review') &&
c.user.login === 'github-actions[bot]'
);
if (existingComment) {
await github.rest.issues.updateComment({
owner, repo,
comment_id: existingComment.id,
body: commentBody
});
console.log(`✅ Updated existing review comment #${existingComment.id}`);
} else {
await github.rest.issues.createComment({
owner, repo,
issue_number: prNumber,
body: commentBody
});
console.log('✅ Posted new review comment');
}
return { success: !!reviewText, reviewLength: reviewText.length, finishReason };