AI Agents · 강의

자동화된 PR 검토 댓글

API로 PR 변경 내용을 읽고 인라인 검토 댓글을 게시합니다.

레슨 3/413개 단계

자동화된 PR 검토 댓글은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Agents 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

자동화된 PR 검토란 무엇인가요

자동화된 PR 검토 에이전트는 변경된 파일을 읽고 차이점을 분석한 다음, 특정 부분을 대상으로 검토 댓글을 게시합니다. 사람 검토자와 같은 작업을 더 빠르고 일관되게 수행합니다. 사용 사례로는 보안 검사, 스타일 적용, 일반적인 버그 탐지, 테스트 범위 확인, PR 변경 내용 요약 등이 있습니다.

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')

# Get a specific pull request by number
pr = repo.get_pull(42)
print(f'PR #{pr.number}: {pr.title}')
print(f'Author: {pr.user.login}')
print(f'Base branch: {pr.base.ref} <- Head: {pr.head.ref}')
print(f'State: {pr.state}')
print(f'Changed files: {pr.changed_files}')
print(f'Additions: +{pr.additions}')
print(f'Deletions: -{pr.deletions}')

열린 PR 나열하기

repo.get_pulls()를 사용하여 PR을 나열하십시오. 상태(open, closed), 대상 브랜치 또는 정렬 순서로 필터링할 수 있습니다. 검토 봇은 일반적으로 아직 검토되지 않은 open PR을 처리합니다.

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')

# All open PRs targeting main
open_prs = repo.get_pulls(
    state='open',
    base='main',
    sort='created',
    direction='desc'
)

for pr in open_prs:
    # Check if agent has already reviewed it
    reviews = list(pr.get_reviews())
    agent_reviewed = any(
        r.user.login == 'my-agent-bot[bot]'
        for r in reviews
    )
    if not agent_reviewed:
        print(f'Needs review: PR #{pr.number} - {pr.title}')

변경된 파일 가져오기

pr.get_files()는 File 객체 목록을 반환합니다. 각 객체는 PR에서 변경된 하나의 파일을 나타냅니다. 주요 속성은 filename, status(추가됨/수정됨/삭제됨), additions, deletions, patch(통합 차이 텍스트)입니다.

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

changed_files = list(pr.get_files())
print(f'Changed files: {len(changed_files)}')

for file in changed_files:
    print(f'\n{file.status.upper()}: {file.filename}')
    print(f'  +{file.additions} -{file.deletions}')

    # The patch is the unified diff
    if file.patch:
        print(f'  Patch preview: {file.patch[:200]}')

# Filter to only Python files
python_files = [
    f for f in changed_files
    if f.filename.endswith('.py') and f.status != 'removed'
]

파일 패치(차이) 읽기

file.patch는 통합 차이 문자열입니다. +로 시작하는 줄은 추가된 줄이고, -로 시작하는 줄은 삭제된 줄입니다. @@ 헤더는 변경 묶음이 어느 줄 번호에서 시작하는지 보여 줍니다. 이를 분석하여 새 코드에서 문제가 정확히 어디에 있는지 식별하십시오.

def parse_patch_hunks(patch):
    if not patch:
        return []

    hunks = []
    current_hunk = None
    new_line_num = 0

    for line in patch.split('\n'):
        if line.startswith('@@'):
            # Parse: @@ -old_start,old_count +new_start,new_count @@
            import re
            match = re.search(r'\+([0-9]+)', line)
            if match:
                new_line_num = int(match.group(1))
            current_hunk = {'header': line, 'lines': [], 'start_line': new_line_num}
            hunks.append(current_hunk)
        elif current_hunk is not None:
            if line.startswith('+'):
                current_hunk['lines'].append({'type': 'add', 'content': line[1:], 'line': new_line_num})
                new_line_num += 1
            elif line.startswith('-'):
                current_hunk['lines'].append({'type': 'remove', 'content': line[1:], 'line': None})
            else:
                new_line_num += 1

    return hunks

# --- demo ---
patch = (
    '@@ -10,3 +10,4 @@\n'
    ' def add(a, b):\n'
    '-    return a - b\n'
    '+    return a + b\n'
    '+    # fixed subtraction bug\n'
)
for hunk in parse_patch_hunks(patch):
    print(hunk['header'])
    for line in hunk['lines']:
        print(' ', line)

PR 커밋 가져오기

pr.get_commits()는 PR에 포함된 커밋을 반환합니다. 각 커밋에는 메시지, 작성자, 변경된 파일 목록이 있습니다. 커밋 메시지를 분석하면 검토 봇이 PR의 변경 내용을 요약하고 커밋 관리 상태가 양호한지 확인하는 데 도움이 됩니다.

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

commits = list(pr.get_commits())
print(f'Commits in PR: {len(commits)}')

for commit in commits:
    print(f'\nCommit: {commit.sha[:8]}')
    print(f'Author: {commit.commit.author.name}')
    print(f'Message: {commit.commit.message[:80]}')
    print(f'Changes: +{commit.stats.additions} -{commit.stats.deletions}')

# Check commit message quality
poor_messages = [
    c for c in commits
    if len(c.commit.message.split()[0] if c.commit.message else '') < 3
    or c.commit.message.lower().startswith(('wip', 'fix', 'test'))
]
if poor_messages:
    print(f'{len(poor_messages)} commits have poor message quality')

PR 검토 게시하기

pr.create_review()를 사용하여 공식 검토를 제출하십시오. event 매개 변수는 검토 유형을 결정합니다. 'APPROVE', 'REQUEST_CHANGES', 'COMMENT' 중 하나를 사용할 수 있습니다. 검토에는 전체 본문과 특정 줄에 대한 인라인 댓글 목록을 포함할 수 있습니다.

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

# Submit a review with a general comment (no inline comments)
pr.create_review(
    body=(
        '**Automated Code Review**\n\n'
        'I analyzed this PR and found the following:\n'
        '- No security issues detected\n'
        '- Style conforms to project guidelines\n'
        '- Test coverage looks adequate\n\n'
        '_This review was generated automatically._'
    ),
    event='COMMENT'  # 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT'
)

인라인 검토 댓글 게시하기

인라인 댓글은 차이 화면의 특정 줄에 직접 표시됩니다. 각 댓글은 path(파일 경로), line(새 파일의 줄 번호), body를 지정합니다. 댓글 목록을 pr.create_review(comments=[...])에 전달하십시오.

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

# Build inline comments from analysis results
inline_comments = [
    {
        'path': 'src/auth.py',
        'line': 47,
        'body': (
            ':warning: **Security Issue:** Using `md5` for password hashing.\n'
            'Use `bcrypt` or `argon2` instead.\n'
            'Example: `import bcrypt; bcrypt.hashpw(password.encode(), bcrypt.gensalt())`'
        )
    },
    {
        'path': 'src/utils.py',
        'line': 23,
        'body': (
            ':bulb: Consider using `pathlib.Path` instead of `os.path.join` '
            'for better cross-platform compatibility.'
        )
    }
]

pr.create_review(
    body='Security and style review complete. See inline comments.',
    event='REQUEST_CHANGES',
    comments=inline_comments
)
print(f'Review submitted with {len(inline_comments)} inline comments')

LLM 기반 코드 분석

보안 및 품질 검토 프롬프트와 함께 파일 차이를 LLM에 전달하십시오. 파일 경로, 줄 번호, 심각도, 설명이 포함된 문제 목록을 구조화된 JSON 응답으로 반환하도록 요청하십시오. 이 결과를 사용하여 인라인 댓글 목록을 만드십시오.

import anthropic
import json
import os

client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])

def analyze_file_with_llm(filename, patch):
    prompt = (
        f'Review this Python code diff for issues.\n'
        f'File: {filename}\n\n'
        f'Return JSON array: [{{"line": N, "severity": "error|warning|info", '
        f'"message": "..."}}]\n'
        f'Focus on: security vulnerabilities, bugs, and anti-patterns.\n'
        f'Diff:\n{patch[:3000]}'
    )

    response = client.messages.create(
        model='claude-opus-4-5',
        max_tokens=1000,
        messages=[{'role': 'user', 'content': prompt}]
    )

    try:
        issues = json.loads(response.content[0].text)
        return [
            {'path': filename, 'line': i.get('line', 1),
             'body': f'[{i.get("severity", "info").upper()}] {i.get("message", "")}'}
            for i in issues if isinstance(i, dict)
        ]
    except json.JSONDecodeError:
        return []

전체 PR 검토 에이전트 흐름

모든 요소를 연결하여 완전한 PR 검토 흐름을 구성하십시오. 파일을 가져오고, 검토 가능한 파일만 필터링하고, 각 파일을 LLM으로 분석하고, 댓글을 모은 다음, 한 번의 API 호출로 검토를 제출합니다. LLM 비용을 관리하려면 변경된 파일 중 처음 10개로 제한하십시오.

def run_pr_review(g, repo_name, pr_number):
    repo = g.get_repo(repo_name)
    pr = repo.get_pull(pr_number)

    print(f'Reviewing PR #{pr_number}: {pr.title}')

    # Get changed files (limit to 10)
    files = [
        f for f in list(pr.get_files())[:10]
        if f.filename.endswith(('.py', '.js', '.ts'))
        and f.patch
    ]

    all_comments = []
    for file in files:
        print(f'  Analyzing: {file.filename}')
        comments = analyze_file_with_llm(file.filename, file.patch)
        all_comments.extend(comments)

    # Determine review event based on findings
    critical = [c for c in all_comments if '[ERROR]' in c['body']]
    event = 'REQUEST_CHANGES' if critical else 'COMMENT'

    summary = (
        f'Analyzed {len(files)} files. '
        f'Found {len(all_comments)} issues '
        f'({len(critical)} critical).'
    )

    pr.create_review(body=summary, event=event, comments=all_comments[:25])
    print(f'Review submitted: {event} with {len(all_comments)} comments')

기존 에이전트 검토 확인하기

검토를 게시하기 전에 에이전트 봇이 현재 상태의 이 PR을 이미 검토했는지 확인하십시오. 기존 검토 목록에서 봇의 사용자 이름을 확인하여 중복 검토를 방지하십시오.

def should_review_pr(pr, bot_username):
    reviews = list(pr.get_reviews())

    # Check if bot already reviewed this PR
    bot_reviews = [
        r for r in reviews
        if r.user and r.user.login == bot_username
    ]

    if not bot_reviews:
        return True  # No previous review, proceed

    # Check if new commits were pushed since last review
    last_review_time = max(r.submitted_at for r in bot_reviews)
    commits = list(pr.get_commits())
    latest_commit_time = commits[-1].commit.author.date if commits else None

    if latest_commit_time and latest_commit_time > last_review_time:
        print(f'New commits since last review, re-reviewing...')
        return True

    print(f'PR #{pr.number} already reviewed by {bot_username}')
    return False

# --- demo: minimal stand-ins for PyGithub-style PR/review/commit objects ---
import datetime
from types import SimpleNamespace

class _FakeCommit:
    def __init__(self, date):
        self.commit = SimpleNamespace(author=SimpleNamespace(date=date))

class _FakeReview:
    def __init__(self, login, submitted_at):
        self.user = SimpleNamespace(login=login)
        self.submitted_at = submitted_at

class _FakePR:
    def __init__(self, number, reviews, commits):
        self.number = number
        self._reviews = reviews
        self._commits = commits
    def get_reviews(self):
        return self._reviews
    def get_commits(self):
        return self._commits

pr_never_reviewed = _FakePR(101, [], [_FakeCommit(datetime.datetime(2026, 8, 1))])
print('PR #101 needs review:', should_review_pr(pr_never_reviewed, 'coddy-bot'))

review_time = datetime.datetime(2026, 8, 1, 10, 0)
pr_up_to_date = _FakePR(102, [_FakeReview('coddy-bot', review_time)],
                        [_FakeCommit(review_time - datetime.timedelta(hours=1))])
print('PR #102 needs review:', should_review_pr(pr_up_to_date, 'coddy-bot'))

PR 검토 보안 검사 패턴

일반적인 자동화 검토 패턴은 추가된 줄에서 보안 안티 패턴을 검사하는 것입니다. 예를 들면 하드코딩된 비밀 값, 사용 중단된 함수 사용, SQL 문자열 연결 또는 eval() 호출이 있습니다. 추가된 각 줄을 패턴 집합과 대조하고 발견 항목을 인라인 주석으로 표시합니다.

import re

SECURITY_PATTERNS = [
    (r'eval\s*\(', 'Dangerous eval() call — code injection risk'),
    (r'os\.system\s*\(', 'os.system() is unsafe; use subprocess with a list'),
    (r'pickle\.loads?\s*\(', 'pickle.load is unsafe with untrusted data'),
    (r'execute\(.*%\s*', 'Possible SQL injection — use parameterized queries'),
    (r'md5\s*\(|hashlib\.md5', 'MD5 is cryptographically broken for passwords; use bcrypt'),
]

def scan_patch_for_security(filename, patch):
    if not patch:
        return []
    comments = []
    new_line = 0
    for line in patch.split('\n'):
        if line.startswith('@@'):
            import re as _re
            m = _re.search(r'\+([0-9]+)', line)
            if m:
                new_line = int(m.group(1))
        elif line.startswith('+') and not line.startswith('+++'):
            content = line[1:]
            for pattern, message in SECURITY_PATTERNS:
                if re.search(pattern, content):
                    comments.append({'path': filename, 'line': new_line,
                                     'body': f':rotating_light: **Security:** {message}'})
            new_line += 1
        elif not line.startswith('-'):
            new_line += 1
    return comments

# --- demo ---
patch = (
    '@@ -1,2 +1,3 @@\n'
    ' def run(cmd):\n'
    '+    os.system(cmd)\n'
    '+    hashlib.md5(cmd.encode())\n'
)
for c in scan_patch_for_security('run.py', patch):
    print(c)

빠른 확인: 검토 이벤트

PR 검토 옵션에 대한 이해도를 테스트해 보세요.

자동화된 PR 검토 요약

이제 에이전트가 프로그래밍 방식으로 풀 리퀘스트를 검토할 수 있습니다.

  • repo.get_pull(number) — 메타데이터가 포함된 PR 객체 가져오기
  • pr.get_files() — filename, patch, additions가 포함된 변경된 파일 목록 가져오기
  • pr.get_commits() — 커밋 메시지와 통계 확인하기
  • pr.create_review(body, event, comments) — 선택적 인라인 주석과 함께 검토 제출하기
  • 인라인 주석 형식: {'path': filename, 'line': N, 'body': markdown}
  • 관찰 사항에는 'COMMENT'를, 차단해야 하는 문제에는 'REQUEST_CHANGES'를 사용하기
  • 중복을 피하기 위해 다시 검토하기 전에 기존 검토 확인하기
  • API 제한을 준수하려면 파일 분석을 10개 파일과 인라인 주석 25개로 제한하기
무료로 시작

AI 튜터와 함께 AI Agents을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
60
레슨
239

자주 묻는 질문

“자동화된 PR 검토 댓글” 강의는 무료인가요?

네 — “자동화된 PR 검토 댓글” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

“자동화된 PR 검토 댓글”에서 뭘 배우나요?

API로 PR 변경 내용을 읽고 인라인 검토 댓글을 게시합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Agents을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“자동화된 PR 검토 댓글” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. GitHub REST API 개요
  2. 이슈 조회 및 관리
  3. 자동화된 PR 검토 댓글
  4. 커밋 기록 및 변경 내용 분석
← AI Agents(으)로 돌아가기