본문 바로가기
Computer/Android&iOS

git-push > AI 리뷰 2탄

by ywlee 2026. 5. 6.

1탄 https://marlboroyw.tistory.com/623

 

git-push > AI 코드리뷰 도입기

배경팀 프로젝트를 진행하면서 PR 코드리뷰에 드는 시간이 점점 부담이 됐다.사람이 직접 리뷰하기 전에, 아키텍처 패턴 위반이나 명백한 버그 같은 것들을 먼저 걸러줄 수 있다면 리뷰 품질이

marlboroyw.tistory.com

 

1탄에서는 claude code 로만 리뷰를 진행했는데, 

생각해보니 

- claude code

- gemini cli

- codex 

 

3개 모두 활용하면 좋을것 같아서 스크립트를 변경했다. 

 

일단, local.properties 에서 각각 ai 에게 review 를 받을지 여부를 flag 로 관리하게 해 놓았고, 

# push AI code review flag
use.claude.review=true
use.gemini.review=true
use.codex.review=true

# code review result to SLACK flag
use.notify.slack=true

# Slack Bot Token
slack.bot.token=xoxb-your-token

# Slack channel id
slack.channel.id=your-channel

 

 

 

이 상태에서 push 하게 되면, nohup 으로 백그라운드로 리뷰를 진행한다.

 

 

완료시 아래와 같이 slack 으로 3개의 리뷰를 전송하게 해 놓았다.

 

 

스레드에 댓글 형식으로 남기게 해놓았다.

 

 

이렇게 각각의 3개 리뷰가 하나의 푸시 메세지에 대한 리뷰를 슬랙 댓글로 남기게 된다. 

 

아래는 전체 pre-push-hook, ai-review 스크립트 전문이다.

 

 

#!/bin/sh

echo "🧪 Pre-push: 단위 테스트 실행 중..."

./gradlew :feature:motion:testDebugUnitTest :motion:testDebugUnitTest --quiet 2>&1

if [ $? -ne 0 ]; then
    echo "❌ 테스트 실패 — push 중단"
    exit 1
fi

echo "✅ 테스트 통과"

echo "🔨 Pre-push: 빌드 검증 중..."

./gradlew :app-kneefresh-vsc:assembleDebug --quiet 2>&1

if [ $? -ne 0 ]; then
    echo "❌ 빌드 실패 — push 중단"
    exit 1
fi

echo "✅ 빌드 성공"

# AI 리뷰는 백그라운드로 — push를 막지 않음
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{upstream} 2>/dev/null || echo "origin/develop")
DIFF=$(git diff "${UPSTREAM}"...HEAD -- \
    "*.kt" "*.kts" "*.gradle" \
    "*AndroidManifest.xml" \
    "gradle.properties" "*/gradle.properties" \
    "*/gradle-wrapper.properties" \
    "*.pro" \
    2>/dev/null | head -n 600)

if [ -z "$DIFF" ]; then
    echo "ℹ️  변경된 파일 없음 — AI 리뷰 스킵"
else
    TMP_DIFF=$(mktemp)
    printf '%s\n' "$DIFF" > "$TMP_DIFF"

    BASE_SHA=$(git merge-base HEAD "${UPSTREAM}" 2>/dev/null)
    BASE_BRANCH=$(echo "$UPSTREAM" | sed 's|^[^/]*/||')
    BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
    AUTHOR=$(git config user.name 2>/dev/null)
    REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)

    HOOK_DIR=$(cd "$(dirname "$0")" && pwd)

    nohup sh "${HOOK_DIR}/ai-review.sh" \
        "$TMP_DIFF" "$BASE_SHA" "$BASE_BRANCH" "$BRANCH" "$AUTHOR" "$REPO_ROOT" \
        </dev/null >>"${TMPDIR:-/tmp}/athome-ai-review.log" 2>&1 &

    echo "🤖 AI 코드 리뷰 백그라운드 실행 중... (완료 시 Slack 알림)"
fi

echo "✅ Push 진행"
exit 0

 

ai-review hook

#!/bin/sh
# 백그라운드 AI 코드 리뷰 스크립트
# pre-push hook에서 nohup으로 실행됨

TMP_DIFF="$1"
BASE_SHA="$2"
BASE_BRANCH="$3"
BRANCH="$4"
AUTHOR="$5"
REPO_ROOT="$6"

# 프로젝트 루트로 이동 (local.properties 접근을 위해)
cd "$REPO_ROOT" || exit 1

# local.properties에서 값을 읽는 헬퍼
prop_enabled() {
    val=$(grep "^${1}=" local.properties 2>/dev/null | cut -d'=' -f2-)
    [ "$val" = "false" ] && echo "false" || echo "true"
}
prop_value() {
    grep "^${1}=" local.properties 2>/dev/null | cut -d'=' -f2-
}

USE_CLAUDE=$(prop_enabled "use.claude.review")
USE_GEMINI=$(prop_enabled "use.gemini.review")
USE_CODEX=$(prop_enabled "use.codex.review")
USE_SLACK=$(prop_enabled "use.notify.slack")

DIFF=$(cat "$TMP_DIFF")
rm -f "$TMP_DIFF"

if [ -z "$DIFF" ]; then
    exit 0
fi

TMP_PROMPT=$(mktemp)
TMP_CLAUDE=$(mktemp); TMP_CLAUDE_ERR=$(mktemp)
TMP_GEMINI=$(mktemp); TMP_GEMINI_ERR=$(mktemp)
TMP_CODEX=$(mktemp);  TMP_CODEX_ERR=$(mktemp)

cat > "$TMP_PROMPT" << 'PROMPT_EOF'
You are reviewing Android project changes including Kotlin source, Gradle build scripts, and AndroidManifest.

Architecture rules (Kotlin):
- Composable layers must follow: Destination → Screen → View
- View composables must NOT have default values for the modifier parameter
- Each screen has a contract/ package with XxxEvent, XxxState, XxxEffect
- Two BaseVM implementations coexist (BaseVM with runningFold, BaseViewModel with MutableStateFlow) — flag inconsistency within a feature

Review Kotlin (.kt) for:
1. Architecture pattern violations
2. Potential bugs or missing error handling
3. Memory leaks (context leaks, unregistered listeners, coroutine scope issues)
4. Code quality issues

Review AndroidManifest.xml for:
1. Dangerous permissions added without necessity
2. Components exported=true without proper permission protection
3. Intent filters that may expose unintended attack surface
4. Missing or incorrect security-related attributes (allowBackup, usesCleartextTraffic, etc.)

Review build scripts (.kts / .gradle) and gradle.properties for:
1. Dependency version downgrades or known-vulnerable versions
2. Debug/test configurations leaking into release builds
3. Signing config or ProGuard/R8 rule changes that could affect security
4. SDK version changes (minSdk, targetSdk, compileSdk) and their implications
5. Gradle wrapper version changes

Respond in Korean. Format:
🔴 심각: [내용]
🟡 주의: [내용]
🟢 양호: [내용]
If nothing to flag, just say "🟢 전반적으로 양호합니다"

Diff:
PROMPT_EOF
printf '%s\n' "$DIFF" >> "$TMP_PROMPT"

# 병렬 실행
PIDS=""

if [ "$USE_CLAUDE" = "true" ] && command -v claude > /dev/null 2>&1; then
    claude -p "$(cat "$TMP_PROMPT")" > "$TMP_CLAUDE" 2>"$TMP_CLAUDE_ERR" &
    PIDS="$PIDS $!"
fi

if [ "$USE_GEMINI" = "true" ] && command -v gemini > /dev/null 2>&1; then
    gemini --skip-trust -p "$(cat "$TMP_PROMPT")" > "$TMP_GEMINI" 2>"$TMP_GEMINI_ERR" &
    PIDS="$PIDS $!"
fi

if [ "$USE_CODEX" = "true" ] && command -v codex > /dev/null 2>&1; then
    if [ -n "$BASE_SHA" ]; then
        codex review --base "$BASE_SHA" > "$TMP_CODEX" 2>"$TMP_CODEX_ERR" &
    else
        codex review > "$TMP_CODEX" 2>"$TMP_CODEX_ERR" &
    fi
    PIDS="$PIDS $!"
fi

[ -n "$PIDS" ] && wait $PIDS 2>/dev/null

# ── Slack 스레드 전송 ────────────────────────────────────
if [ "$USE_SLACK" = "true" ]; then
    SLACK_BOT_TOKEN=$(prop_value "slack.bot.token")
    SLACK_CHANNEL_ID=$(prop_value "slack.channel.id")

    if [ -n "$SLACK_BOT_TOKEN" ] && [ -n "$SLACK_CHANNEL_ID" ]; then

        # GitHub compare 링크 — SHA 기반
        REMOTE_URL=$(git remote get-url origin 2>/dev/null)
        REPO_URL=$(printf '%s' "$REMOTE_URL" \
            | sed 's|git@\([^:]*\):\(.*\)\.git$|https://\1/\2|; s|\.git$||')
        COMPARE_URL=""
        [ -n "$REPO_URL" ] && [ -n "$BASE_SHA" ] && \
            COMPARE_URL="${REPO_URL}/compare/${BASE_SHA}...${BRANCH}"

        # 파일 내용 → JSON 문자열 인코딩
        json_escape() {
            cat "$1" | head -c 2800 \
                | sed 's/\\/\\\\/g; s/"/\\"/g' \
                | awk '{printf "%s\\n", $0}' \
                | sed 's/\\n$//'
        }

        # 1. 초기 메시지
        ESC_AUTHOR=$(printf '%s' "$AUTHOR" | sed 's/\\/\\\\/g; s/"/\\"/g')
        ESC_BRANCH=$(printf '%s' "$BRANCH" | sed 's/\\/\\\\/g; s/"/\\"/g')
        if [ -n "$COMPARE_URL" ]; then
            HEADER_TEXT="🔍 *${ESC_AUTHOR}의 Push에 대한 AI 리뷰*\\n*브랜치:* \`${ESC_BRANCH}\`  |  <${COMPARE_URL}|📎 Diff 보기>"
        else
            HEADER_TEXT="🔍 *${ESC_AUTHOR}의 Push에 대한 AI 리뷰*\\n*브랜치:* \`${ESC_BRANCH}\`"
        fi

        RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
            -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
            -H "Content-Type: application/json; charset=utf-8" \
            -d "{\"channel\":\"${SLACK_CHANNEL_ID}\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"${HEADER_TEXT}\"}}]}")

        TS=$(printf '%s' "$RESPONSE" | grep -o '"ts":"[^"]*"' | head -n1 | cut -d'"' -f4)

        if [ -n "$TS" ]; then
            # 2. 각 AI 리뷰를 스레드 답글로 전송
            post_thread() {
                icon="$1"; label="$2"; tmpout="$3"
                [ -s "$tmpout" ] || return
                ESC=$(json_escape "$tmpout")
                curl -s -X POST "https://slack.com/api/chat.postMessage" \
                    -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
                    -H "Content-Type: application/json; charset=utf-8" \
                    -d "{\"channel\":\"${SLACK_CHANNEL_ID}\",\"thread_ts\":\"${TS}\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"${icon}  *${label}*\\n\\n${ESC}\"}}]}" \
                    > /dev/null
            }

            [ "$USE_CLAUDE" = "true" ] && command -v claude > /dev/null 2>&1 && \
                post_thread "🤖" "Claude" "$TMP_CLAUDE"

            [ "$USE_GEMINI" = "true" ] && command -v gemini > /dev/null 2>&1 && \
                post_thread "✨" "Gemini" "$TMP_GEMINI"

            [ "$USE_CODEX"  = "true" ] && command -v codex  > /dev/null 2>&1 && \
                post_thread "⚡" "Codex"  "$TMP_CODEX"
        fi
    fi
fi

rm -f "$TMP_PROMPT" \
      "$TMP_CLAUDE" "$TMP_CLAUDE_ERR" \
      "$TMP_GEMINI" "$TMP_GEMINI_ERR" \
      "$TMP_CODEX"  "$TMP_CODEX_ERR"