diff --git a/.github/scripts/claude-review-run.sh b/.github/scripts/claude-review-run.sh new file mode 100755 index 0000000..3465152 --- /dev/null +++ b/.github/scripts/claude-review-run.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="$HOME/.local/bin:$PATH" + +REVIEW_FILE="/tmp/claude-review-${PR_NUMBER}.json" + +PROMPT_FILE=$(mktemp /tmp/claude-review-prompt.XXXXXX.txt) +trap 'rm -f "$PROMPT_FILE"' EXIT + +cat > "$PROMPT_FILE" < |\n| ๐Ÿ” Regression Risk | <๐Ÿ”ด ๋งค์šฐ ๋†’์Œ / ๐ŸŸก ๋ณดํ†ต / ๐ŸŸข ๋‚ฎ์Œ + ํ•œ ์ค„ ์ด์œ > |\n| โš ๏ธ Risky Changes | <์žˆ์Œ: ๋‚ด์šฉ ์š”์•ฝ / ์—†์Œ> |\n| ๐Ÿงช Missing Tests | <์žˆ์Œ: ๋‚ด์šฉ ์š”์•ฝ / ์—†์Œ> |\n\n### ๐Ÿ“ ์ƒ์„ธ ๋ฆฌ๋ทฐ\n\n<์ƒ์„ธ ๋ฆฌ๋ทฐ ๋‚ด์šฉ์„ ๋งˆํฌ๋‹ค์šด์œผ๋กœ ์ž‘์„ฑ. ํŒŒ์ผ๋ช…, ๋ผ์ธ, ๊ทผ๊ฑฐ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ>", + "event": "COMMENT", + "comments": [ + { + "path": "relative/path/to/file.java", + "line": , + "side": "RIGHT", + "body": "" + } + ] +} + +7. Post the review: + gh api /repos/eottabom/sandbox-load-test/pulls/${PR_NUMBER}/reviews --method POST --input ${REVIEW_FILE} + +Rules: +- Only add inline comments on lines that exist in the diff (added or changed lines on the RIGHT side). +- The 'line' must be the actual line number in the current file, not the diff position. +- If there are no specific inline findings, use an empty array for 'comments'. +- If there are no findings at all, still post a summary saying so. +- Write everything in Korean. +PROMPT_EOF + +claude --dangerously-skip-permissions --verbose --output-format stream-json < "$PROMPT_FILE" diff --git a/.github/scripts/codex-review-run.sh b/.github/scripts/codex-review-run.sh new file mode 100755 index 0000000..7d51392 --- /dev/null +++ b/.github/scripts/codex-review-run.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="$HOME/.local/bin:$PATH" +: "${RUNNER_WORKDIR:?RUNNER_WORKDIR env var must be set}" + +REVIEW_FILE="/tmp/codex-review-${PR_NUMBER}.json" + +PROMPT_FILE=$(mktemp /tmp/codex-review-prompt.XXXXXX.txt) +trap 'rm -f "$PROMPT_FILE"' EXIT + +cat > "$PROMPT_FILE" < |\n| ๐Ÿ” Regression Risk | <๐Ÿ”ด ๋งค์šฐ ๋†’์Œ / ๐ŸŸก ๋ณดํ†ต / ๐ŸŸข ๋‚ฎ์Œ + ํ•œ ์ค„ ์ด์œ > |\n| โš ๏ธ Risky Changes | <์žˆ์Œ: ๋‚ด์šฉ ์š”์•ฝ / ์—†์Œ> |\n| ๐Ÿงช Missing Tests | <์žˆ์Œ: ๋‚ด์šฉ ์š”์•ฝ / ์—†์Œ> |\n\n### ๐Ÿ“ ์ƒ์„ธ ๋ฆฌ๋ทฐ\n\n<์ƒ์„ธ ๋ฆฌ๋ทฐ ๋‚ด์šฉ์„ ๋งˆํฌ๋‹ค์šด์œผ๋กœ ์ž‘์„ฑ. ํŒŒ์ผ๋ช…, ๋ผ์ธ, ๊ทผ๊ฑฐ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ>", + "event": "COMMENT", + "comments": [ + { + "path": "relative/path/to/file.java", + "line": , + "side": "RIGHT", + "body": "" + } + ] +} + +7. Post the review: + gh api /repos/eottabom/sandbox-load-test/pulls/${PR_NUMBER}/reviews --method POST --input ${REVIEW_FILE} + +Rules: +- Only add inline comments on lines that exist in the diff (added or changed lines on the RIGHT side). +- The 'line' must be the actual line number in the current file, not the diff position. +- If there are no specific inline findings, use an empty array for 'comments'. +- If there are no findings at all, still post a summary saying so. +- Write everything in Korean. +PROMPT_EOF + +codex exec \ + -C "$RUNNER_WORKDIR" \ + --dangerously-bypass-approvals-and-sandbox \ + - < "$PROMPT_FILE" diff --git a/.github/workflows/pr-auto-review.yml b/.github/workflows/pr-auto-review.yml new file mode 100644 index 0000000..b444c2c --- /dev/null +++ b/.github/workflows/pr-auto-review.yml @@ -0,0 +1,136 @@ +name: ๐Ÿค– PR Auto Review + +on: + pull_request: + types: [opened, ready_for_review] + +permissions: + contents: read + pull-requests: write + +jobs: + claude-review: + if: | + github.event.pull_request.draft == false && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) + timeout-minutes: 30 + concurrency: + group: claude-review-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + runs-on: [self-hosted, macOS, X64] + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + + defaults: + run: + working-directory: ${{ vars.RUNNER_WORKDIR }} + + steps: + - name: Checkout PR head + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/eottabom/sandbox-load-test.git" + git fetch origin "refs/pull/${PR_NUMBER}/head" + git checkout --detach FETCH_HEAD + git reset --hard FETCH_HEAD + test "$(git rev-parse HEAD)" = "$HEAD_SHA" + git remote set-url origin "https://github.com/eottabom/sandbox-load-test.git" + + - name: Check Claude login status + id: claude_auth + env: + HOME: ${{ vars.RUNNER_HOME }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export PATH="$HOME/.local/bin:$PATH" + if claude auth status 2>/dev/null | jq -e '.loggedIn == true' >/dev/null; then + echo "logged_in=true" >> "$GITHUB_OUTPUT" + else + echo "logged_in=false" >> "$GITHUB_OUTPUT" + gh pr comment "$PR_NUMBER" --repo eottabom/sandbox-load-test --body "โš ๏ธ **eottabom-review-bot** self-hosted ๋Ÿฌ๋„ˆ์˜ \`claude\` CLI ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์–ด ์ž๋™ ๋ฆฌ๋ทฐ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Ÿฌ๋„ˆ ๋จธ์‹ ์—์„œ \`claude /login\`์œผ๋กœ ์žฌ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”." + fi + + - name: Run Claude review + if: steps.claude_auth.outputs.logged_in == 'true' + env: + HOME: ${{ vars.RUNNER_HOME }} + CI: "" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + unset CI + unset GITHUB_ACTIONS + unset GITHUB_TOKEN + set +e + set -o pipefail + bash "${{ vars.RUNNER_WORKDIR }}/.github/scripts/claude-review-run.sh" 2>&1 | \ + jq --unbuffered -r ' + if .type == "assistant" then + (.message.content // [])[] | + if .type == "text" then .text + elif .type == "tool_use" then "[tool:\(.name)] \(.input | tostring | .[0:200])" + else empty end + elif .type == "tool_result" then + " โ†’ \((.content // [])[] | select(.type == "text") | .text | .[0:300])" + elif .type == "result" then + "\n=== \(.subtype) ===" + else empty end + ' 2>/dev/null + rc="${PIPESTATUS[0]}" + set -e + exit "$rc" + + codex-review: + if: | + github.event.pull_request.draft == false && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) + timeout-minutes: 30 + concurrency: + group: codex-review-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + runs-on: [self-hosted, macOS, X64] + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + + defaults: + run: + working-directory: ${{ vars.RUNNER_WORKDIR }} + + steps: + - name: Checkout PR head + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/eottabom/sandbox-load-test.git" + git fetch origin "refs/pull/${PR_NUMBER}/head" + git checkout --detach FETCH_HEAD + git reset --hard FETCH_HEAD + test "$(git rev-parse HEAD)" = "$HEAD_SHA" + git remote set-url origin "https://github.com/eottabom/sandbox-load-test.git" + + - name: Check Codex login status + id: codex_auth + env: + HOME: ${{ vars.RUNNER_HOME }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export PATH="$HOME/.local/bin:$PATH" + if codex login status 2>/dev/null | grep -qi "logged in"; then + echo "logged_in=true" >> "$GITHUB_OUTPUT" + else + echo "logged_in=false" >> "$GITHUB_OUTPUT" + gh pr comment "$PR_NUMBER" --repo eottabom/sandbox-load-test --body "โš ๏ธ **eottabom-review-bot** self-hosted ๋Ÿฌ๋„ˆ์˜ \`codex\` CLI ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์–ด ์ž๋™ ๋ฆฌ๋ทฐ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Ÿฌ๋„ˆ ๋จธ์‹ ์—์„œ \`codex login\`์œผ๋กœ ์žฌ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”." + fi + + - name: Run Codex review + if: steps.codex_auth.outputs.logged_in == 'true' + env: + HOME: ${{ vars.RUNNER_HOME }} + CI: "" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUNNER_WORKDIR: ${{ vars.RUNNER_WORKDIR }} + run: | + unset CI + bash "${{ vars.RUNNER_WORKDIR }}/.github/scripts/codex-review-run.sh" diff --git a/.github/workflows/pr-mention-review.yml b/.github/workflows/pr-mention-review.yml new file mode 100644 index 0000000..df6d6d1 --- /dev/null +++ b/.github/workflows/pr-mention-review.yml @@ -0,0 +1,191 @@ +name: ๐Ÿ’ฌ PR Mention Re-review + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + +jobs: + claude-review: + if: | + contains(github.event.comment.body, '@eottabom-review-bot') && + github.event.issue.pull_request != null && + github.event.comment.user.type != 'Bot' && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + timeout-minutes: 30 + concurrency: + group: claude-review-pr-${{ github.event.issue.number }} + cancel-in-progress: true + runs-on: [self-hosted, macOS, X64] + env: + PR_NUMBER: ${{ github.event.issue.number }} + + defaults: + run: + working-directory: ${{ vars.RUNNER_WORKDIR }} + + steps: + - name: React with eyes emoji + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes' + }); + + - name: Get PR info + id: pr_info + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number + }); + core.setOutput('base_ref', pr.base.ref); + core.setOutput('head_sha', pr.head.sha); + + - name: Checkout PR head + env: + HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }} + run: | + git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/eottabom/sandbox-load-test.git" + git fetch origin "refs/pull/${PR_NUMBER}/head" + git checkout --detach FETCH_HEAD + git reset --hard FETCH_HEAD + test "$(git rev-parse HEAD)" = "$HEAD_SHA" + git remote set-url origin "https://github.com/eottabom/sandbox-load-test.git" + + - name: Check Claude login status + id: claude_auth + env: + HOME: ${{ vars.RUNNER_HOME }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export PATH="$HOME/.local/bin:$PATH" + if claude auth status 2>/dev/null | jq -e '.loggedIn == true' >/dev/null; then + echo "logged_in=true" >> "$GITHUB_OUTPUT" + else + echo "logged_in=false" >> "$GITHUB_OUTPUT" + gh pr comment "$PR_NUMBER" --repo eottabom/sandbox-load-test --body "โš ๏ธ **eottabom-review-bot** self-hosted ๋Ÿฌ๋„ˆ์˜ \`claude\` CLI ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์–ด ์žฌ๋ฆฌ๋ทฐ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Ÿฌ๋„ˆ ๋จธ์‹ ์—์„œ \`claude /login\`์œผ๋กœ ์žฌ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”." + fi + + - name: Run Claude review + if: steps.claude_auth.outputs.logged_in == 'true' + env: + HOME: ${{ vars.RUNNER_HOME }} + CI: "" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_REF: ${{ steps.pr_info.outputs.base_ref }} + run: | + unset CI + unset GITHUB_ACTIONS + unset GITHUB_TOKEN + set +e + bash "${{ vars.RUNNER_WORKDIR }}/.github/scripts/claude-review-run.sh" 2>&1 | \ + jq --unbuffered -r ' + if .type == "assistant" then + (.message.content // [])[] | + if .type == "text" then .text + elif .type == "tool_use" then "[tool:\(.name)] \(.input | tostring | .[0:200])" + else empty end + elif .type == "tool_result" then + " โ†’ \((.content // [])[] | select(.type == "text") | .text | .[0:300])" + elif .type == "result" then + "\n=== \(.subtype) ===" + else empty end + ' 2>/dev/null + rc="${PIPESTATUS[0]}" + set -e + exit "$rc" + + codex-review: + if: | + contains(github.event.comment.body, '@eottabom-review-bot') && + github.event.issue.pull_request != null && + github.event.comment.user.type != 'Bot' && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + timeout-minutes: 30 + concurrency: + group: codex-review-pr-${{ github.event.issue.number }} + cancel-in-progress: true + runs-on: [self-hosted, macOS, X64] + env: + PR_NUMBER: ${{ github.event.issue.number }} + + defaults: + run: + working-directory: ${{ vars.RUNNER_WORKDIR }} + + steps: + - name: React with eyes emoji + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes' + }); + + - name: Get PR info + id: pr_info + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number + }); + core.setOutput('base_ref', pr.base.ref); + core.setOutput('head_sha', pr.head.sha); + + - name: Checkout PR head + env: + HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }} + run: | + git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/eottabom/sandbox-load-test.git" + git fetch origin "refs/pull/${PR_NUMBER}/head" + git checkout --detach FETCH_HEAD + git reset --hard FETCH_HEAD + test "$(git rev-parse HEAD)" = "$HEAD_SHA" + git remote set-url origin "https://github.com/eottabom/sandbox-load-test.git" + + - name: Check Codex login status + id: codex_auth + env: + HOME: ${{ vars.RUNNER_HOME }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export PATH="$HOME/.local/bin:$PATH" + if codex login status 2>/dev/null | grep -qi "logged in"; then + echo "logged_in=true" >> "$GITHUB_OUTPUT" + else + echo "logged_in=false" >> "$GITHUB_OUTPUT" + gh pr comment "$PR_NUMBER" --repo eottabom/sandbox-load-test --body "โš ๏ธ **eottabom-review-bot** self-hosted ๋Ÿฌ๋„ˆ์˜ \`codex\` CLI ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์–ด ์žฌ๋ฆฌ๋ทฐ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Ÿฌ๋„ˆ ๋จธ์‹ ์—์„œ \`codex login\`์œผ๋กœ ์žฌ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”." + fi + + - name: Run Codex review + if: steps.codex_auth.outputs.logged_in == 'true' + env: + HOME: ${{ vars.RUNNER_HOME }} + CI: "" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_REF: ${{ steps.pr_info.outputs.base_ref }} + RUNNER_WORKDIR: ${{ vars.RUNNER_WORKDIR }} + run: | + unset CI + bash "${{ vars.RUNNER_WORKDIR }}/.github/scripts/codex-review-run.sh"