배포 전, 릴리즈 노트를 AI가 검수하게 만들기: claude -p로 짜는 에이전틱 CI/CD 파이프라인
claude -p headless 모드는 문서를 다시 쓰는 손일 뿐 아니라 사람이 하던 판단을 대신하는 눈이 될 수 있습니다. 릴리즈 노트가 실제 커밋과 어긋나는지 GitLab CI가 검수하는 파이프라인을 예제로, --output-format json으로 판정을 받아 exit 1로 게이트에 연결하는 에이전틱 CI/CD 패턴을 정리합니다.
앞 글에서 claude -p로 코드베이스 문서를 정기 갱신하는 방법을 봤습니다. 그때 claude -p는 문서를 다시 쓰는 손이었습니다. 이번에는 같은 claude -p를 더 실무와 가깝게 배치해보겠습니다. 릴리즈 노트를 사람 대신 AI가 1차 검수하고, 문제가 있는 건만 사람에게 올리는 파이프라인으로요. 하나의 검수를 구현하는 두 가지 방식(Claude API 직접 호출과 claude -p)을 나란히 놓고 비교하는 제안 형태로 정리했습니다.
배경
많은 팀의 릴리즈 프로세스는 릴리즈 노트를 사람이 수동으로 검토하고 조직장의 승인이 있어야 머지되는 구조입니다. 릴리즈 빈도가 늘면서 조직장에게 승인 병목과 과부하가 생깁니다. 릴리즈마다 노트와 실제 커밋을 한 줄씩 대조하는 일은 지루하고, 지루한 검토는 형식적으로 흐르기 쉽습니다.
이 제안의 골자는 릴리즈 태그가 생성되는 순간 AI가 릴리즈 노트를 1차 검수하고, 문제가 있는 건만 사람에게 올라오도록 승인 구조를 뒤집는 것입니다. GitLab 기준으로 흐름은 이렇습니다. v1.2.3 형태의 릴리즈 태그를 push하면 파이프라인이 돌고,1 이번 태그의 메시지(릴리즈 노트)와 직전 태그 이후의 변경을 검수해 APPROVE 또는 NEEDS_REVIEW를 판정합니다. APPROVE면 파이프라인이 통과하고, NEEDS_REVIEW면 실패시켜 조직장이 그 릴리즈만 확인합니다.
AI 검수 기준
파이프라인이 Claude에게 부여하는 검수 기준은 네 가지입니다.
커밋 일치성. 릴리즈 노트가 실제 커밋 내역과 일치하는가.
Breaking change 누락 여부. 사용자 영향이 큰 변경이 노트에서 빠지지 않았는가.
문장 명확성. 사용자가 이해할 수 있는 명확한 문장인가.
보안 변경 표기. 보안 관련 변경이 적절히 표기됐는가.
공통 사전 준비는 하나입니다. GitLab의 Settings > CI/CD > Variables에 ANTHROPIC_API_KEY를 Masked·Protected로 등록하는 것 입니다.
방식 A: Claude API 직접 호출
쉘 스크립트로 릴리즈 노트와 커밋 로그를 수집한 뒤, curl로 Claude API를 1회 호출해 판정받는 방식입니다.2 구조가 단순하고 호출당 비용이 예측 가능합니다.
# =====================================================================
# 릴리즈 노트 AI 자동 검수 파이프라인 (방식 A: Claude API 직접 호출)
# ---------------------------------------------------------------------
# 동작 흐름:
# 1) 개발자가 릴리즈 태그(예: v1.2.3)를 push
# 2) 태그 파이프라인이 자동 실행됨
# 3) 태그 메시지 + 직전 태그 이후의 커밋 로그를 수집
# 4) Claude API에 보내서 릴리즈 노트를 검수
# 5) 판정 결과(APPROVE / NEEDS_REVIEW)에 따라 파이프라인 성공/실패
# NEEDS_REVIEW면 파이프라인이 실패하고, 조직장은 실패 건만 보면 된다
# =====================================================================
# workflow: 파이프라인 전체가 언제 실행될지 제어하는 최상위 규칙
workflow:
rules: # 위에서부터 순서대로 평가
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' # v1.2.3 형태의 시맨틱 버전 태그일 때만
when: always
- when: never # 그 외에는 파이프라인 자체를 만들지 않음
stages:
- ai-review
release-note-ai-review:
stage: ai-review
image: alpine:3.20 # curl과 jq만 있으면 되는 가벼운 이미지
variables:
CLAUDE_MODEL: "claude-sonnet-5" # 검수 품질과 비용의 균형에 Sonnet을 권장
MAX_TOKENS: "2000" # 검수 결과 JSON에는 이 정도면 충분
GIT_DEPTH: "0" # 전체 히스토리를 받아 이전 태그와 비교
before_script:
- apk add --no-cache curl jq git
script:
# 1단계: 검수 대상 텍스트 수집
- RELEASE_NOTES=$(git tag -l --format='%(contents)' "${CI_COMMIT_TAG}") # annotated tag 본문을 노트로 간주
- PREV_TAG=$(git describe --tags --abbrev=0 "${CI_COMMIT_TAG}^" 2>/dev/null || echo "")
- |
if [ -n "$PREV_TAG" ]; then
COMMIT_LOG=$(git log --oneline "${PREV_TAG}..${CI_COMMIT_TAG}")
else
COMMIT_LOG=$(git log --oneline "${CI_COMMIT_TAG}")
fi
- |
if [ -z "$RELEASE_NOTES" ]; then
echo "릴리즈 노트(태그 메시지)가 비어 있습니다. annotated tag를 사용하세요."
exit 1
fi
# 2단계: 요청 본문(JSON)을 jq로 안전하게 생성 (문자열 직접 조립 시 특수문자 깨짐 방지)
- |
REQUEST_BODY=$(jq -n \
--arg model "$CLAUDE_MODEL" \
--arg notes "$RELEASE_NOTES" \
--arg commits "$COMMIT_LOG" \
--arg tag "$CI_COMMIT_TAG" \
--argjson max_tokens "$MAX_TOKENS" \
'{
model: $model,
max_tokens: $max_tokens,
system: "당신은 릴리즈 노트 검수 담당자입니다. 반드시 JSON만 출력하세요. 형식: {\"verdict\": \"APPROVE\" 또는 \"NEEDS_REVIEW\", \"score\": 0-100, \"issues\": [문제점 배열], \"summary\": \"한 줄 요약\"}. 검수 기준: (1) 노트가 커밋 내역과 일치하는가 (2) 사용자 영향이 큰 변경이 누락되지 않았는가 (3) 문장이 명확한가 (4) 보안 관련 변경이 적절히 표기됐는가.",
messages: [
{ role: "user",
content: ("릴리즈 태그: " + $tag + "\n\n## 릴리즈 노트\n" + $notes + "\n\n## 실제 커밋 목록\n" + $commits) }
]
}')
# 3단계: Claude API 호출
- |
RESPONSE=$(curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$REQUEST_BODY")
- |
if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then
echo "Claude API 오류:"; echo "$RESPONSE" | jq '.error'; exit 1
fi
# 4단계: 응답 파싱 및 판정. content[0].text에 모델이 출력한 JSON 문자열이 들어있다
- REVIEW_JSON=$(echo "$RESPONSE" | jq -r '.content[0].text')
- REVIEW_JSON=$(echo "$REVIEW_JSON" | sed 's/```json//g; s/```//g') # 코드블록으로 감싼 경우 대비
- VERDICT=$(echo "$REVIEW_JSON" | jq -r '.verdict')
- SCORE=$(echo "$REVIEW_JSON" | jq -r '.score')
- echo "$REVIEW_JSON" | jq .
- echo "$REVIEW_JSON" > ai-review-result.json
# 5단계: 최종 판정. APPROVE면 통과, 아니면 실패시켜 배포를 막는 게이트 역할
- |
if [ "$VERDICT" = "APPROVE" ]; then
echo "AI 검수 통과 (점수 ${SCORE}). 자동 승인."
else
echo "AI 검수 결과 사람 검토 필요 (점수 ${SCORE}). 이 파이프라인 링크를 조직장에게 공유하세요."
exit 1
fi
artifacts:
paths:
- ai-review-result.json # 검수 결과 JSON을 보존
expire_in: 30 days # 감사 기록 보존 기간
when: always # 실패 건이 더 중요하므로 항상 저장
timeout: 10m # API 지연 등으로 무한 대기하지 않도록 제한
방식 A의 특징이자 한계는 Claude가 우리가 떠먹여 준 커밋 제목 목록만 본다는 점입니다. '노트에 적힌 기능이 실제 코드 변경에 정말 들어갔는지'는 우리가 diff를 넘기지 않았으니 확인하지 못합니다.
방식 B: Claude Code CLI (claude -p)
Claude Code를 headless 모드(claude -p)로 실행하는 방식입니다.3 커밋 수집 코드가 필요 없습니다. Claude가 에이전트로서 허용된 git log·git diff·git show를 스스로 실행하며 릴리즈 노트와 실제 변경을 직접 대조합니다. 노트에 언급된 기능이 실제 diff에 존재하는지까지 더 깊게 검증할 수 있습니다.
# =====================================================================
# 릴리즈 노트 AI 자동 검수 파이프라인 (방식 B: Claude Code CLI, claude -p)
# ---------------------------------------------------------------------
# 방식 A와의 차이:
# - curl로 API를 직접 부르는 대신 Claude Code를 headless 모드(-p)로 실행
# - Claude가 에이전트로 동작하며 git log/diff를 스스로 실행해
# 릴리즈 노트와 실제 변경을 직접 대조한다 (커밋 수집 코드 불필요)
# - --allowedTools로 읽기 전용 도구만 허용해 안전하게 실행
# =====================================================================
workflow:
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: always
- when: never
stages:
- ai-review
release-note-ai-review:
stage: ai-review
image: node:20-alpine # Claude Code는 npm 패키지라 Node.js 이미지가 필요
variables:
GIT_DEPTH: "0" # 전체 히스토리 clone. Claude가 이전 태그와 비교 가능
CLAUDE_MODEL: "claude-sonnet-5" # 사용할 모델 (--model 플래그로 전달)
before_script:
- apk add --no-cache git jq
- npm install -g @anthropic-ai/claude-code
- claude --version # 설치 확인용 버전 출력
script:
# 1단계: 릴리즈 노트(태그 메시지) 추출
- RELEASE_NOTES=$(git tag -l --format='%(contents)' "${CI_COMMIT_TAG}")
- |
if [ -z "$RELEASE_NOTES" ]; then
echo "릴리즈 노트(태그 메시지)가 비어 있습니다. annotated tag를 사용하세요."
exit 1
fi
# 2단계: Claude Code headless 실행
# 핵심: 커밋 로그를 우리가 넘기지 않는다. Claude가 허용된 git 명령을 스스로 실행해 확인한다.
- |
RESULT=$(claude -p "릴리즈 태그 ${CI_COMMIT_TAG}의 릴리즈 노트를 검수하세요.
## 릴리즈 노트
${RELEASE_NOTES}
## 검수 방법
git log와 git diff를 사용해 직전 태그 이후의 실제 변경을 확인하고, 릴리즈 노트와 대조하세요.
## 검수 기준
1. 노트가 실제 커밋/변경 내역과 일치하는가
2. 사용자 영향이 큰 변경(breaking change)이 누락되지 않았는가
3. 문장이 명확한가
4. 보안 관련 변경이 적절히 표기됐는가
## 출력 형식 (반드시 이 JSON만 최종 답변으로 출력)
{\"verdict\": \"APPROVE\" 또는 \"NEEDS_REVIEW\", \"score\": 0-100, \"issues\": [\"문제점\"], \"summary\": \"한 줄 요약\"}" \
--bare \
--model "${CLAUDE_MODEL}" \
--output-format json \
--allowedTools "Read,Grep,Glob,Bash(git log *),Bash(git diff *),Bash(git show *),Bash(git tag *),Bash(git describe *)" \
--max-turns 15)
# 플래그 설명:
# -p : headless(print) 모드. 프롬프트 실행 후 결과만 출력하고 종료
# --bare : 로컬 훅/MCP/CLAUDE.md 자동 탐색을 건너뛰어 CI에서 항상 동일하게 동작
# 인증은 ANTHROPIC_API_KEY 환경 변수로 처리됨
# --model : 사용할 모델 지정
# --output-format : json이면 {result, is_error, total_cost_usd, ...} 구조로 출력
# --allowedTools : 승인 프롬프트 없이 실행을 허용할 도구 목록
# 읽기 도구와 읽기 전용 git 명령만 허용해 쓰기/수정 불가
# "Bash(git log *)"처럼 공백과 *로 특정 명령만 화이트리스트
# --max-turns 15 : 에이전트 턴 수 상한. 무한 탐색으로 인한 비용 폭주 방지
# 3단계: 실행 결과 파싱. Claude Code 자체 오류(인증 실패 등)를 먼저 거른다
- |
if [ "$(echo "$RESULT" | jq -r '.is_error')" = "true" ]; then
echo "Claude Code 실행 오류:"; echo "$RESULT" | jq -r '.result'; exit 1
fi
- REVIEW_JSON=$(echo "$RESULT" | jq -r '.result') # .result에 최종 답변(JSON)이 들어있다
- REVIEW_JSON=$(echo "$REVIEW_JSON" | sed 's/```json//g; s/```//g')
- VERDICT=$(echo "$REVIEW_JSON" | jq -r '.verdict')
- SCORE=$(echo "$REVIEW_JSON" | jq -r '.score')
- echo "검수 비용 \$$(echo "$RESULT" | jq -r '.total_cost_usd')" # 건별 실행 비용을 로그로 남긴다
- echo "$REVIEW_JSON" | jq .
- echo "$REVIEW_JSON" > ai-review-result.json
# 4단계: 최종 판정 (게이트)
- |
if [ "$VERDICT" = "APPROVE" ]; then
echo "AI 검수 통과 (점수 ${SCORE}). 자동 승인."
else
echo "AI 검수 결과 사람 검토 필요 (점수 ${SCORE}). 이 파이프라인 링크를 조직장에게 공유하세요."
exit 1
fi
artifacts:
paths:
- ai-review-result.json
expire_in: 30 days
when: always
timeout: 15m # 에이전트 탐색 시간을 고려해 방식 A보다 여유있게
방식 비교
두 방식은 판정 게이트(APPROVE/NEEDS_REVIEW)와 artifacts 구조가 같아서, 파일럿에서 나란히 돌려 비교하기 좋습니다.
항목 | 방식 A · API 직접 호출 | 방식 B · claude -p |
|---|---|---|
동작 | 쉘로 커밋 로그를 모아 API를 1회 호출 | Claude가 git log·diff를 스스로 실행하며 대조 |
검수 깊이 | 커밋 제목 목록 기준 | 실제 diff 내용까지 검증 |
비용 | 단건 호출이라 예측 가능 | 여러 턴이라 더 들 수 있음( |
파이프라인 코드 | 수집 스크립트가 길어짐 | 수집 코드가 없고 프롬프트가 중심 |
안전장치 | API 키 권한만 관리 |
|
확장성 | 용도마다 별도 구현 | 프롬프트만 바꾸면 MR 리뷰·CHANGELOG로 확장 |
커밋 제목만으로 충분한 가벼운 대조는 방식 A가 싸고 빠르고, 노트와 실제 코드의 어긋남까지 봐야 하는 깊은 검수는 방식 B가 제 값을 합니다.
운영에서 조정할 것
파일럿을 실제 운영으로 옮길 때 조직 정책에 맞춰 손보게 되는 지점이 몇 군데 있습니다.
게이트 강도.
NEEDS_REVIEW에 파이프라인을 실패시키는 대신 MR 코멘트만 남기는 선택도 있습니다. 반대로APPROVE라도 배포 잡을when: manual로 두어 사람 손을 한 번 거치게 할 수 있습니다. AI 승인은 어디까지나 보조 수단이라는 합의가 먼저입니다.노트 출처. 이 예제는 annotated 태그 메시지를 노트로 봅니다. GitLab Release 기능이나
CHANGELOG.md를 쓴다면 Releases API로 소스를 바꾸면 됩니다.모델 이원화. 평범한 릴리즈는 비용이 낮은 모델로 돌리고 보안 릴리즈처럼 중요한 건만 상위 모델로 올리면 비용과 품질을 함께 잡을 수 있습니다.
알림. 실패 시 Slack·메일 웹훅을 붙이면 조직장에게 걸러진 예외 건만 밀어 줄 수 있습니다.
같은 패턴의 다른 용례
'claude -p에 판정을 시키고 그 결과를 파이프라인 상태로 연결한다'는 골격은 릴리즈 노트 말고도 그대로 쓸 수 있습니다. MR의 git diff를 넘겨 보안 취약점을 훑게 하거나, MR의 변경이 acquire-codebase-knowledge로 만든 문서와 어긋나는지 검사해 코멘트로 남기는 식입니다. 프롬프트만 바꾸면 같은 파이프라인 골격이 다른 검수로 확장됩니다.
정리
방식 A와 방식 B의 차이가 '에이전틱'이라는 말을 가장 잘 설명합니다. 방식 A는 우리가 판단에 필요한 재료를 모아 한 번 넘기는 함수 호출이고, 방식 B는 Claude가 무엇을 봐야 할지 스스로 정하게 해 저장소를 조사하는 에이전틱 방식입니다. 방식 B는 방식 A보다 인간을 더 깊게 보조하는 대신 비용과 시간이 늘어납니다. 그래서 --max-turns와 읽기 전용 --allowedTools로 울타리를 치는 일이 필요합니다.
어느 방식이든 핵심 이득은 같습니다. 조직장이 모든 릴리즈를 다 보는 대신, AI가 걸러낸 예외 건만 확인하면 됩니다. 판정이 흔들릴 수 있다는 점만 잊지 않고 게이트를 안전하게 설계하면, 승인 병목을 줄이면서도 감사 추적이 남는 릴리즈 검수를 에이전틱 파이프라인이 대신 챙겨줍니다.
GitLab, CI_COMMIT_TAG predefined variable, GitLab Docs.
↩Anthropic, Messages API, Claude API Docs.
↩Anthropic, Run Claude Code programmatically (headless mode), Claude Code Docs.
↩
이 글은 모던웹연구소 (www.modernweblabs.com)에서 처음 발행되었습니다. © 모던웹연구소. 무단 전재 및 재배포를 금합니다.
뉴스레터
엔터프라이즈 현장 전문가들이 검증한 노트, 격주 발행.
Claude Code, GitHub Copilot, AI 네이티브 엔지니어링 전략과 도입 사례를 격주로 정리해 보내드립니다.
모던웹연구소 · 컨설팅 안내
글을 읽었다면, 다음은 팀에 이식할 차례입니다.
이 글에서 다룬 방식을 우리 팀에 어떻게 적용할지, 짧은 대화부터 시작하면 됩니다.
함께할 수 있는 일
Claude Code · GitHub Copilot
2일 핸즈온 + AI 채점 기반 사내 인증
AI 네이티브 전략
운영 표준·측정·거버넌스 재설계
웹 플랫폼
Next.js 기반 풀스택 서비스 구축