Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: "Claude Code Review"
on:
pull_request:
types: [ opened, reopened, synchronize ]
jobs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[개선] concurrency 설정 누락

synchronize 이벤트가 짧은 간격으로 여러 번 발생하면(빠른 연속 커밋) 여러 워크플로우가 동시에 실행될 수 있습니다. 이 경우 삭제 → 리뷰 생성 단계가 서로 경쟁하여 예상치 못한 동작이 발생할 수 있습니다.

concurrency 설정을 추가하면 동일 PR에 대한 이전 실행을 자동으로 취소할 수 있습니다:

Suggested change
jobs:
jobs:
concurrency:
group: code-review-${{ github.event.pull_request.number }}
cancel-in-progress: true

(들여쓰기 없이 jobs: 와 같은 레벨에 위치해야 합니다.)

review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Delete Previous Claude Reviews
if: github.event.action == 'synchronize'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set +e # Continue on error
PR_NUMBER=${{ github.event.pull_request.number }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[보안] 스크립트 인젝션 취약점

${{ ... }} 표현식을 run 블록 내에 직접 보간하면 스크립트 인젝션 위험이 있습니다. GitHub Actions 공식 보안 가이드에서는 컨텍스트 값을 환경 변수로 전달할 것을 권장합니다.

github.event.pull_request.number는 정수이므로 현재는 안전하지만, 패턴 자체가 위험한 습관입니다.

Suggested change
PR_NUMBER=${{ github.event.pull_request.number }}
PR_NUMBER=$PR_NUMBER_ENV
REPO=$REPO_ENV

env 블록에 다음을 추가하세요:

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  PR_NUMBER_ENV: ${{ github.event.pull_request.number }}
  REPO_ENV: ${{ github.repository }}

REPO=${{ github.repository }}
echo "🔍 이전 Claude 리뷰 검색 중..."
# 1. PR 코멘트(요약) 삭제 - github-actions[bot]이 작성한 코멘트
gh api "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]") | .id' \
| while read comment_id; do
if [ -n "$comment_id" ]; then
echo "🗑️ PR 코멘트 삭제: $comment_id"
gh api -X DELETE "repos/$REPO/issues/comments/$comment_id" 2>/dev/null || echo "⚠️ 삭제 실패 (무시)"
sleep 0.3
fi
done
# 2. 인라인 리뷰 코멘트 삭제
# - 미해결 스레드 중 bot 코멘트만 있는 스레드만 삭제
# - 사용자 답글이 있는 스레드는 삭제하지 않음 (코드만 덩그러니 남는 문제 방지)
OWNER=${REPO%/*}
NAME=${REPO#*/}
gh api graphql -F owner="$OWNER" -F name="$NAME" -F number=$PR_NUMBER -f query='
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
reviewThreads(last: 100) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제한] GraphQL 페이지네이션 미처리

reviewThreads(last: 100)은 최대 100개의 스레드만 가져옵니다. 리뷰 스레드가 100개를 초과하는 대형 PR에서는 오래된 스레드가 삭제되지 않고 남을 수 있습니다.

필요하다면 pageInfohasNextPage/endCursor를 이용한 페이지네이션을 추가하는 것을 고려해주세요. (일반적인 PR에서는 100개면 충분할 수 있으므로, 트레이드오프를 고려하여 판단하세요.)

nodes {
isResolved
comments(first: 50) {
nodes {
databaseId
author {
login
}
}
}
}
}
}
}
}' \
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | select([.comments.nodes[].author.login] | all(. == "github-actions")) | .comments.nodes[].databaseId' \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[버그] bot 로그인 식별자 불일치

25번째 줄에서는 "github-actions[bot]"을 사용하고, 여기서는 "github-actions"를 사용하고 있습니다. GitHub REST API와 GraphQL API에서 bot 계정의 login 필드가 다르게 반환될 수 있어 한쪽 삭제 로직이 동작하지 않을 수 있습니다.

  • REST API: user.login == "github-actions[bot]"
  • GraphQL API: author.login은 보통 "github-actions[bot]" 또는 "app/github-actions"

GraphQL에서 실제 반환값을 확인하고 일치시켜야 합니다.

Suggested change
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | select([.comments.nodes[].author.login] | all(. == "github-actions")) | .comments.nodes[].databaseId' \
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | select([.comments.nodes[].author.login] | all(. == "github-actions[bot]")) | .comments.nodes[].databaseId' \

| while read comment_id; do
if [ -n "$comment_id" ]; then
echo "🗑️ 인라인 코멘트 삭제: $comment_id"
gh api -X DELETE "repos/$REPO/pulls/comments/$comment_id" 2>/dev/null || echo "⚠️ 삭제 실패 (무시)"
sleep 0.3
fi
done
echo "✅ 이전 리뷰 삭제 완료"
- name: Run Claude Code Review
uses: anthropics/claude-code-action@v1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[보안] 액션 버전 고정 권장

@v1과 같은 가변 태그는 태그가 다른 커밋을 가리키도록 변경될 경우 예기치 않은 코드가 실행될 수 있습니다(공급망 공격). 특히 외부 써드파티 액션은 특정 커밋 SHA로 고정하는 것이 더 안전합니다.

Suggested change
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@da822ce4b5bbccd4e0cf9e4cd1c4c6fb9a61f531 # v1

SHA는 사용 시점의 최신 v1 태그 커밋을 확인하여 업데이트하세요.

with:
show_full_output: true
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
이 PR을 리뷰하고 코멘트로 작성해주세요.
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"