Gemini Automated Code Review #44
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Gemini Automated Code Review | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize] | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: "리뷰할 PR 번호 (기존 PR 수동 리뷰 시 입력)" | |
| required: false | |
| jobs: | |
| code_review: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Install GoogleGenerativeAI SDK | |
| run: npm install @google/generative-ai@latest | |
| - name: Determine PR Info | |
| id: pr_info | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| let pr_number; | |
| if (context.eventName === "workflow_dispatch") { | |
| pr_number = context.payload.inputs["pr_number"]; | |
| if (!pr_number) { | |
| core.setFailed("❌ 수동 실행 시에는 PR 번호 입력이 필요합니다."); | |
| return; | |
| } | |
| } else { | |
| pr_number = context.payload.pull_request.number; | |
| } | |
| console.log(`🔍 리뷰할 PR 번호: ${pr_number}`); | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr_number | |
| }); | |
| console.log(`📂 base: ${pr.base.ref}, head: ${pr.head.ref}`); | |
| core.setOutput("number", pr_number); | |
| core.setOutput("base", pr.base.ref); | |
| core.setOutput("head", pr.head.ref); | |
| - name: Generate Git Diff for PR | |
| run: | | |
| echo "📂 base: ${{ steps.pr_info.outputs.base }}" | |
| echo "📂 head: ${{ steps.pr_info.outputs.head }}" | |
| git fetch origin "${{ steps.pr_info.outputs.base }}" | |
| git fetch origin "${{ steps.pr_info.outputs.head }}" | |
| git diff "origin/${{ steps.pr_info.outputs.base }}"..."origin/${{ steps.pr_info.outputs.head }}" > diff.txt | |
| - name: Run Gemini Review (Line-by-Line JSON) | |
| id: gemini_review | |
| uses: actions/github-script@v7 | |
| env: | |
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const diff_output = fs.readFileSync("diff.txt", "utf8"); | |
| const { GoogleGenerativeAI } = require("@google/generative-ai"); | |
| if (!diff_output.trim()) { | |
| console.log("No code changes detected, skipping review."); | |
| fs.writeFileSync("review_result.txt", "변경된 코드가 없어 리뷰를 건너뜁니다."); | |
| return; | |
| } | |
| const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); | |
| const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); | |
| const prompt = ` | |
| Explain in Korean. | |
| 당신은 **시니어 프론트엔드 개발자**입니다. | |
| 아래 git diff를 기반으로 변경된 줄마다 코드리뷰 코멘트를 작성하세요. | |
| 반드시 다음 JSON 형식을 **엄격히** 지켜서 출력하십시오: | |
| [ | |
| { | |
| "path": "src/components/Example.tsx", | |
| "line": 123, | |
| "text": "라인별 리뷰 코멘트", | |
| "side": "RIGHT" | |
| } | |
| ] | |
| 규칙: | |
| 1. **무조건 src 내부에 있는 .ts 및 .tsx 파일만 리뷰합니다.** | |
| 2. **성능 문제**가 있는지 해당 라인 기준으로 평가합니다. | |
| 3. **타입스크립트 문법 오류, 타입 미스매치, 타입 개선 포인트**가 있으면 지적합니다. | |
| 4. **변수명, 함수명, 주석 품질**이 부족한 경우 개선 의견을 냅니다. | |
| 5. **중복 코드 또는 리팩토링 포인트**가 보이면 간단하게 제안합니다. | |
| 6. 단, 라인별 리뷰 특성상 **각 코멘트는 1~2줄로 간결하게 작성**합니다. | |
| (전체 파일 기준 5~8개 제안은 불가능하므로 "해당 줄에 필요한 제안"만 작성) | |
| 매우 중요: | |
| - diff 의 "@@ -a,b +c,d @@" 정보를 이용해 **정확한 실제 변경 라인 번호(line)** 를 계산해야 합니다. | |
| - JSON 형식이 조금이라도 깨지면 안 됩니다. | |
| - JSON 이외의 설명 텍스트를 절대 출력하지 마세요. | |
| <git diff> | |
| ${diff_output} | |
| </git diff> | |
| `; | |
| const result = await model.generateContent(prompt); | |
| const text = result.response.text(); | |
| fs.writeFileSync("review.json", text); | |
| - name: Convert JSON to diff positions | |
| id: convert | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const review = JSON.parse(fs.readFileSync("review.json", "utf8")); | |
| const diff = fs.readFileSync("diff.txt", "utf8"); | |
| const output = []; | |
| const fileBlocks = diff.split("diff --git").slice(1); | |
| for (const block of fileBlocks) { | |
| const pathMatch = block.match(/b\/(.+)\nindex/); | |
| if (!pathMatch) continue; | |
| const path = pathMatch[1]; | |
| const fileComments = review.filter(c => c.path === path); | |
| if (fileComments.length === 0) continue; | |
| const lines = block.split("\n"); | |
| let position = 0; | |
| let newLine = 0; | |
| let oldLine = 0; | |
| for (const line of lines) { | |
| position++; | |
| if (line.startsWith("@@")) { | |
| const m = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/); | |
| if (m) { | |
| oldLine = parseInt(m[1]); | |
| newLine = parseInt(m[2]); | |
| } | |
| continue; | |
| } | |
| if (line.startsWith("+")) { | |
| newLine++; | |
| const comment = fileComments.find(c => c.line === newLine); | |
| if (comment) { | |
| output.push({ | |
| path, | |
| position, | |
| body: comment.text | |
| }); | |
| } | |
| continue; | |
| } | |
| if (line.startsWith("-")) { | |
| oldLine++; | |
| continue; | |
| } | |
| oldLine++; | |
| newLine++; | |
| } | |
| } | |
| core.setOutput("comments", JSON.stringify(output)); | |
| - name: Submit Code Review | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const comments = JSON.parse(`${{ steps.convert.outputs.comments }}`); | |
| if (!comments.length) { | |
| console.log("No review comments generated."); | |
| return; | |
| } | |
| await github.rest.pulls.createReview({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: ${{ steps.pr_info.outputs.number }}, | |
| event: "COMMENT", | |
| comments | |
| }); | |
| console.log("라인별 코드 리뷰 제출됨!"); |