Skip to content

SK: 2832 Added Claude Code setup for Java #21

SK: 2832 Added Claude Code setup for Java

SK: 2832 Added Claude Code setup for Java #21

name: Claude PR Review
on:
pull_request:
branches: [main]
paths:
- 'src/**/*.java'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to re-run review on'
required: false
permissions:
pull-requests: write
contents: read
jobs:
sdk-review:
name: SDK PR Review (changed lines only)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude CLI
run: npm install -g @anthropic-ai/claude-code
- name: Check for non-generated Java changes
id: check
run: |
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
| grep '\.java$' | grep -v 'generated')
if [ -z "$CHANGED" ]; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
LINES=$(git diff origin/${{ github.base_ref }}...HEAD -- '*.java' \
| grep -c '^[+-][^+-]' 2>/dev/null || echo 1)
echo "changed_lines=$LINES" >> $GITHUB_OUTPUT
fi
- name: Verify Claude CLI
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "::error::ANTHROPIC_API_KEY secret is not set."
exit 1
fi
echo "Claude version: $(claude --version)"
set +e
TEST=$(claude --dangerously-skip-permissions --output-format json -p "Reply with only the word: WORKING" 2>&1)
EXIT=$?
set -e
IS_ERROR=$(echo "$TEST" | jq -r '.is_error // empty' 2>/dev/null)
if [ "$EXIT" -ne 0 ] || [ "$IS_ERROR" = "true" ]; then
MSG=$(echo "$TEST" | jq -r '.result // .error // .' 2>/dev/null | head -c 300)
echo "::error::Claude CLI check failed (invalid key, no API credits, or connectivity): $MSG"
exit 1
fi
echo "Claude CLI OK"
- name: Run code review
if: steps.check.outputs.has_changes == 'true'
id: review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
GITHUB_BASE_REF: ${{ github.base_ref }}
GITHUB_ACTIONS: "true"
run: |
set +e
RAW=$(claude --dangerously-skip-permissions --output-format json --model claude-sonnet-4-6 -p "/code-review" 2>&1)
CLAUDE_EXIT=$?
set -e
IS_ERROR=$(echo "$RAW" | jq -r '.is_error // empty' 2>/dev/null)
RESULT=$(echo "$RAW" | jq -r '.result // "_Review failed to produce output._"')
INPUT_TOK=$(echo "$RAW" | jq '[.modelUsage[].inputTokens // 0] | add // 0')
OUTPUT_TOK=$(echo "$RAW" | jq '[.modelUsage[].outputTokens // 0] | add // 0')
CACHED_TOK=$(echo "$RAW" | jq '[.modelUsage[].cacheReadInputTokens // 0] | add // 0')
COST=$(echo "$RAW" | jq -r '.total_cost_usd // 0')
LINES="${{ steps.check.outputs.changed_lines }}"
echo "result<<EOF" >> $GITHUB_OUTPUT
echo "$RESULT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
{
echo "## Token Usage — SDK Review"
echo "| Metric | Value |"
echo "|--------|-------|"
echo "| Input tokens | $INPUT_TOK |"
echo "| Output tokens | $OUTPUT_TOK |"
echo "| Cache hits | $CACHED_TOK |"
echo "| Total cost | \$$COST |"
echo "| Changed lines | $LINES |"
} >> $GITHUB_STEP_SUMMARY
# Fail the check on a real API/runtime error (invalid key, no credits, etc.)
# instead of posting the error text as a "review".
if [ "$CLAUDE_EXIT" -ne 0 ] || [ "$IS_ERROR" = "true" ]; then
echo "::error::Claude review failed (invalid key, no API credits, or runtime error): $(echo "$RESULT" | head -c 300)"
exit 1
fi
- name: Post review comment
if: steps.check.outputs.has_changes == 'true'
uses: actions/github-script@v7
env:
REVIEW_BODY: ${{ steps.review.outputs.result }}
with:
script: |
const raw = process.env.REVIEW_BODY || '_Review output unavailable._';
const pull_number = context.payload.pull_request.number;
// Extract inline findings, then strip the block so it never leaks into the comment.
// Primary format: HTML-comment sentinel <!-- ai-review-inline [...] --> (never renders,
// and backticks inside the JSON can't break extraction). Legacy fallback: ```json:inline fence.
let summary = raw;
let inline = [];
const html = raw.match(/<!--\s*ai-review-inline\s*([\s\S]*?)-->/);
const fenced = raw.match(/```+\s*json:inline\s*([\s\S]*?)```/);
const block = html || fenced;
if (block) { try { inline = JSON.parse(block[1].trim()); } catch (e) { inline = []; } }
// Strip the sentinel block and any stray json:inline fence (to end-of-text, since it must
// be last) so a malformed block can never appear in the visible summary.
summary = summary
.replace(/<!--\s*ai-review-inline[\s\S]*?-->/g, '')
.replace(/```+\s*json:inline[\s\S]*$/i, '')
.trim();
// Strip any leading model preamble/narration before the verdict line
// (tolerates markdown decoration like **REQUEST CHANGES** or a heading).
const lines = summary.split('\n');
const idx = lines.findIndex(l =>
/\b(REQUEST CHANGES|APPROVE WITH FIXES|APPROVE)\b/.test(l) || /^#{1,6}\s/.test(l));
if (idx > 0) summary = lines.slice(idx).join('\n').trim();
const comments = (Array.isArray(inline) ? inline : [])
.filter(f => f && f.path && Number.isInteger(f.line))
.map(f => ({
path: f.path,
line: f.line,
side: 'RIGHT',
body: `**${f.severity || 'Finding'}${f.category ? ' · ' + f.category : ''}**: ${f.comment || ''}`.trim()
}));
const body = `## AI Code Review\n\n${summary}`;
// One PR review: summary as the body + inline comments anchored to changed lines.
try {
await github.rest.pulls.createReview({
...context.repo,
pull_number,
event: 'COMMENT',
body,
comments
});
} catch (e) {
// Inline anchoring fails if a line is not in the diff — fall back to a single
// summary comment and list the would-be inline findings so nothing is lost.
const list = comments.length
? '\n\n<details><summary>Inline findings (could not attach to lines)</summary>\n\n'
+ comments.map(c => `- \`${c.path}:${c.line}\` — ${c.body}`).join('\n')
+ '\n</details>'
: '';
await github.rest.issues.createComment({
...context.repo,
issue_number: pull_number,
body: body + list
});
}