Skip to content
Santekno.com | Level Up Your Engineering Skills
ID
📖 0%
13 Oct 2026 · 30 mnt baca ·Artikel 65 / 208
Go

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.

IH
Ihsan Arif
Penulis di Santekno · Backend Engineer

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.

text
 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 comment

Desain 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.

bash
 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.

go
  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.

yaml
  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          fi

Perhatikan 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.

markdown
 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/100

Keunggulan 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.

yaml
 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.

bash
 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"
64fi

Perhatikan 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.

yaml
 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.


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.

yaml
 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/degradation

Data 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.

bash
 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.

yaml
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.

yaml
 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_KEY

Dua 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.

bash
 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 APPROVED

Test 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.

bash
 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.

go
 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.

go
 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.

bash
 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.

text
 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 Slack

Loop 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.

Artikel Terkait

💬 Komentar