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 등으로 실패 알림
- 모니터링: 워크플로우 실행 시간, 성공률 추적
🚀 다음 단계
- 비용 모니터링 - API 사용량 추적 및 최적화
- Docker LLM 환경 - CI/CD에서 사용할 컨테이너 구축
- API 키 관리 - GitHub Secrets 보안 관리
핵심 정리
- CI/CD & LLM 통합의 핵심 개념과 흐름을 정리합니다.
- GitHub Actions 기본를 단계별로 이해합니다.
- 실전 적용 시 기준과 주의점을 확인합니다.
실무 팁
- 입력/출력 예시를 고정해 재현성을 확보하세요.
- CI/CD & LLM 통합 범위를 작게 잡고 단계적으로 확장하세요.
- GitHub Actions 기본 조건을 문서화해 대응 시간을 줄이세요.