1- name : AI PR Review
1+ name : AI Code Review
22
33on :
44 pull_request :
5- types : [opened, synchronize, reopened ]
5+ types : [opened, synchronize]
66
77jobs :
88 ai-review :
99 runs-on : ubuntu-latest
10- if : github.actor != 'dependabot[bot]'
1110 permissions :
1211 contents : read
1312 pull-requests : write
14- issues : write
1513
1614 steps :
17- - name : Checkout code
18- uses : actions/checkout@v4
19- with :
20- fetch-depth : 0
21-
22- - name : AI Code Review
23- uses : actions/github-script@v7
24- with :
25- script : |
26- const OPENROUTER_API_KEY = '${{ secrets.OPENROUTER_API_KEY }}';
27-
28- const diff = await github.rest.repos.compareCommits({
29- owner: context.repo.owner,
30- repo: context.repo.repo,
31- base: context.payload.pull_request.base.sha,
32- head: context.payload.pull_request.head.sha
33- });
34-
35- const filesToReview = diff.data.files.filter(file =>
36- file.patch &&
37- !file.filename.includes('test/') &&
38- !file.filename.includes('build/') &&
39- (file.filename.endsWith('.kt') ||
40- file.filename.endsWith('.kts') ||
41- file.filename.endsWith('.java') ||
42- file.filename.endsWith('.yml') ||
43- file.filename.endsWith('.yaml') ||
44- file.filename.endsWith('.md'))
45- );
46-
47- if (filesToReview.length === 0) {
48- console.log('No files to review');
49- return;
50- }
51-
52- console.log(`Found ${filesToReview.length} files to review`);
53-
54- let overallReview = `## 🤖 AI 코드 리뷰\n\n`;
55-
56- let allChanges = '';
57- const filesSummary = [];
58-
59- for (const file of filesToReview) {
60- filesSummary.push(`- ${file.filename} (+${file.additions}/-${file.deletions})`);
61- allChanges += `\n### ${file.filename}\n`;
62- allChanges += `\`\`\`diff\n${file.patch}\n\`\`\`\n`;
63- }
64-
65- try {
66- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
67- method: 'POST',
68- headers: {
69- 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
70- 'Content-Type': 'application/json'
71- },
72- body: JSON.stringify({
73- model: 'x-ai/grok-4-fast:free',
74- messages: [{
75- role: 'system',
76- content: `코드 리뷰어입니다. 다음 규칙만 검토하세요:
15+ - uses : actions/checkout@v4
16+ with :
17+ fetch-depth : 0
18+
19+ - uses : actions/github-script@v7
20+ env :
21+ OPENROUTER_API_KEY : ${{ secrets.OPENROUTER_API_KEY }}
22+ with :
23+ script : |
24+ const diff = await github.rest.repos.compareCommits({
25+ owner: context.repo.owner,
26+ repo: context.repo.repo,
27+ base: context.payload.pull_request.base.sha,
28+ head: context.payload.pull_request.head.sha
29+ });
7730
78- # # 검토 항목
31+ const kotlinFiles = diff.data.files.filter(file =>
32+ (file.filename.endsWith('.kt') || file.filename.endsWith('.kts')) && file.patch
33+ );
34+
35+ if (kotlinFiles.length === 0) return;
36+
37+ for (const file of kotlinFiles.slice(0, 3)) {
38+ const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
39+ method: 'POST',
40+ headers: {
41+ 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
42+ 'Content-Type': 'application/json'
43+ },
44+ body: JSON.stringify({
45+ model: 'x-ai/grok-4-fast:free',
46+ messages: [{
47+ role: 'system',
48+ content: `코드 리뷰어입니다. 다음 규칙을 검토하세요:
49+
50+ # # 필수 검토 항목
79511. **글로벌 익셉션 처리** : @ControllerAdvice 사용, 표준 에러 응답
80522. **ApiResponse 사용** : 모든 API는 ApiResponse<T>로 감싸서 응답
81533. **Kotlin 최적화** : data class, null safety, when 표현식, 확장함수 등
82544. **ktlint 규칙** : 포맷팅, 네이밍 컨벤션
8355
8456# # 응답 형식
85- # ## ✅ 준수사항
86- - [잘 지켜진 부분]
87-
88- # ## ❌ 위반사항
89- - [파일:라인] 문제점과 수정방법
90-
91- **점수**: X/10`
92- }, {
93- role : ' user' ,
94- content : ` 다음 PR의 변경사항을 리뷰해주세요:
95-
96- PR 제목: ${{ github.event.pull_request.title }}
97- PR 설명: ${{ github.event.pull_request.body }}
98-
99- 변경된 파일:
100- ${filesSummary.join('\n ')}
101-
102- 전체 변경사항:
103- ${allChanges}`
104- }],
105- temperature : 0.3
106- })
107- });
108-
109- if (response.ok) {
110- const result = await response.json();
111- overallReview += result.choices[0].message.content + '\n\n';
112- console.log('✅ API review completed successfully!');
113- } else {
114- const errorText = await response.text();
115- console.error('API request failed:', response.status, errorText);
116- overallReview += `> ⚠️ 리뷰 실패 : ${response.status} - ${errorText}\n\n`;
117- }
118- } catch (error) {
119- console.error('Error during review:', error);
120- overallReview += `> ⚠️ 리뷰 실패 : ${error.message}\n\n`;
121- }
122-
123- overallReview += `---\n`;
124- overallReview += `<details>\n<summary>💡 리뷰 정보</summary>\n\n`;
125- overallReview += `- 모델 : Grok-4-fast\n`;
126- overallReview += `- 리뷰 시간 : ${new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'})}\n`;
127- overallReview += `- PR : # ${{ github.event.number }}\n\n`;
128- overallReview += `</details>`;
129-
130- await github.rest.pulls.createReview({
131- owner : context.repo.owner,
132- repo : context.repo.repo,
133- pull_number : context.payload.pull_request.number,
134- body : overallReview,
135- event : ' COMMENT'
136- });
137-
138- console.log(`✅ Review completed for ${filesToReview.length} files!`);
57+ 🟢 **좋은점** : [규칙을 잘 지킨 부분]
58+ 🟡 **개선사항** : [더 좋게 할 수 있는 부분]
59+ 🔴 **문제점** : [반드시 수정해야 할 부분]`
60+ }, {
61+ role : ' user' ,
62+ content : ` 파일: ${file.filename}\n\n 변경사항:\n ${file.patch}`
63+ }],
64+ max_tokens : 500,
65+ temperature : 0
66+ })
67+ });
68+
69+ if (response.ok) {
70+ const result = await response.json();
71+ const review = result.choices[0].message.content;
72+
73+ await github.rest.pulls.createReview({
74+ owner : context.repo.owner,
75+ repo : context.repo.repo,
76+ pull_number : context.payload.pull_request.number,
77+ body : ` ## 🤖 AI 리뷰 - \` ${file.filename}\` \n\n ${review}` ,
78+ event : ' COMMENT'
79+ });
80+ }
81+ }
0 commit comments