CI/CD & LLM 통합

GitHub Actions와 LLM을 활용한 자동 코드 리뷰, 테스트, 문서화 파이프라인 구축 가이드

업데이트 안내: 모델/요금/버전/정책 등 시점에 민감한 정보는 변동될 수 있습니다. 최신 내용은 공식 문서를 확인하세요.
⚡ LLM을 CI/CD에 통합하는 이유
  • 자동 코드 리뷰: PR마다 AI가 코드 품질, 버그, 보안 취약점 검토
  • PR 설명 생성: diff를 분석하여 자동으로 PR 설명 작성
  • 테스트 생성: 새 코드에 대한 유닛 테스트 자동 생성
  • 문서 자동화: 코드 변경 시 README, API 문서 자동 업데이트
  • 커밋 메시지 검증: 일관된 커밋 컨벤션 강제

GitHub Actions 기본

시크릿 설정

# GitHub 저장소 → Settings → Secrets and variables → Actions

# 1. "New repository secret" 클릭
# 2. 시크릿 추가:
Name: ANTHROPIC_API_KEY
Value: sk-ant-api03-...

Name: OPENAI_API_KEY
Value: sk-proj-...

Name: GITHUB_TOKEN
# (자동으로 제공되므로 추가 불필요)

기본 워크플로우 구조

# .github/workflows/example.yml
name: Example Workflow

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  example-job:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install anthropic

      - name: Run LLM task
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python llm_script.py

자동 코드 리뷰

Claude를 이용한 PR 리뷰

.github/workflows/code-review.yml

name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get PR diff
        id: diff
        run: |
          git fetch origin ${{ github.base_ref }}
          DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
          echo "diff<<EOF" >> $GITHUB_OUTPUT
          echo "$DIFF" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install anthropic

      - name: Review code with Claude
        id: review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python << 'EOF'
          import anthropic
          import os
          import json

          client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

          diff = """${{ steps.diff.outputs.diff }}"""

          prompt = f"""다음 코드 변경사항을 리뷰해주세요:

          {diff}

          다음 관점에서 검토해주세요:
          1. 코드 품질 (가독성, 유지보수성)
          2. 잠재적 버그
          3. 보안 취약점
          4. 성능 문제
          5. 베스트 프랙티스 준수

          간결하고 실용적인 피드백을 제공해주세요.
          """

          message = client.messages.create(
              model="claude-",
              max_tokens=4096,
              messages=[{"role": "user", "content": prompt}]
          )

          review_text = message.content[0].text

          # GitHub Actions output으로 전달
          with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
              f.write(f"review<name: Post review comment
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const review = `${{ steps.review.outputs.review }}`;

            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## 🤖 AI Code Review\n\n${review}`
            });

인라인 코멘트 생성

review_code.py

import anthropic
import os
import json
from github import Github

def review_pr(pr_number):
    # GitHub 클라이언트
    g = Github(os.getenv("GITHUB_TOKEN"))
    repo = g.get_repo(os.getenv("GITHUB_REPOSITORY"))
    pr = repo.get_pull(pr_number)

    # Claude 클라이언트
    client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

    # 변경된 파일별로 리뷰
    for file in pr.get_files():
        if not file.patch:
            continue

        prompt = f"""다음 파일의 변경사항을 리뷰해주세요:

파일: {file.filename}

```diff
{file.patch}
```

구체적인 라인별 피드백을 JSON 형식으로 제공해주세요:
[
  {{"line": 라인번호, "comment": "피드백"}}
]

중요한 문제만 지적해주세요."""

        message = client.messages.create(
            model="claude-",
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}]
        )

        try:
            comments = json.loads(message.content[0].text)

            for comment in comments:
                pr.create_review_comment(
                    body=f"🤖 AI Review: {comment['comment']}",
                    commit=pr.get_commits()[0],
                    path=file.filename,
                    line=comment['line']
                )
        except json.JSONDecodeError:
            # JSON 파싱 실패 시 전체 코멘트로
            pr.create_issue_comment(f"## {file.filename}\n\n{message.content[0].text}")

if __name__ == "__main__":
    pr_number = int(os.getenv("PR_NUMBER"))
    review_pr(pr_number)

기존 액션 사용

name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # CodeRabbit (상용, 무료 티어 제공)
      - uses: coderabbitai/ai-pr-reviewer@latest
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          openai_api_key: ${{ secrets.OPENAI_API_KEY }}

      # 또는 Anthropic 사용
      - uses: freeedcom/ai-codereviewer@main
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

PR 자동화

PR 설명 자동 생성

.github/workflows/pr-description.yml

name: Auto PR Description

on:
  pull_request:
    types: [opened]

jobs:
  generate-description:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Generate PR description
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const { execSync } = require('child_process');

            // Git diff 가져오기
            const diff = execSync(
              `git diff origin/${{ github.base_ref }}...HEAD`,
              { encoding: 'utf-8' }
            );

            // Claude API 호출
            const response = await fetch('https://api.anthropic.com/v1/messages', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'x-api-key': '${{ secrets.ANTHROPIC_API_KEY }}',
                'anthropic-version': '2023-06-01'
              },
              body: JSON.stringify({
                model: 'claude-',
                max_tokens: 2048,
                messages: [{
                  role: 'user',
                  content: `다음 코드 변경사항을 분석하여 PR 설명을 작성해주세요:

${diff}

다음 형식으로 작성해주세요:
## 변경 사항
- 주요 변경사항을 bullet point로

## 목적
변경 목적을 1-2문장으로

## 테스트 방법
테스트 방법을 간단히

## 체크리스트
- [ ] 테스트 추가됨
- [ ] 문서 업데이트됨`
                }]
              })
            });

            const data = await response.json();
            const description = data.content[0].text;

            // PR 업데이트
            await github.rest.pulls.update({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.issue.number,
              body: description
            });

PR 제목 검증

name: Validate PR Title

on:
  pull_request:
    types: [opened, edited, synchronize]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Validate title with Conventional Commits
        uses: amannn/action-semantic-pull-request@v5
        with:
          types: |
            feat
            fix
            docs
            style
            refactor
            test
            chore
          requireScope: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Suggest better title with AI
        if: failure()
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const response = await fetch('https://api.anthropic.com/v1/messages', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'x-api-key': '${{ secrets.ANTHROPIC_API_KEY }}',
                'anthropic-version': '2023-06-01'
              },
              body: JSON.stringify({
                model: 'claude-',
                max_tokens: 256,
                messages: [{
                  role: 'user',
                  content: `PR 제목 "${{ github.event.pull_request.title }}"을 Conventional Commits 형식으로 변환해주세요.

형식: <type>(<scope>): <description>
예: feat(auth): add OAuth login

제안된 제목만 출력해주세요.`
                }]
              })
            });

            const data = await response.json();
            const suggestion = data.content[0].text.trim();

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `⚠️ PR 제목이 Conventional Commits 형식이 아닙니다.\n\n**제안된 제목:**\n\`${suggestion}\``
            });

테스트 자동 생성

유닛 테스트 생성 워크플로우

.github/workflows/generate-tests.yml

name: Generate Tests

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  generate:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install anthropic

      - name: Generate tests for new files
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python << 'EOF'
          import anthropic
          import os
          import subprocess
          from pathlib import Path

          client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

          # 새로 추가된 Python 파일 찾기
          result = subprocess.run(
              ["git", "diff", "--name-only", "--diff-filter=A", "origin/${{ github.base_ref }}"],
              capture_output=True,
              text=True
          )

          new_files = [f for f in result.stdout.strip().split('\n') if f.endswith('.py')]

          for file_path in new_files:
              if not Path(file_path).exists() or file_path.startswith('test_'):
                  continue

              with open(file_path, 'r') as f:
                  code = f.read()

              prompt = f"""다음 Python 코드에 대한 pytest 유닛 테스트를 생성해주세요:

```python
{code}
```

요구사항:
1. pytest 사용
2. 모든 public 함수/메서드 테스트
3. Edge case 포함
4. Mocking 필요 시 pytest-mock 사용
5. 주석으로 각 테스트 설명

전체 테스트 코드만 출력해주세요 (마크다운 없이)."""

              message = client.messages.create(
                  model="claude-",
                  max_tokens=8192,
                  messages=[{"role": "user", "content": prompt}]
              )

              test_code = message.content[0].text.strip()

              # 테스트 파일 저장
              test_file = file_path.replace('.py', '_test.py')
              test_file = test_file.replace('/', '/test_', 1) if '/' in test_file else f'test_{test_file}'

              with open(test_file, 'w') as f:
                  f.write(test_code)

              print(f"Generated: {test_file}")
          EOF

      - name: Commit generated tests
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add test_*.py *_test.py

          if git diff --staged --quiet; then
            echo "No tests generated"
          else
            git commit -m "test: auto-generate tests with Claude"
            git push
          fi

테스트 생성 스크립트

generate_tests.py

import anthropic
import os
import sys
from pathlib import Path

def generate_test(source_file: str, test_file: str):
    """주어진 소스 파일에 대한 테스트 생성"""
    client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

    with open(source_file, 'r') as f:
        code = f.read()

    prompt = f"""다음 Python 코드에 대한 완전한 pytest 테스트 스위트를 생성해주세요:

```python
{code}
```

요구사항:
1. pytest와 pytest-mock 사용
2. 모든 함수와 클래스 메서드 테스트
3. 정상 케이스와 edge case 모두 포함
4. 필요 시 fixtures 사용
5. 명확한 테스트 함수 이름 (test_function_name_when_condition_then_result)
6. Docstring으로 각 테스트 설명

완전한 테스트 코드만 출력해주세요."""

    message = client.messages.create(
        model="claude-",
        max_tokens=8192,
        messages=[{"role": "user", "content": prompt}]
    )

    test_code = message.content[0].text

    # 코드 블록 제거 (```python ... ```)
    if test_code.startswith("```"):
        lines = test_code.split('\n')
        test_code = '\n'.join(lines[1:-1])

    with open(test_file, 'w') as f:
        f.write(test_code)

    print(f"✅ Generated: {test_file}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python generate_tests.py <source_file>")
        sys.exit(1)

    source = sys.argv[1]
    test = f"test_{Path(source).name}"

    generate_test(source, test)

문서 자동화

README 자동 업데이트

.github/workflows/update-readme.yml

name: Update README

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'lib/**'

jobs:
  update:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - name: Generate README
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python << 'EOF'
          import anthropic
          import os
          from pathlib import Path

          client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

          # 프로젝트 구조 수집
          structure = []
          for path in Path("src").rglob("*.py"):
              with open(path) as f:
                  content = f.read()
                  structure.append(f"### {path}\n```python\n{content[:500]}...\n```")

          project_info = "\n\n".join(structure)

          prompt = f"""다음 Python 프로젝트의 README.md를 생성해주세요:

{project_info}

다음 섹션을 포함해주세요:
1. 프로젝트 소개
2. 주요 기능
3. 설치 방법
4. 사용 예제
5. API 문서
6. 기여 가이드
7. 라이선스

실용적이고 명확하게 작성해주세요."""

          message = client.messages.create(
              model="claude-",
              max_tokens=8192,
              messages=[{"role": "user", "content": prompt}]
          )

          readme = message.content[0].text

          with open("README.md", "w") as f:
              f.write(readme)
          EOF

      - name: Commit README
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add README.md

          if git diff --staged --quiet; then
            echo "No changes to README"
          else
            git commit -m "docs: auto-update README"
            git push
          fi

API 문서 생성

name: Generate API Docs

on:
  push:
    branches: [main]
    paths:
      - 'api/**'

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Generate OpenAPI docs
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python << 'EOF'
          import anthropic
          import os
          import json

          client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

          # FastAPI 라우터 수집
          with open("api/routes.py") as f:
              routes_code = f.read()

          prompt = f"""다음 FastAPI 코드를 분석하여 OpenAPI 3.0 스펙을 생성해주세요:

```python
{routes_code}
```

완전한 openapi.json 파일을 생성해주세요."""

          message = client.messages.create(
              model="claude-",
              max_tokens=8192,
              messages=[{"role": "user", "content": prompt}]
          )

          # JSON 추출
          spec_text = message.content[0].text
          start = spec_text.find("{")
          end = spec_text.rfind("}") + 1
          spec = json.loads(spec_text[start:end])

          with open("docs/openapi.json", "w") as f:
              json.dump(spec, f, indent=2)
          EOF

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./docs

CHANGELOG 자동 생성

name: Generate Changelog

on:
  release:
    types: [created]

jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Generate changelog
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          # 이전 릴리스 이후 커밋 가져오기
          PREV_TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))
          COMMITS=$(git log $PREV_TAG..HEAD --pretty=format:"%s")

          python << EOF
          import anthropic
          import os

          client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

          commits = """$COMMITS"""

          prompt = f"""다음 커밋 메시지들을 분석하여 CHANGELOG를 생성해주세요:

{commits}

다음 형식으로 작성해주세요:

## [버전] - 날짜

### Added
- 새 기능

### Changed
- 변경사항

### Fixed
- 버그 수정

### Deprecated
- 폐기 예정

Keep A Changelog 형식을 따라주세요."""

          message = client.messages.create(
              model="claude-",
              max_tokens=4096,
              messages=[{"role": "user", "content": prompt}]
          )

          with open("CHANGELOG.md", "a") as f:
              f.write("\n" + message.content[0].text + "\n")
          EOF

      - name: Commit changelog
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add CHANGELOG.md
          git commit -m "docs: update CHANGELOG for ${{ github.event.release.tag_name }}"
          git push

배포 파이프라인

Docker 빌드 및 배포

name: Build and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: user/app:${{ github.sha }},user/app:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        run: |
          # SSH로 서버에 접속하여 배포
          ssh user@server "docker pull user/app:latest && docker-compose up -d"

스모크 테스트 (AI 검증)

name: Smoke Tests

on:
  deployment_status:

jobs:
  test:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Run smoke tests
        run: |
          curl https://api.example.com/health

      - name: Verify with AI
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          # API 응답 수집
          RESPONSE=$(curl -s https://api.example.com/test)

          python << EOF
          import anthropic
          import os

          client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

          response = """$RESPONSE"""

          prompt = f"""다음 API 응답을 분석하여 정상 작동 여부를 판단해주세요:

{response}

다음 관점에서 검증해주세요:
1. 응답 형식이 올바른가?
2. 필수 필드가 모두 있는가?
3. 에러나 경고가 있는가?
4. 성능 문제가 있는가?

"PASS" 또는 "FAIL"로 시작하여 간단히 설명해주세요."""

          message = client.messages.create(
              model="claude-",
              max_tokens=1024,
              messages=[{"role": "user", "content": prompt}]
          )

          result = message.content[0].text
          print(result)

          if result.startswith("FAIL"):
              exit(1)
          EOF

비용 최적화

프롬프트 캐싱

# 공통 컨텍스트를 캐시하여 비용 절감
message = client.messages.create(
    model="claude-",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "You are a code reviewer...",
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=messages
)

조건부 실행

on:
  pull_request:
    types: [opened, synchronize]
    paths:
      - 'src/**'
      - 'lib/**'
      # docs 변경은 리뷰 생략
      - '!docs/**'
      - '!*.md'

비용 제한

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 5  # 최대 5분
    steps:
      - name: Review code
        env:
          MAX_TOKENS: 2048  # 토큰 제한
        run: |
          python review.py --max-tokens $MAX_TOKENS

베스트 프랙티스

보안

  • 시크릿 관리: GitHub Secrets 사용, 절대 하드코딩 금지
  • 권한 최소화: 필요한 권한만 부여 (permissions 명시)
  • 공개 PR: 외부 PR에서는 시크릿 노출 방지 (pull_request_target 사용)
  • 감사 로그: API 호출 기록 및 비용 추적

성능

  • 캐싱: actions/cache로 의존성 캐시
  • 병렬 실행: 독립적인 작업은 병렬로
  • 조건부 실행: paths, if 조건으로 불필요한 실행 방지
  • 타임아웃: 무한 대기 방지

안정성

  • 재시도: API 실패 시 재시도 로직
  • 에러 핸들링: 실패해도 워크플로우 중단 방지 (continue-on-error)
  • 알림: Slack, Discord 등으로 실패 알림
  • 모니터링: 워크플로우 실행 시간, 성공률 추적
🚀 다음 단계

핵심 정리

  • CI/CD & LLM 통합의 핵심 개념과 흐름을 정리합니다.
  • GitHub Actions 기본를 단계별로 이해합니다.
  • 실전 적용 시 기준과 주의점을 확인합니다.

실무 팁

  • 입력/출력 예시를 고정해 재현성을 확보하세요.
  • CI/CD & LLM 통합 범위를 작게 잡고 단계적으로 확장하세요.
  • GitHub Actions 기본 조건을 문서화해 대응 시간을 줄이세요.