Automated Code Review AI di Pull Request Golang: Setup Lengkap
Bangun sistem automated code review dengan AI untuk Go PR. Custom reviewer bot, Copilot integration, review rules berdasarkan CLAUDE.md, scoring, dan enforcement di GitHub Actions.
Automated Code Review dengan AI di Pull Request
Automated code review dengan AI di pull request untuk Golang mengubah review dari aktivitas manual yang sporadis menjadi gate yang konsisten di setiap PR. Di Artikel 03 kita setup dasar Claude di GitHub Actions. Sekarang kita perdalam ke implementasi yang production-ready: sistem automated code review yang benar-benar meng-enforce Go standards, memberikan feedback yang actionable, dan bisa dikonfigurasi sesuai kebutuhan tim.
Ini bukan sekadar “AI baca kode dan komen” — tapi sistem yang punya rules eksplisit, scoring yang objektif, dan enforcement yang graduated dari warning sampai hard block.
05.1 Arsitektur Review System
Sebelum menulis kode, penting memahami bentuk keseluruhan sistem yang akan kita bangun. Diagram berikut memetakan tiga layer review beserta alur eksekusi dan aturan gate per layer.
1Komponen yang akan kita bangun:
2
3┌─────────────────────────────────────────────────────────────┐
4│ LAYER 1: Fast Pre-filter (< 30 detik, zero AI cost) │
5│ Secret scan + basic pattern check via grep/regex │
6├─────────────────────────────────────────────────────────────┤
7│ LAYER 2: AI Review Engine (1-2 menit, low cost) │
8│ Claude haiku + CLAUDE.md context → structured output │
9├─────────────────────────────────────────────────────────────┤
10│ LAYER 3: Result Processor (instant) │
11│ Parse AI output → post PR comment → enforce score gate │
12└─────────────────────────────────────────────────────────────┘
13
14Flow per PR:
15 PR push → Layer 1 (fast filter) → Layer 2 (AI review)
16 → Layer 3 (post comment + enforce)
17
18 Jika Layer 1 fail: stop, tidak buang token AI
19 Jika Layer 2 score < 60: block PR
20 Jika score 60-80: warning comment
21 Jika score > 80: approved commentDesain berlapis ini punya satu prinsip biaya yang penting: Layer 1 yang gratis menyaring pelanggaran obvious sebelum Layer 2 yang berbayar dijalankan. Token AI hanya dibelanjakan untuk kode yang lolos pengecekan murah.
05.2 Review Rules yang Komprehensif
Inti dari sistem review adalah rules yang eksplisit dan terukur — tanpa ini, AI hanya memberi opini subjektif. Script berikut mendefinisikan rules yang akan diinjeksi ke prompt, lengkap dengan bobot poin per kategori dan format output yang machine-parseable.
1# scripts/review-rules.sh
2# Definisi rules yang akan diinjeksi ke AI prompt
3
4cat << 'RULES'
5=== GO REVIEW RULES FOR SANTEKNO SHOP ===
6
7CRITICAL (setiap violation = -15 poin):
8------------------------------------------
9C1. ERROR WRAPPING
10 WRONG: return err
11 return errors.New("message") di non-domain
12 RIGHT: return fmt.Errorf("packageName.MethodName: %w", err)
13 Exception: domain error types boleh return errors.New()
14
15C2. REPOSITORY NOT-FOUND PATTERN
16 WRONG: return nil, pgx.ErrNoRows
17 return nil, sql.ErrNoRows
18 RIGHT: return nil, nil (not-found = nil value, nil error)
19 Context: repository layer hanya — usecase layer check nil result
20
21C3. MONETARY TYPE
22 WRONG: Price float64
23 Amount float32
24 Total float64
25 RIGHT: PriceIDR int64 (atau via money.IDR type)
26 Rule: SEMUA monetary values harus int64 (cents)
27
28C4. ARCHITECTURE VIOLATION
29 WRONG: Handler imports repository implementation
30 Usecase imports database driver (pgx, redis)
31 Domain imports framework (echo, gin)
32 RIGHT: Handler → usecase interface only
33 Usecase → repository interface only
34 Domain → zero external imports
35
36C5. ERROR IGNORED
37 WRONG: result, _ := someFunc() (di non-test code)
38 go func() { someFunc() }() (goroutine dengan unhandled error)
39 RIGHT: result, err := someFunc(); if err != nil { ... }
40
41IMPORTANT (setiap violation = -7 poin):
42------------------------------------------
43I1. CONTEXT PROPAGATION
44 WRONG: db.Query("SELECT...") tanpa ctx
45 http.Get(url) tanpa context
46 RIGHT: db.QueryContext(ctx, "SELECT...")
47 req, _ := http.NewRequestWithContext(ctx, ...)
48
49I2. NIL CHECK SETELAH REPO CALL
50 WRONG: order, _ := repo.GetByID(ctx, id); order.Status
51 RIGHT: order, err := repo.GetByID(ctx, id)
52 if err != nil { return ... }
53 if order == nil { return ErrNotFound }
54
55I3. INTERFACE SIZE
56 WRONG: Interface dengan > 7 methods
57 RIGHT: Split ke smaller, focused interfaces
58
59SUGGESTION (setiap violation = -3 poin):
60------------------------------------------
61S1. GODOC untuk exported symbols
62S2. Test coverage untuk error paths
63S3. Context timeout untuk external calls
64S4. Named return values yang confusing
65S5. Magic numbers tanpa constant
66
67OUTPUT FORMAT (strict, machine-parseable):
68------------------------------------------
69CRITICAL C1: [description] at [file:line]
70 Fix: [exact code fix]
71
72IMPORTANT I1: [description] at [file:line]
73 Fix: [exact code fix]
74
75SUGGESTION S1: [description]
76
77SCORE: [0-100]
78VERDICT: [APPROVED|WARNING|BLOCK]
79 (APPROVED=score≥85, WARNING=score 60-84, BLOCK=score<60)
80RULES'Perhatikan bahwa setiap rule punya contoh WRONG vs RIGHT dan bobot poin yang jelas — inilah yang membuat skor konsisten antar-PR. Format output yang terstruktur memastikan hasil AI bisa di-parse oleh langkah berikutnya tanpa ambiguitas.
05.3 Review Bot: Go Implementation
Rules di atas perlu dieksekusi oleh sesuatu. Kita bangun review bot sebagai binary Go standalone yang menerima diff via stdin, memanggil Claude dengan rules dan konteks CLAUDE.md, lalu mengeluarkan JSON terstruktur dan exit code sesuai verdict.
1// cmd/review-bot/main.go
2// Custom review bot yang bisa run secara standalone atau di CI
3
4package main
5
6import (
7 "context"
8 "encoding/json"
9 "fmt"
10 "os"
11 "os/exec"
12 "strings"
13
14 anthropic "github.com/anthropics/anthropic-sdk-go"
15 "github.com/anthropics/anthropic-sdk-go/option"
16)
17
18type ReviewResult struct {
19 Score int `json:"score"`
20 Verdict string `json:"verdict"` // APPROVED | WARNING | BLOCK
21 Critical []Issue `json:"critical"`
22 Important []Issue `json:"important"`
23 Suggestions []Issue `json:"suggestions"`
24}
25
26type Issue struct {
27 Rule string `json:"rule"`
28 Description string `json:"description"`
29 Location string `json:"location"`
30 Fix string `json:"fix"`
31}
32
33func main() {
34 // Get diff dari stdin atau git
35 var diff string
36 if len(os.Args) > 1 && os.Args[1] == "--from-git" {
37 baseBranch := "main"
38 if len(os.Args) > 2 {
39 baseBranch = os.Args[2]
40 }
41 out, err := exec.Command("git", "diff",
42 "origin/"+baseBranch+"...HEAD", "--", "*.go").Output()
43 if err != nil {
44 fmt.Fprintln(os.Stderr, "git diff failed:", err)
45 os.Exit(1)
46 }
47 diff = string(out)
48 } else {
49 buf := make([]byte, 10*1024*1024) // 10MB max
50 n, _ := os.Stdin.Read(buf)
51 diff = string(buf[:n])
52 }
53
54 if strings.TrimSpace(diff) == "" {
55 fmt.Println("No Go changes detected, skip review")
56 os.Exit(0)
57 }
58
59 // Limit diff size untuk cost control
60 if len(diff) > 50000 {
61 diff = diff[:50000] + "\n[... diff truncated for cost control ...]"
62 }
63
64 // Load CLAUDE.md context
65 claudeMD := ""
66 if content, err := os.ReadFile("CLAUDE.md"); err == nil {
67 lines := strings.Split(string(content), "\n")
68 if len(lines) > 150 {
69 lines = lines[:150] // first 150 lines
70 }
71 claudeMD = strings.Join(lines, "\n")
72 }
73
74 // Load review rules
75 rules := loadReviewRules()
76
77 // Run AI review
78 result, err := runAIReview(context.Background(), diff, claudeMD, rules)
79 if err != nil {
80 fmt.Fprintln(os.Stderr, "Review failed:", err)
81 // Graceful degradation: jangan block pipeline karena AI fail
82 os.Exit(0)
83 }
84
85 // Output result
86 jsonOut, _ := json.MarshalIndent(result, "", " ")
87 fmt.Println(string(jsonOut))
88
89 // Exit code berdasarkan verdict
90 switch result.Verdict {
91 case "BLOCK":
92 os.Exit(2) // block PR
93 case "WARNING":
94 os.Exit(1) // warning, tidak block
95 default:
96 os.Exit(0) // approved
97 }
98}
99
100func loadReviewRules() string {
101 // Load dari file jika ada, fallback ke embedded
102 if content, err := os.ReadFile("scripts/review-rules.md"); err == nil {
103 return string(content)
104 }
105 return defaultReviewRules // embedded default
106}
107
108func runAIReview(ctx context.Context, diff, claudeMD, rules string) (*ReviewResult, error) {
109 client := anthropic.NewClient(
110 option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
111 )
112
113 prompt := fmt.Sprintf(`You are a strict Go code reviewer for a production e-commerce service.
114
115PROJECT CONTEXT (from CLAUDE.md):
116%s
117
118REVIEW RULES:
119%s
120
121CODE DIFF TO REVIEW:
122%s
123
124Apply the review rules above. Output ONLY valid JSON:
125{
126 "score": 85,
127 "verdict": "APPROVED",
128 "critical": [
129 {"rule": "C1", "description": "...", "location": "file.go:45", "fix": "..."}
130 ],
131 "important": [],
132 "suggestions": []
133}
134
135Verdict: APPROVED=score>=85, WARNING=score 60-84, BLOCK=score<60`, claudeMD, rules, diff)
136
137 msg, err := client.Messages.New(ctx, anthropic.MessageNewParams{
138 Model: anthropic.F(anthropic.ModelClaudeHaiku4_5),
139 MaxTokens: anthropic.F(int64(1000)),
140 Messages: anthropic.F([]anthropic.MessageParam{
141 anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
142 }),
143 })
144 if err != nil {
145 return nil, fmt.Errorf("runAIReview: %w", err)
146 }
147
148 // Parse JSON dari response
149 text := msg.Content[0].Text
150 // Strip markdown code blocks jika ada
151 text = strings.TrimPrefix(text, "```json")
152 text = strings.TrimSuffix(text, "```")
153 text = strings.TrimSpace(text)
154
155 var result ReviewResult
156 if err := json.Unmarshal([]byte(text), &result); err != nil {
157 return nil, fmt.Errorf("parse review result: %w (raw: %s)", err, text[:min(len(text), 200)])
158 }
159
160 return &result, nil
161}
162
163const defaultReviewRules = `
164CRITICAL (-15): C1=error wrap, C2=repo nil-nil, C3=monetary int64, C4=arch layer, C5=error ignored
165IMPORTANT (-7): I1=context, I2=nil check, I3=interface size
166SUGGESTION (-3): S1=godoc, S2=test coverage, S3=timeout
167`
168
169func min(a, b int) int {
170 if a < b {
171 return a
172 }
173 return b
174}Dua keputusan desain menonjol: graceful degradation (jika API Claude gagal, bot exit 0 alih-alih memblokir PR) dan cost control (diff dipotong di 50KB, CLAUDE.md dibatasi 150 baris). AI review adalah enhancement, bukan gate yang boleh menyebabkan outage palsu.
05.4 GitHub Actions Workflow Lengkap
Setelah bot siap, kita rangkai ketiga layer menjadi workflow GitHub Actions. Workflow berikut menjalankan pre-filter gratis, lalu AI review, lalu mem-posting komentar terstruktur dan menegakkan gate skor.
1# .github/workflows/code-review.yml
2name: AI Code Review
3
4on:
5 pull_request:
6 types: [opened, synchronize, ready_for_review]
7 paths: ['**.go']
8
9env:
10 GO_VERSION: '1.22'
11 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
12
13jobs:
14 # LAYER 1: Fast pre-filter (no AI cost)
15 pre-filter:
16 name: Pre-filter Check
17 runs-on: ubuntu-latest
18 if: "!github.event.pull_request.draft"
19 outputs:
20 has_changes: ${{ steps.check.outputs.has_changes }}
21 diff_size: ${{ steps.check.outputs.diff_size }}
22 steps:
23 - uses: actions/checkout@v4
24 with:
25 fetch-depth: 0
26
27 - name: Check for Go changes
28 id: check
29 run: |
30 DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- '*.go')
31
32 if [ -z "$DIFF" ]; then
33 echo "has_changes=false" >> $GITHUB_OUTPUT
34 exit 0
35 fi
36
37 # Redact secrets dari diff
38 SAFE_DIFF=$(echo "$DIFF" | \
39 sed 's/sk-ant-[a-zA-Z0-9_-]*/sk-ant-REDACTED/g' | \
40 sed 's/ghp_[a-zA-Z0-9]*/ghp_REDACTED/g')
41
42 echo "$SAFE_DIFF" > /tmp/safe-diff.txt
43 DIFF_SIZE=$(wc -c < /tmp/safe-diff.txt)
44
45 echo "has_changes=true" >> $GITHUB_OUTPUT
46 echo "diff_size=$DIFF_SIZE" >> $GITHUB_OUTPUT
47 echo "Go changes detected: $DIFF_SIZE bytes"
48
49 - name: Quick pattern check (no AI)
50 if: steps.check.outputs.has_changes == 'true'
51 run: |
52 # Fast grep untuk obvious violations
53 VIOLATIONS=""
54
55 # Check float64 untuk monetary (quick win)
56 if grep -nE '(Price|Amount|Total|Cost|Fee|Salary)\s+float(32|64)' \
57 <(git diff origin/${{ github.base_ref }}...HEAD -- '*.go') 2>/dev/null; then
58 VIOLATIONS="$VIOLATIONS\n- CRITICAL: float64 detected for monetary field"
59 fi
60
61 # Check for _ ignoring errors di non-test files
62 if git diff origin/${{ github.base_ref }}...HEAD -- '*.go' \
63 | grep '^+' | grep -v '.*_test.go' \
64 | grep -E ',\s*_\s*:?=\s*(dao|repo|db|cache|svc)' > /dev/null 2>&1; then
65 VIOLATIONS="$VIOLATIONS\n- CRITICAL: error ignored with _ in non-test code"
66 fi
67
68 if [ -n "$VIOLATIONS" ]; then
69 echo "Quick scan found violations:"
70 echo -e "$VIOLATIONS"
71 echo "::warning::Quick scan violations found — AI review will detail these"
72 fi
73
74 # LAYER 2: AI Review Engine
75 ai-review:
76 name: AI Review Engine
77 runs-on: ubuntu-latest
78 needs: pre-filter
79 if: needs.pre-filter.outputs.has_changes == 'true'
80 permissions:
81 pull-requests: write
82 contents: read
83 steps:
84 - uses: actions/checkout@v4
85 with:
86 fetch-depth: 0
87 - uses: actions/setup-go@v5
88 with:
89 go-version: '${{ env.GO_VERSION }}'
90 cache: true
91
92 - name: Build review bot
93 run: |
94 go build -o /tmp/review-bot ./cmd/review-bot/
95
96 - name: Run AI Review
97 id: review
98 continue-on-error: true # graceful degradation
99 run: |
100 # Reconstruct safe diff
101 git diff origin/${{ github.base_ref }}...HEAD -- '*.go' \
102 | sed 's/sk-ant-[a-zA-Z0-9_-]*/REDACTED/g' > /tmp/safe-diff.txt
103
104 # Run review bot
105 REVIEW_JSON=$(/tmp/review-bot < /tmp/safe-diff.txt 2>/tmp/review-err.txt)
106 EXIT_CODE=$?
107
108 echo "review_json<<EOF" >> $GITHUB_OUTPUT
109 echo "$REVIEW_JSON" >> $GITHUB_OUTPUT
110 echo "EOF" >> $GITHUB_OUTPUT
111
112 # Extract key fields
113 SCORE=$(echo "$REVIEW_JSON" | jq '.score // 85' 2>/dev/null || echo "85")
114 VERDICT=$(echo "$REVIEW_JSON" | jq -r '.verdict // "APPROVED"' 2>/dev/null || echo "APPROVED")
115
116 echo "score=$SCORE" >> $GITHUB_OUTPUT
117 echo "verdict=$VERDICT" >> $GITHUB_OUTPUT
118 echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
119
120 echo "Review score: $SCORE, verdict: $VERDICT"
121
122 # LAYER 3: Result Processor
123 - name: Format and post review comment
124 uses: actions/github-script@v7
125 with:
126 script: |
127 const reviewJson = `${{ steps.review.outputs.review_json }}`;
128 const score = parseInt('${{ steps.review.outputs.score }}') || 85;
129 const verdict = '${{ steps.review.outputs.verdict }}' || 'APPROVED';
130
131 let review;
132 try {
133 review = JSON.parse(reviewJson);
134 } catch {
135 // AI gagal parse — graceful degradation
136 console.log('Could not parse review JSON, skipping comment');
137 return;
138 }
139
140 // Build comment markdown
141 const scoreEmoji = score >= 85 ? '🟢' : score >= 60 ? '🟡' : '🔴';
142 const verdictEmoji = verdict === 'APPROVED' ? '✅' : verdict === 'WARNING' ? '⚠️' : '❌';
143
144 const lines = [
145 `## 🤖 AI Code Review`,
146 ``,
147 `**Score:** ${scoreEmoji} ${score}/100 | **Verdict:** ${verdictEmoji} ${verdict}`,
148 ``
149 ];
150
151 if (review.critical?.length > 0) {
152 lines.push('### ❌ Critical Issues (must fix before merge)');
153 lines.push('');
154 for (const issue of review.critical) {
155 lines.push(`**${issue.rule}** at \`${issue.location}\``);
156 lines.push(`> ${issue.description}`);
157 lines.push(`> **Fix:** \`${issue.fix}\``);
158 lines.push('');
159 }
160 }
161
162 if (review.important?.length > 0) {
163 lines.push('### ⚠️ Important Issues (strongly recommended)');
164 lines.push('');
165 for (const issue of review.important) {
166 lines.push(`**${issue.rule}** at \`${issue.location}\``);
167 lines.push(`> ${issue.description}`);
168 lines.push('');
169 }
170 }
171
172 if (review.suggestions?.length > 0) {
173 lines.push('<details><summary>💡 Suggestions (optional improvements)</summary>');
174 lines.push('');
175 for (const s of review.suggestions) {
176 lines.push(`- **${s.rule}:** ${s.description}`);
177 }
178 lines.push('</details>');
179 lines.push('');
180 }
181
182 lines.push('---');
183 lines.push(`*AI Review by Claude claude-haiku-4-5 | Score threshold: 60 (block) / 85 (approved)*`);
184
185 const body = lines.join('\n');
186
187 // Update existing comment or create new
188 const { data: comments } = await github.rest.issues.listComments({
189 owner: context.repo.owner,
190 repo: context.repo.repo,
191 issue_number: context.issue.number
192 });
193
194 const existing = comments.find(c => c.body.includes('🤖 AI Code Review'));
195
196 if (existing) {
197 await github.rest.issues.updateComment({
198 owner: context.repo.owner,
199 repo: context.repo.repo,
200 comment_id: existing.id,
201 body
202 });
203 } else {
204 await github.rest.issues.createComment({
205 owner: context.repo.owner,
206 repo: context.repo.repo,
207 issue_number: context.issue.number,
208 body
209 });
210 }
211
212 - name: Enforce score gate
213 run: |
214 SCORE=${{ steps.review.outputs.score }}
215 VERDICT=${{ steps.review.outputs.verdict }}
216
217 echo "Final: Score=$SCORE, Verdict=$VERDICT"
218
219 # Phase 1: warning only (uncomment untuk Phase 2/3)
220 # if [ "$VERDICT" == "BLOCK" ]; then
221 # echo "::error::PR blocked by AI review (score $SCORE < 60)"
222 # exit 1
223 # fi
224
225 if [ "$VERDICT" == "BLOCK" ]; then
226 echo "::warning::AI review score $SCORE < 60 — consider fixing critical issues"
227 elif [ "$VERDICT" == "WARNING" ]; then
228 echo "::warning::AI review score $SCORE — some issues found"
229 else
230 echo "✅ AI review passed: $SCORE/100"
231 fiPerhatikan langkah “Enforce score gate” masih dalam mode warning (blok yang mem-block di-comment). Ini sesuai strategi enforcement bertahap: mulai permisif, ketatkan setelah tim terbiasa dengan output.
05.5 Copilot Review dengan Custom Instructions
Untuk tim yang lebih memilih Copilot daripada custom bot, GitHub Copilot bisa dikonfigurasi lewat file instruksi. File berikut mendefinisikan aturan review yang sama — CRITICAL, IMPORTANT, SUGGESTION — dalam format yang dibaca Copilot secara native.
1# .github/copilot-instructions.md
2# Version: 2.0 | Updated: 2026-07
3
4You are reviewing Go code for Santekno Shop — a production B2C e-commerce
5platform running on Go 1.22, Echo v4, pgx/v5, Redis, and Kafka.
6
7## CRITICAL RULES — Flag as CRITICAL if violated
8
9### C1: Error Wrapping
10Every error from external calls MUST be wrapped:
11```go
12// WRONG:
13return err
14return errors.New("database failed")
15
16// RIGHT:
17return fmt.Errorf("orderRepository.GetByID: %w", err)
18```
19
20### C2: Repository Not-Found Pattern
21When a record is not found, repositories MUST return (nil, nil):
22```go
23// WRONG:
24if err == pgx.ErrNoRows {
25 return nil, pgx.ErrNoRows
26}
27
28// RIGHT:
29if errors.Is(err, pgx.ErrNoRows) {
30 return nil, nil // not-found is not an error
31}
32```
33
34### C3: Monetary Types
35ALL monetary values MUST use int64 (representing cents):
36```go
37// WRONG:
38type Order struct {
39 TotalAmount float64
40}
41
42// RIGHT:
43type Order struct {
44 TotalAmountIDR int64 // 50000 = Rp 500.00
45}
46```
47
48### C4: Architecture Boundaries
49Layer import rules:
50- handler: ONLY import usecase interfaces (never impl or repo)
51- usecase: ONLY import domain types + repository interfaces
52- repository: ONLY import domain types + database drivers
53- domain: ZERO external imports (stdlib only)
54
55### C5: Error Ignoring
56NEVER ignore errors with _ in non-test code:
57```go
58// WRONG:
59result, _ := someFunc()
60
61// RIGHT:
62result, err := someFunc()
63if err != nil { return fmt.Errorf("...%w", err) }
64```
65
66## IMPORTANT RULES — Flag as IMPORTANT
67
68- I1: Context (ctx) must be first param and passed to all downstream calls
69- I2: Always nil-check after repository calls before using the result
70- I3: Interfaces should have <= 5 methods (split if larger)
71
72## SUGGESTIONS — Flag as SUGGESTION
73- S1: Exported functions/types/constants need godoc comments
74- S2: Error paths should have corresponding test cases
75- S3: External calls should use context with timeout
76
77## OUTPUT FORMAT
78For each issue: [CRITICAL|IMPORTANT|SUGGESTION] at file:line — brief explanation
79End with: REVIEW_SCORE: X/100Keunggulan pendekatan Copilot adalah integrasi native dengan PR checks tanpa infrastruktur tambahan. Trade-off-nya: kamu kehilangan kontrol penuh atas model dan scoring yang dimiliki custom bot — pilihan tergantung seberapa dalam tim ingin men-tune review.
05.6 Multi-Reviewer Pattern
Untuk PR bernilai tinggi, kita bisa menjalankan dua reviewer independen dan menggabungkan skornya. Workflow berikut menjalankan Copilot dan custom Claude bot secara paralel, lalu job ketiga meng-agregasi hasil menjadi satu ringkasan.
1# Untuk high-value PRs: run multiple reviewers
2# dan aggregate hasilnya
3
4jobs:
5 # Reviewer 1: Copilot (built-in GitHub)
6 copilot-review:
7 name: Copilot Review
8 runs-on: ubuntu-latest
9 permissions:
10 pull-requests: write
11 contents: read
12 steps:
13 - uses: actions/checkout@v4
14 with: { fetch-depth: 0 }
15 - uses: github/copilot-for-pull-requests@v1
16 with:
17 github-token: ${{ secrets.GITHUB_TOKEN }}
18 review-instructions: |
19 Review per .github/copilot-instructions.md.
20 End with: REVIEW_SCORE: X/100
21
22 # Reviewer 2: Custom Claude Bot
23 claude-review:
24 name: Claude Review
25 runs-on: ubuntu-latest
26 permissions:
27 pull-requests: write
28 contents: read
29 env:
30 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
31 steps:
32 - uses: actions/checkout@v4
33 with: { fetch-depth: 0 }
34 - uses: actions/setup-go@v5
35 with: { go-version: '1.22' }
36 - run: go build -o /tmp/review-bot ./cmd/review-bot/
37 - run: |
38 git diff origin/${{ github.base_ref }}...HEAD -- '*.go' \
39 > /tmp/diff.txt
40 /tmp/review-bot < /tmp/diff.txt > /tmp/review.json || true
41
42 # Aggregator: combine scores dan post summary
43 aggregate-reviews:
44 name: Aggregate Review Scores
45 runs-on: ubuntu-latest
46 needs: [copilot-review, claude-review]
47 if: always()
48 permissions:
49 pull-requests: write
50 steps:
51 - uses: actions/github-script@v7
52 with:
53 script: |
54 // Get all PR comments
55 const { data: comments } = await github.rest.issues.listComments({
56 owner: context.repo.owner,
57 repo: context.repo.repo,
58 issue_number: context.issue.number
59 });
60
61 // Extract scores dari comments
62 const scorePattern = /REVIEW_SCORE:\s*(\d+)/;
63 const scores = comments
64 .map(c => c.body.match(scorePattern))
65 .filter(Boolean)
66 .map(m => parseInt(m[1]));
67
68 if (scores.length === 0) return;
69
70 const avg = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
71 const min = Math.min(...scores);
72
73 // Post summary comment
74 await github.rest.issues.createComment({
75 owner: context.repo.owner,
76 repo: context.repo.repo,
77 issue_number: context.issue.number,
78 body: [
79 `## 📊 Review Score Summary`,
80 ``,
81 `| Reviewer | Score |`,
82 `|----------|-------|`,
83 ...scores.map((s, i) => `| Reviewer ${i + 1} | ${s}/100 |`),
84 `| **Average** | **${avg}/100** |`,
85 `| **Minimum** | **${min}/100** |`,
86 ``,
87 min < 60 ? `❌ **Blocked:** minimum score ${min} < 60 threshold` :
88 avg < 80 ? `⚠️ **Warning:** average score ${avg}/100` :
89 `✅ **Approved:** all reviewers passed`,
90 ].join('\n')
91 });Dua reviewer independen menurunkan risiko false negative yang mungkin lolos dari satu reviewer. Perhatikan agregator memakai minimum untuk keputusan block — satu reviewer yang menemukan masalah serius cukup untuk menahan merge.
05.7 Review untuk File Spesifik
Kadang review per-file lebih berguna daripada review satu diff besar, terutama untuk feedback yang presisi. Script berikut meng-iterasi setiap file Go yang berubah, mereview satu per satu, dan menghitung skor rata-rata.
1#!/bin/bash
2# scripts/review-changed-files.sh
3# Review hanya files yang berubah, per file
4
5set -euo pipefail
6
7BASE_BRANCH="${1:-main}"
8OVERALL_SCORE=0
9FILE_COUNT=0
10
11# Get list of changed Go files (excluding generated)
12CHANGED_FILES=$(git diff origin/${BASE_BRANCH}...HEAD --name-only -- '*.go' \
13 | grep -v '_mock\|\.pb\.\|_gen\.' || true)
14
15if [ -z "$CHANGED_FILES" ]; then
16 echo "No relevant Go files changed"
17 exit 0
18fi
19
20for file in $CHANGED_FILES; do
21 if [ ! -f "$file" ]; then continue; fi
22
23 echo "Reviewing: $file..."
24
25 # Get file diff
26 FILE_DIFF=$(git diff origin/${BASE_BRANCH}...HEAD -- "$file")
27
28 # Get full file content untuk context
29 FILE_CONTENT=$(cat "$file" | head -100)
30
31 PROMPT="Review this Go file change for production code issues.
32
33FILE: $file
34CONTEXT (first 100 lines):
35$FILE_CONTENT
36
37DIFF:
38$FILE_DIFF
39
40Check: error wrapping, monetary types (int64 not float64),
41nil checks after repo calls, context propagation, architecture violations.
42
43Output JSON: {\"score\": 85, \"issues\": [{\"severity\": \"critical|important|suggestion\",
44\"line\": 45, \"description\": \"...\", \"fix\": \"...\"}]}"
45
46 RESULT=$(./scripts/claude-call.sh "$PROMPT" 500) || {
47 echo " Review failed for $file, skip"
48 continue
49 }
50
51 SCORE=$(echo "$RESULT" | jq '.score // 85' 2>/dev/null || echo "85")
52 ISSUES=$(echo "$RESULT" | jq '.issues | length' 2>/dev/null || echo "0")
53
54 echo " Score: $SCORE, Issues: $ISSUES"
55
56 OVERALL_SCORE=$((OVERALL_SCORE + SCORE))
57 FILE_COUNT=$((FILE_COUNT + 1))
58done
59
60if [ "$FILE_COUNT" -gt 0 ]; then
61 AVG_SCORE=$((OVERALL_SCORE / FILE_COUNT))
62 echo ""
63 echo "Average score: $AVG_SCORE/100 across $FILE_COUNT files"
64fiPerhatikan filter grep -v yang mengecualikan mock dan generated files — mereview kode yang di-generate mesin hanya buang token. Pendekatan per-file berguna saat kamu ingin komentar inline yang menempel di baris tertentu, bukan satu ringkasan besar.
05.8 Handling Large PRs
PR yang menyentuh puluhan file berisiko melebihi context window dan mahal untuk di-review penuh. Langkah berikut mendeteksi PR besar, melewati AI review, dan menggantinya dengan checklist manual.
1# Strategy untuk PR yang sangat besar (> 50 files)
2
3- name: Check PR size
4 id: size_check
5 run: |
6 CHANGED=$(git diff origin/${{ github.base_ref }}...HEAD --name-only -- '*.go' | wc -l)
7 echo "changed_files=$CHANGED" >> $GITHUB_OUTPUT
8
9 if [ "$CHANGED" -gt 50 ]; then
10 echo "too_large=true" >> $GITHUB_OUTPUT
11 else
12 echo "too_large=false" >> $GITHUB_OUTPUT
13 fi
14
15- name: Large PR notice
16 if: steps.size_check.outputs.too_large == 'true'
17 uses: actions/github-script@v7
18 with:
19 script: |
20 await github.rest.issues.createComment({
21 owner: context.repo.owner,
22 repo: context.repo.repo,
23 issue_number: context.issue.number,
24 body: [
25 `## ⚠️ Large PR Detected`,
26 ``,
27 `This PR touches ${steps.size_check.outputs.changed_files} Go files.`,
28 `Full AI review skipped for cost efficiency.`,
29 ``,
30 `**Manual review checklist:**`,
31 `- [ ] Error handling: all errors wrapped with fmt.Errorf`,
32 `- [ ] Monetary types: all float64 → int64`,
33 `- [ ] Architecture: no cross-layer imports`,
34 `- [ ] Tests: coverage for error paths`,
35 ``,
36 `Consider splitting this PR into smaller, focused PRs.`
37 ].join('\n')
38 });
39
40- name: AI Review (skip if too large)
41 if: steps.size_check.outputs.too_large != 'true'
42 run: |
43 # Hanya review jika PR tidak terlalu besar
44 /tmp/review-bot --from-git ${{ github.base_ref }}Cost control di sini bukan keterbatasan, tapi fitur: PR besar yang di-skip justru mendapat dorongan untuk dipecah menjadi PR kecil yang lebih mudah di-review — baik oleh AI maupun manusia.
05.9 Review History dan Trending
Untuk melihat apakah kualitas kode membaik dari waktu ke waktu, setiap hasil review disimpan sebagai artifact. Langkah berikut menyimpan skor per commit dengan retensi 90 hari.
1# Track review scores overtime untuk melihat improvement
2
3- name: Save review score to artifact
4 if: always()
5 run: |
6 mkdir -p /tmp/review-history
7 cat > /tmp/review-history/score-${{ github.sha }}.json << EOF
8 {
9 "pr": ${{ github.event.number }},
10 "sha": "${{ github.sha }}",
11 "author": "${{ github.actor }}",
12 "score": ${{ steps.review.outputs.score }},
13 "verdict": "${{ steps.review.outputs.verdict }}",
14 "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
15 "base_branch": "${{ github.base_ref }}"
16 }
17 EOF
18
19- uses: actions/upload-artifact@v4
20 if: always()
21 with:
22 name: review-score-${{ github.sha }}
23 path: /tmp/review-history/
24 retention-days: 90
25
26# Scheduled job untuk aggregate monthly stats:
27# - Average score per developer
28# - Most common violation types
29# - Trending improvement/degradationData historis ini menjadi bahan retro bulanan yang objektif: alih-alih berdebat soal “kualitas kode menurun” secara subjektif, tim punya angka konkret per developer dan per tipe pelanggaran.
05.10 Review Rules Versioning
Rules review akan berevolusi seiring tim belajar. Memberi versi pada file rules memungkinkan kita melacak perubahan dan mengecualikan PR lama dari aturan baru. Contoh berikut menunjukkan header versi dan changelog di file rules.
1# scripts/review-rules.md
2# Version: 1.2 | Updated: 2026-07
3
4## Changelog
5# v1.2: Added I3 (interface size limit)
6# v1.1: Relaxed S2 (godoc) from IMPORTANT to SUGGESTION
7# v1.0: Initial rules
8
9## Critical Rules
10[... rules content ...]
11
12# Gunakan version di prompt:
13PROMPT="Using review rules version $(grep 'Version:' scripts/review-rules.md | head -1)..."Versi ini juga bisa diverifikasi di CI agar tidak ada rules basi yang terpakai diam-diam. Langkah berikut mencetak versi rules yang sedang aktif.
1# Verify rules version di CI
2- name: Check review rules version
3 run: |
4 VERSION=$(grep 'Version:' scripts/review-rules.md | head -1 | grep -oP '\d+\.\d+')
5 echo "Using review rules v$VERSION"
6 echo "::notice::Review rules version: $VERSION"Dengan versi yang eksplisit dan diumumkan ke tim, perubahan rules tidak mengejutkan siapa pun — PR yang dibuat sebelum update bisa diberi masa tenggang satu sprint sebelum aturan baru berlaku penuh.
05.11 Integrasi dengan PR Status Checks
Agar review benar-benar jadi gate, ia harus terdaftar sebagai required status check di branch protection. Konfigurasi berikut menunjukkan cara mencocokkan nama job dengan required check dan menangani PR dari fork.
1# Tambahkan AI review sebagai required status check:
2
3# 1. Di GitHub: Settings → Branches → Branch protection rules
4# → Require status checks → add "AI Review Engine"
5
6# 2. Required check name harus match dengan job name:
7jobs:
8 ai-review:
9 name: "AI Review Engine" # ← ini yang di-require
10
11# 3. For PRs dari fork (tidak ada secrets):
12jobs:
13 ai-review:
14 if: github.event.pull_request.head.repo.full_name == github.repository
15 # Skip review untuk fork PRs yang tidak ada ANTHROPIC_API_KEYDua detail mudah terlewat: nama job harus persis sama dengan nama required check, dan PR dari fork tidak punya akses secrets sehingga harus di-skip agar tidak gagal palsu. Menangani keduanya sejak awal menghindari pipeline yang macet.
05.12 Debugging Review Output
Sebelum mengandalkan bot, kita perlu memverifikasi ia benar-benar menangkap pelanggaran yang seharusnya. Script berikut membuat file dengan violation yang disengaja, menjalankan bot, dan membandingkan output dengan ekspektasi.
1# Debug review bot secara lokal
2
3# 1. Test dengan sample diff yang mengandung violations
4cat > /tmp/test-violation.go << 'GOTEST'
5package usecase
6
7import "errors"
8
9type Order struct {
10 Price float64 // VIOLATION: should be int64
11}
12
13func GetOrder() (*Order, error) {
14 order, err := repository.Get()
15 return order, err // VIOLATION: error not wrapped
16}
17GOTEST
18
19# Create fake diff
20git diff HEAD /dev/null -- /tmp/test-violation.go > /tmp/test.diff
21
22# Run review bot
23export ANTHROPIC_API_KEY="sk-ant-xxx"
24go run cmd/review-bot/main.go < /tmp/test.diff
25
26# Expected output:
27# {
28# "score": 60,
29# "verdict": "WARNING",
30# "critical": [
31# {"rule": "C3", "description": "Price field uses float64", ...},
32# {"rule": "C1", "description": "error not wrapped", ...}
33# ],
34# ...
35# }
36
37# 2. Test false positive: kode yang harusnya pass
38git diff HEAD~1 HEAD -- internal/usecase/order/cancel_order.go > /tmp/good.diff
39go run cmd/review-bot/main.go < /tmp/good.diff
40# Expected: score >= 85, verdict APPROVEDTest dua arah ini penting: satu memastikan bot menangkap violation (true positive), satu lagi memastikan bot tidak menandai kode bersih (false positive). Jalankan keduanya setiap kali rules diubah.
05.13 Review untuk Test Files
Test files punya aturan berbeda dari kode production — hal yang acceptable di test bisa jadi pelanggaran di production. Prompt berikut memberi konteks agar AI menerapkan rules yang sesuai per tipe file.
1# Dalam review prompt, tambahkan context:
2PROMPT="Review Go code.
3NOTE: Test files (*_test.go) have different rules:
4- OK to use _ for ignoring errors in test setup
5- OK to use t.Fatal() instead of fmt.Errorf
6- OK to have direct struct initialization (no constructor)
7- NOT OK: float64 for monetary even in tests
8- NOT OK: wrong error types in test assertions
9
10Diff includes test files — apply appropriate rules per file type."Tanpa konteks ini, bot akan membanjiri test files dengan false positive — misalnya menandai _ yang mengabaikan error di setup test, padahal itu praktik yang wajar. Membedakan aturan per tipe file menjaga review tetap relevan.
05.14 Performance: Parallel Review untuk Batch
Untuk monorepo dengan banyak service, mereview satu per satu terlalu lambat. Fungsi Go berikut menjalankan review paralel dengan semaphore untuk membatasi jumlah panggilan AI konkuren demi kontrol biaya.
1// Untuk monorepo dengan banyak service: parallel review
2
3func reviewAllServices(ctx context.Context, services []string) []ReviewResult {
4 results := make([]ReviewResult, len(services))
5
6 var wg sync.WaitGroup
7 // Semaphore untuk limit concurrent AI calls (cost control)
8 sem := make(chan struct{}, 3) // max 3 parallel reviews
9
10 for i, service := range services {
11 wg.Add(1)
12 go func(idx int, svc string) {
13 defer wg.Done()
14 sem <- struct{}{}
15 defer func() { <-sem }()
16
17 diff := getServiceDiff(svc)
18 result, err := runAIReview(ctx, diff, "", defaultReviewRules)
19 if err != nil {
20 results[idx] = ReviewResult{Score: 85, Verdict: "APPROVED"} // graceful
21 return
22 }
23 results[idx] = *result
24 }(i, service)
25 }
26
27 wg.Wait()
28 return results
29}Semaphore sem := make(chan struct{}, 3) adalah kunci: ia membatasi maksimal tiga review berjalan bersamaan, menyeimbangkan kecepatan dengan biaya dan rate limit API. Pola ini idiomatik di Go untuk membatasi konkurensi tanpa library eksternal.
05.15 Review Metrics Dashboard
Artifact skor yang tersimpan perlu diolah menjadi wawasan. Tool Go berikut membaca artifact review, mengelompokkan per author, dan mencetak statistik rata-rata beserta tren keseluruhan.
1// cmd/review-metrics/main.go
2// Aggregate review scores dari GitHub Actions artifacts
3
4package main
5
6import (
7 "encoding/json"
8 "fmt"
9 "os"
10 "path/filepath"
11 "sort"
12)
13
14type ScoreRecord struct {
15 PR int `json:"pr"`
16 Author string `json:"author"`
17 Score int `json:"score"`
18 Verdict string `json:"verdict"`
19 Timestamp string `json:"timestamp"`
20}
21
22func main() {
23 artifactDir := os.Args[1] // path ke downloaded artifacts
24
25 var records []ScoreRecord
26
27 filepath.Walk(artifactDir, func(path string, info os.FileInfo, err error) error {
28 if !info.IsDir() && filepath.Ext(path) == ".json" {
29 data, _ := os.ReadFile(path)
30 var r ScoreRecord
31 if json.Unmarshal(data, &r) == nil {
32 records = append(records, r)
33 }
34 }
35 return nil
36 })
37
38 // Group by author
39 byAuthor := make(map[string][]int)
40 for _, r := range records {
41 byAuthor[r.Author] = append(byAuthor[r.Author], r.Score)
42 }
43
44 fmt.Println("=== Review Score Metrics ===")
45 fmt.Println()
46
47 type AuthorStats struct {
48 Name string
49 Average float64
50 Min int
51 PRCount int
52 }
53
54 var stats []AuthorStats
55 for author, scores := range byAuthor {
56 sum := 0
57 minScore := 100
58 for _, s := range scores {
59 sum += s
60 if s < minScore {
61 minScore = s
62 }
63 }
64 stats = append(stats, AuthorStats{
65 Name: author,
66 Average: float64(sum) / float64(len(scores)),
67 Min: minScore,
68 PRCount: len(scores),
69 })
70 }
71
72 sort.Slice(stats, func(i, j int) bool {
73 return stats[i].Average > stats[j].Average
74 })
75
76 for _, s := range stats {
77 fmt.Printf("%-20s avg: %.1f/100 min: %d PRs: %d\n",
78 s.Name, s.Average, s.Min, s.PRCount)
79 }
80
81 // Overall trend
82 if len(records) >= 2 {
83 first := records[0].Score
84 last := records[len(records)-1].Score
85 trend := last - first
86 symbol := "📈"
87 if trend < 0 {
88 symbol = "📉"
89 }
90 fmt.Printf("\nOverall trend: %s %+d points (%d → %d)\n",
91 symbol, trend, first, last)
92 }
93}Dashboard sederhana ini mengubah data mentah menjadi percakapan yang berguna: siapa yang konsisten skor tinggi, siapa yang mungkin butuh dukungan, dan apakah tren tim membaik. Yang penting, ini bahan diskusi konstruktif, bukan alat menghakimi.
05.16 Review untuk Database Query Changes
Perubahan yang menyentuh SQL punya risiko khusus yang layak review terpisah. Prompt berikut memfokuskan analisis pada lima kategori masalah query: injection, N+1, missing index, unbounded query, dan transaction.
1# Special review untuk perubahan yang menyentuh SQL:
2
3PROMPT="Review Go code yang mengandung database query changes.
4
5Focus khusus untuk query changes:
61. SQL INJECTION: apakah ada string concatenation di query?
7 WRONG: fmt.Sprintf('SELECT * FROM orders WHERE id = ' + userInput)
8 RIGHT: pool.QueryRow(ctx, 'SELECT ... WHERE id = \$1', userInput)
9
102. N+1 QUERY: apakah ada query di dalam loop?
11 WRONG: for _, order := range orders { db.Get(order.UserID) }
12 RIGHT: batch query atau JOIN
13
143. MISSING INDEX: apakah filter column yang mungkin tidak ada index?
15 Warning: large table filter by non-PK column
16
174. UNBOUNDED QUERY: SELECT tanpa LIMIT untuk potentially large table?
18 WRONG: 'SELECT * FROM orders WHERE user_id = \$1'
19 RIGHT: 'SELECT * FROM orders WHERE user_id = \$1 LIMIT \$2 OFFSET \$3'
20
215. TRANSACTION USAGE: apakah operasi yang atomic sudah dalam transaction?
22 Multi-table update yang harus atomic: wajib transaction
23
24Diff:
25\${DIFF}"Review query khusus ini menangkap kelas bug yang sering lolos review generik — terutama N+1 dan unbounded query yang tidak terlihat berbahaya di PR kecil tapi meledak saat data production membesar.
05.17 Feedback Loop: Dari Review ke CLAUDE.md Update
Sistem review yang baik terus belajar dari pola yang muncul. Diagram alur berikut menggambarkan siklus continuous improvement: dari retro sprint, analitik bulanan, sampai versioning rules.
1Workflow untuk continuous improvement review rules:
2
3Sprint End Review (15 menit):
4 Pertanyaan:
5 1. Apa yang paling sering di-flag oleh AI review bulan ini?
6 2. Apakah ada false positives yang annoying?
7 3. Ada pattern baru yang perlu ditambahkan ke rules?
8
9 Action:
10 → Update .github/copilot-instructions.md
11 → Update cmd/review-bot/ rules
12 → Update CLAUDE.md jika relevant
13
14Monthly Analytics (dari metrics dashboard):
15 → Average score per developer
16 → Most common violation types
17 → Trending: improving atau degrading?
18 → PR yang consistently low score → need mentoring?
19
20Version control rules:
21 → Setiap update ke review rules = semver bump
22 → CHANGELOG di scripts/review-rules.md
23 → Announce ke tim via SlackLoop ini menjadikan review rules dokumen hidup, bukan aturan statis yang cepat basi. Pola yang sering di-flag jadi kandidat masuk CLAUDE.md; false positive yang mengganggu jadi kandidat pengecualian eksplisit.
05.18 Tips & Gotchas
💡 Tip 1: Graceful degradation. Jika AI review fail (API down, timeout), jangan block PR. continue-on-error: true dan log ke step summary, tapi tidak fail job.
💡 Tip 2: Update comment, jangan tambah. Setiap push baru ke PR mengupdate review comment yang sama — tidak menumpuk komentar yang membingungkan.
💡 Tip 3: Score yang jelas vs verbose. Satu baris score di atas comment lebih berguna dari 50 baris penjelasan. Developer perlu tahu pass/fail dalam 3 detik.
💡 Tip 4: Collapsible suggestions. Gunakan <details> HTML tag untuk suggestions — supaya PR tidak terlihat cluttered tapi informasi tetap ada.
⚠️ Gotcha 1: Jangan review generated files. Mock files, protobuf generated, dan vendor folder harus diexclude dari diff sebelum dikirim ke AI.
⚠️ Gotcha 2: Token limit. Diff yang sangat besar bisa melebihi context window. Implementasikan chunking atau truncation dengan explicit [truncated] marker.
⚠️ Gotcha 3: Review yang terlalu lenient. Jika semua PR dapat score 90+, rules perlu di-tighten. Review rules efektif memberikan variance yang meaningful.
05.19 Ringkasan Artikel 05
Kita sudah membangun sistem automated code review yang production-ready:
Custom review bot (cmd/review-bot/) dengan rules eksplisit, JSON output, dan scoring yang objektif.
GitHub Actions workflow dengan 3-layer: pre-filter (zero cost) → AI review → result processor.
Copilot integration sebagai alternatif via .github/copilot-instructions.md yang comprehensive.
Graduated enforcement: dari continue-on-error: true sampai hard block.
Kunci keberhasilan adalah memulai permisif dan mengetatkan secara bertahap — tim yang langsung menerapkan hard enforcement cenderung mencari cara bypass, sedangkan tim yang membuktikan nilai lebih dulu mencapai adopsi yang genuine.
05.20 Review Otomatis sebagai Standar Tim
Review otomatis yang konsisten lebih berharga dari review manual yang sporadis. Dengan sistem yang dibangun di artikel ini, setiap PR mendapat review yang sama — tidak bergantung pada ketersediaan reviewer, tidak bergantung pada jam kerja atau tingkat energi.
Langkah adopsi yang realistis dalam 30 hari:
- Week 1: Setup Copilot instructions, aktifkan di draft PR dulu, mode warning-only.
- Week 2: Aktifkan untuk semua non-draft PR, tune false positive rate.
- Week 3-4: Enforce CRITICAL issues, update review rules berdasarkan pola nyata.
- Month 2: Ukur perbaikan PR cycle time, presentasikan hasil ke tim.
Dengan review rules yang baik dan context file yang comprehensive, AI review score yang konsisten di atas 85 adalah target yang realistis dalam sebulan. Di artikel berikutnya, kita tambahkan layer keamanan yang lebih dalam: security audit otomatis yang men-scan dependency dan kode untuk vulnerability.