SK: 2832 Added Claude Code setup for Java #21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); | |
| } |