refactor: 라인별 코드리뷰로 변경 #47
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 (Chunked) | |
| 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"); | |
| const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); | |
| const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); | |
| if (!diff_output.trim()) { | |
| fs.writeFileSync("review.json", "[]"); | |
| return; | |
| } | |
| const CHUNK_SIZE = 30000; | |
| let chunks = []; | |
| for (let i = 0; i < diff_output.length; i += CHUNK_SIZE) { | |
| chunks.push(diff_output.slice(i, i + CHUNK_SIZE)); | |
| } | |
| console.log(`📦 청크 개수: ${chunks.length}`); | |
| // --- 2) Request Gemini for each chunk --- | |
| async function askGemini(prompt) { | |
| const res = await model.generateContent(prompt); | |
| return res.response.text(); | |
| } | |
| let merged = []; | |
| for (let idx = 0; idx < chunks.length; idx++) { | |
| const chunk = chunks[idx]; | |
| const prompt = ` | |
| Explain in Korean. | |
| 당신은 **시니어 프론트엔드 개발자**입니다. | |
| 아래 git diff 조각(chunk ${idx+1}/${chunks.length})을 기반으로 | |
| 변경된 코드의 '변경된 줄'마다 리뷰 코멘트를 생성하세요. | |
| 출력은 반드시 **JSON 배열만**: | |
| [ | |
| { | |
| "path": "src/components/Example.tsx", | |
| "line": 123, | |
| "text": "리뷰 코멘트", | |
| "side": "RIGHT" | |
| } | |
| ] | |
| 규칙: | |
| - src 내부의 .ts / .tsx 파일만 리뷰 | |
| - 성능 문제 판단 | |
| - 타입스크립트 문법/타입 개선 지적 | |
| - 변수명/함수명/주석 품질 평가 | |
| - 중복 코드/리팩토링 포인트 제안 | |
| - 각 코멘트는 1~2줄만 작성 | |
| 매우 중요: | |
| - "@@ -a,b +c,d @@" 를 사용해 정확한 새 라인 번호 계산 | |
| - JSON 코드블록 금지 (```json 포함 금지) | |
| - JSON 외 다른 텍스트를 절대 출력하지 말 것 | |
| <git diff> | |
| ${chunk} | |
| </git diff> | |
| `; | |
| console.log(`🤖 Gemini 요청: chunk ${idx+1}/${chunks.length}`); | |
| let raw = await askGemini(prompt); | |
| // 코드블록 제거 | |
| raw = raw.replace(/```json/g, "").replace(/```/g, "").trim(); | |
| try { | |
| const parsed = JSON.parse(raw); | |
| merged.push(...parsed); | |
| } catch (e) { | |
| console.log("❌ JSON 파싱 실패. 출력:"); | |
| console.log(raw); | |
| throw e; | |
| } | |
| } | |
| fs.writeFileSync("review.json", JSON.stringify(merged, null, 2)); | |
| console.log("🎉 모든 청크 JSON 병합 완료"); | |
| - name: Convert JSON to diff positions | |
| id: convert | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| let raw = fs.readFileSync("review.json", "utf8") | |
| .replace(/```json/g, "") | |
| .replace(/```/g, "") | |
| .trim(); | |
| let review = JSON.parse(raw); | |
| 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 match = fileComments.find(c => c.line === newLine); | |
| if (match) { | |
| output.push({ | |
| path, | |
| position, | |
| body: match.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("코멘트 없음 — 리뷰 생략"); | |
| 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("라인별 코드 리뷰 제출 완료!"); |