Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
187 changes: 187 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# 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 npm dependencies
run: |
npm install -g @anthropic-ai/claude-code @musistudio/claude-code-router@1.0.72
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH

- name: Setup and start Claude Code Router
env:
NIM_KEY: ${{ secrets.NIM_KEY }}
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
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
shell: bash

- 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" }
}
}


25 changes: 25 additions & 0 deletions tools/ai_cli_docker_test/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# start from the official, clean Node.js 20 image (Debian-based)
# Gemini seems to go haywire with older node versions (e.g. 18)
FROM node:20

COPY pr_preflight_launcher.py /usr/local/bin/
COPY utils.py /usr/local/bin/tools/
COPY git_helpers.py /usr/local/bin/tools/
COPY ai_cli_wrapper.py /usr/local/bin/tools/

# Add Python 3 and vim
RUN apt-get update && apt-get install -y python3 python3-pip vim

# Install the latest Gemini CLI globally inside the container
RUN npm install -g @google/gemini-cli --no-update-notifier

RUN node -v
RUN python3 --version
RUN gemini --version

# 5. Set a working directory (good practice)
WORKDIR /app

# 6. Set the default command. When the container starts,
# it will just run "bash", giving you an interactive shell.
CMD ["/bin/bash"]
5 changes: 5 additions & 0 deletions tools/ai_cli_docker_test/build_ai_cli_docker
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

# run me from the 'ai_cli_docker_test' directory

(cd .. && docker build -f ai_cli_docker_test/Dockerfile -t ai_cli_docker_test_shell .)
Loading
Loading