Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
201 changes: 201 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-present NVIDIA CORPORATION & AFFILIATES.
# All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause

name: Claude CLI PR Review
on:
pull_request:
types: [opened, synchronize, ready_for_review]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

run-name: Claude review for PR ${{ github.event.pull_request.number }} - ${{ github.event.pull_request.head.sha }}

jobs:
claude-code-review:
name: Run Claude Code Review
# Skip if PR is in draft
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
env:
CLAUDE_OUTPUT_DIR: artifacts/claude_review/${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}

steps:
- name: Checkout code
uses: actions/checkout@v4
Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: missing fetch-depth: 0 - without full git history, git merge-base in pr_preflight_launcher.py:85 will fail when computing the merge base between branches

Suggested change
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install Claude Code
run: |
npm install -g @anthropic-ai/claude-code
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH

- name: Install Claude Code Router
run: npm install -g @musistudio/claude-code-router@1.0.72

- name: Setup Claude Code Router config
run: |
mkdir -p $HOME/.claude-code-router
cat <<EOF > $HOME/.claude-code-router/config.json
{
"LOG": true,
"API_TIMEOUT_MS": 60000,
"NON_INTERACTIVE_MODE": true,
"Providers": [
{
"name": "nim",
"api_base_url": "https://integrate.api.nvidia.com/v1/chat/completions",
"api_key": "\$NIM_KEY",
"models": [
"moonshotai/kimi-k2-thinking",
"minimaxai/minimax-m2"
],
"transformer": {
"use": []
}
}
],
"Router": {
"default": "nim,moonshotai/kimi-k2-thinking"
},
"transformers": []
}
EOF
shell: bash

- name: Start Claude Code Router background service
env:
NIM_KEY: ${{ secrets.NIM_KEY }}
run: |
nohup ccr restart &
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: ccr restart requires router already running - use ccr start for initial launch

Suggested change
nohup ccr restart &
nohup ccr start &

Copy link
Contributor

Choose a reason for hiding this comment

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

logic: use ccr start for first-time initialization - restart requires router already running and will fail

Suggested change
nohup ccr restart &
nohup ccr start &

sleep 5 # Give it some time to start
shell: bash

- name: Check Claude CLI availability
run: |
echo "PATH=$PATH"
which claude || echo "claude not found on PATH"
claude --version || true
echo "npm prefix bin: $(npm config get prefix)/bin" || true

- name: Run Claude Code via wrapper
env:
ANTHROPIC_AUTH_TOKEN: 'test'
ANTHROPIC_API_KEY: ''
ANTHROPIC_BASE_URL: http://localhost:3456
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
python -m tools.pr_preflight_launcher --ai-backend claude --output-dir "${CLAUDE_OUTPUT_DIR}"

- name: Print Claude error (if any)
if: always()
run: |
if [ -f "${{ env.CLAUDE_OUTPUT_DIR }}/error.txt" ]; then
echo "===== Claude error.txt ====="
sed -n '1,200p' "${{ env.CLAUDE_OUTPUT_DIR }}/error.txt"
else
echo "No error.txt found in ${{ env.CLAUDE_OUTPUT_DIR }}"
fi

- name: Upload Claude review artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}
path: ${{ env.CLAUDE_OUTPUT_DIR }}/**

- name: Post Claude review to PR
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = require('path');

const outputDir = process.env.CLAUDE_OUTPUT_DIR;
const commentMarker = '<!-- claude-pr-review-bot -->';

// Check if review errored or has no output
const errorFile = path.join(outputDir, 'error.txt');
const successFile = path.join(outputDir, 'success_raw_output.txt');

// Skip posting if error occurred
if (fs.existsSync(errorFile)) {
console.log('Review encountered an error. Skipping comment post.');
return;
}

// Skip if no success output
if (!fs.existsSync(successFile)) {
console.log('No review output found. Skipping comment post.');
return;
}

// Note: We post even if verdict is FAILED because that helps
// PR developers address issues and improve their code

// Read review output
const reviewContent = fs.readFileSync(successFile, 'utf8');

// Prepare comment body
const commentBody = `${commentMarker}
## 🤖 Claude PR Review

**Commit:** ${context.payload.pull_request.head.sha}

${reviewContent}

---
*Review generated at ${new Date().toISOString()}*`;

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
});

const existingComment = comments.find(comment =>
comment.body && comment.body.includes(commentMarker)
);

// Create or update comment
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: commentBody,
});
console.log('Updated existing PR comment');
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: commentBody,
});
console.log('Created new PR comment');
}
165 changes: 165 additions & 0 deletions .github/workflows/gemini-cli-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Gemini CLI PR Review
on:
pull_request:
types: [opened, synchronize, ready_for_review]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

run-name: Gemini review for PR ${{ github.event.pull_request.number }} - ${{ github.event.pull_request.head.sha }}

jobs:
gemini-pr-review:
# Temporarily disabled to avoid burning API tokens
if: false
# Skip if PR is in draft
# if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
GEMINI_OUTPUT_DIR: artifacts/gemini_review/${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install Gemini CLI
shell: bash
run: |
npm install -g @google/gemini-cli@latest
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH

- name: Verify Gemini CLI
shell: bash
run: |
which gemini
gemini --version

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install/Upgrade Google Python Client
shell: bash
run: |
python -m pip install --upgrade pip
pip install --upgrade google-generativeai
# If you have a requirements.txt file, you might use this instead:
# pip install -r requirements.txt --upgrade

- name: Run Gemini PR review
env:
#GEMINI_MODEL: gemini-1.5-flash-latest
GEMINI_API_KEY: ${{ secrets.GEMINI_TEST }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
python -m tools.pr_preflight_launcher --ai-backend gemini --output-dir "${GEMINI_OUTPUT_DIR}"

- name: Print Gemini error (if any)
if: always()
run: |
if [ -f "${{ env.GEMINI_OUTPUT_DIR }}/error.txt" ]; then
echo "===== Gemini error.txt ====="
sed -n '1,200p' "${{ env.GEMINI_OUTPUT_DIR }}/error.txt"
else
echo "No error.txt found in ${{ env.GEMINI_OUTPUT_DIR }}"
fi

- name: Post Gemini review to PR
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = require('path');

const outputDir = process.env.GEMINI_OUTPUT_DIR;
const commentMarker = '<!-- gemini-pr-review-bot -->';

// Check if review failed or errored - if so, don't post comment
const errorFile = path.join(outputDir, 'error.txt');
const verdictFile = path.join(outputDir, 'review_verdict.txt');
const successFile = path.join(outputDir, 'success_raw_output.txt');

// Skip posting if error occurred
if (fs.existsSync(errorFile)) {
console.log('Review encountered an error. Skipping comment post.');
return;
}

// Skip posting if review failed
if (fs.existsSync(verdictFile)) {
const verdict = fs.readFileSync(verdictFile, 'utf8').trim();
if (verdict.includes('FAILED')) {
console.log('Review verdict: FAILED. Skipping comment post.');
return;
}
}

// Skip if no success output
if (!fs.existsSync(successFile)) {
console.log('No review output found. Skipping comment post.');
return;
}

// Read review output
const reviewContent = fs.readFileSync(successFile, 'utf8');

// Prepare comment body
const commentBody = `${commentMarker}
## 🤖 Gemini PR Review

**Commit:** ${context.payload.pull_request.head.sha}

${reviewContent}

---
*Review generated at ${new Date().toISOString()}*`;

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
});

const existingComment = comments.find(comment =>
comment.body && comment.body.includes(commentMarker)
);

// Create or update comment
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: commentBody,
});
console.log('Updated existing PR comment');
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: commentBody,
});
console.log('Created new PR comment');
}


Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: file ends with blank lines after closing brace - syntax error leaves JavaScript incomplete

Suggested change
}

Comment on lines +163 to +165
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: trailing blank lines after closing brace - the github-script block's JavaScript is incomplete and will cause syntax errors when the workflow runs

Suggested change
}
}

11 changes: 11 additions & 0 deletions .github/workflows/review_test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"action": "opened",
"pull_request": {
"draft": false,
"number": 5489,
"head": { "sha": "9f9a6b2f91519b3dc02fe7ec7a5f2a3b98398338", "ref": "feature/branch" },
"base": { "sha": "f8b8551a720cd5c3a9aa8950e5a50fb7d420cbe5", "ref": "main" }
}
}


Loading
Loading