Skip to content

Instantly share code, notes, and snippets.

@ghiculescu
Created May 15, 2025 05:17
Show Gist options
  • Save ghiculescu/d8804931ddb7bb9af7de58ab81aac1de to your computer and use it in GitHub Desktop.
Save ghiculescu/d8804931ddb7bb9af7de58ab81aac1de to your computer and use it in GitHub Desktop.
Code Review by Claude Code using Cursor Rules
name: 'Claude Code Review'
on:
pull_request:
types: [labeled, synchronize, ready_for_review]
paths:
- 'app/**/**'
# This section enables workflow concurrency and cancellation
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
claude_code_review:
name: Run Claude Code Review
runs-on: PIT-198-8cpu
permissions:
contents: read
pull-requests: write
steps:
- name: Check for ready-for-review label and draft status
id: check_label
uses: actions/github-script@v6
with:
github-token: ${{ github.token }}
script: |
// Get PR information to check draft status
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
// Check if the PR is in draft mode
if (pr.draft) {
console.log('PR is in draft mode, skipping review');
return core.setOutput('has_label', 'false');
}
// For newly added label
if (context.payload.action === 'labeled' && context.payload.label.name === 'ready-for-review') {
console.log('PR was just labeled with ready-for-review and is not in draft mode');
return core.setOutput('has_label', 'true');
}
// Otherwise, check if label exists
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const hasLabel = labels.some(label => label.name === 'ready-for-review');
console.log(`PR sync: has ready-for-review label? ${hasLabel}, is draft? ${pr.draft}`);
return core.setOutput('has_label', hasLabel ? 'true' : 'false');
# Checkout the code only if the label check passes
- uses: actions/checkout@v4
if: steps.check_label.outputs.has_label == 'true'
with:
ref: ${{ github.head_ref }}
- name: Install Claude Code
if: steps.check_label.outputs.has_label == 'true'
run: npm install -g @anthropic-ai/claude-code
- name: Find previous comment
if: steps.check_label.outputs.has_label == 'true'
id: find_comment
uses: actions/github-script@v6
with:
github-token: ${{ github.token }}
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// Find comment from the bot
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Claude Code Review')
);
if (botComment) {
console.log(`Found previous comment: ${botComment.id}`);
return core.setOutput('comment_id', botComment.id);
} else {
console.log('No previous comment found');
return core.setOutput('comment_id', '');
}
- name: Fetch PR diff
if: steps.check_label.outputs.has_label == 'true'
id: fetch_diff
uses: actions/github-script@v6
with:
github-token: ${{ github.token }}
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
// Fetch the PR diff using the GitHub API with raw header
const diff = await github.request({
url: pr.url,
headers: {
accept: 'application/vnd.github.v3.diff'
}
});
// Save diff to a file
const fs = require('fs');
fs.writeFileSync('pr_diff.txt', diff.data);
console.log('PR diff saved to pr_diff.txt');
- name: Run Claude Code review
if: steps.check_label.outputs.has_label == 'true'
id: claude_review
env:
ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_API_KEY }}
run: |
cat > review_prompt.txt << 'EOT'
You are doing a code review focused files changed in this PR. You should focus on the changes from this PR, but you can look at surrounding code as needed.
Please analyze the code changes and highlight ONLY serious issues with a clear and objectively better resolution.
FORMAT YOUR RESPONSE AS FOLLOWS:
- If you find no issues or only minor issues, respond EXACTLY with "no issues found"
- If you find serious issues, provide specific feedback for each issue:
- File and line number
- Description of the issue
- A clear and specific recommendation for how to fix it
- Example code for the solution if possible
DO NOT comment on:
- Minor optimizations
- Speculative issues without clear evidence
Here is the PR diff for you to review:
```diff
$(cat pr_diff.txt)
```
EOT
# Run Claude Code with JSON output format and capture output
CLAUDE_OUTPUT=$(claude -p "$(cat review_prompt.txt)" --output-format json)
# Echo the full JSON response for debugging
echo "Full Claude JSON response:"
echo "$CLAUDE_OUTPUT" | jq .
# Extract the response text and cost from the JSON output
RESULTS=$(echo "$CLAUDE_OUTPUT" | jq -r '.result')
COST=$(echo "$CLAUDE_OUTPUT" | jq -r '.cost_usd' | awk '{printf "%.3f", $0}')
echo "RESULTS<<EOF" >> $GITHUB_ENV
echo "$RESULTS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "COST=$COST" >> $GITHUB_ENV
# Check if there are issues found
if [[ "$RESULTS" == *"no issues found"* ]]; then
echo "ISSUES_FOUND=false" >> $GITHUB_ENV
else
echo "ISSUES_FOUND=true" >> $GITHUB_ENV
fi
- name: Resolve previous comment if exists
if: steps.check_label.outputs.has_label == 'true' && steps.find_comment.outputs.comment_id != ''
uses: actions/github-script@v6
with:
github-token: ${{ github.token }}
script: |
// First get the PR details to find all review threads
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
// Find the thread containing our comment using GraphQL
const commentId = ${{ steps.find_comment.outputs.comment_id }};
console.log(`Looking for threads containing comment ${commentId}`);
// Get review threads for this PR using GraphQL
const graphqlQuery = `
query GetPullRequestThreads($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
reviewThreads(first: 100) {
nodes {
id
isResolved
comments(first: 10) {
nodes {
id
databaseId
}
}
}
}
}
}
}
`;
const variables = {
owner: context.repo.owner,
repo: context.repo.repo,
prNumber: context.issue.number
};
try {
// Use GraphQL to find threads
const result = await github.graphql(graphqlQuery, variables);
const threads = result.repository.pullRequest.reviewThreads.nodes;
// Look for the thread containing our comment
for (const thread of threads) {
const hasComment = thread.comments.nodes.some(
comment => comment.databaseId === commentId
);
if (hasComment && !thread.isResolved) {
console.log(`Found thread containing comment ${commentId}, resolving it`);
// Resolve the thread with GraphQL mutation
const resolveResult = await github.graphql(`
mutation ResolveThread($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread {
id
isResolved
}
}
}
`, {
threadId: thread.id
});
console.log(`Thread resolved: ${resolveResult.resolveReviewThread.thread.isResolved}`);
return;
}
}
// If we can't find the thread or resolve it, fall back to deleting the comment
console.log("Couldn't find thread for comment or comment not in review thread, falling back to deletion");
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId
});
console.log('Deleted previous comment');
} catch (error) {
console.error('Error resolving comment thread:', error);
console.log('Falling back to comment deletion...');
try {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId
});
console.log('Deleted previous comment');
} catch (deleteError) {
if (deleteError.status === 404) {
console.log('Comment was already deleted or does not exist, continuing...');
} else {
console.error('Error deleting comment:', deleteError);
throw deleteError;
}
}
}
- name: Post review comment
if: steps.check_label.outputs.has_label == 'true' && env.ISSUES_FOUND == 'true'
uses: actions/github-script@v6
with:
github-token: ${{ github.token }}
script: |
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## Claude Code Review
${process.env.RESULTS}
_This review was generated automatically using Claude Code. API call cost: $${process.env.COST} USD_`
});
console.log('Posted review comment with cost information');
- name: Handle review result
if: steps.check_label.outputs.has_label == 'true' && env.ISSUES_FOUND == 'true'
run: |
echo "❌ Claude found some issues - see PR comment for details"
exit 1

Rails

This is a Ruby on Rails application. It should follow common Rails conventions.

See @.cursor/rules/accepted-patterns.mdc for coding style.

See @.cursor/rules/active-record-performance.mdc for rules around writing performant Ruby.

See @.cursor/rules/security.mdc for rules around writing secure Ruby.

Views

Views live in subfolders of app/views/

Hotwire and Turbo are used throughout the app.

Views should use the Design System for components, and Tailwind CSS for styling. See @.cursor/rules/design-system.mdc for design system documentation.

Avoid writing new React code.

Javascript

Stimulus JS should be used for any view that is written in HAML.

Most Stimulus controllers live in /app/assets/webpack/stimulus/shared_controllers. Put new controllers in here unless prompted otherwise.

If they are only for the mobile app they'll be in /app/assets/webpack/mobile_app/stimulus_controllers and if only for the desktop app they'll be in /app/assets/webpack/desktop/stimulus_controllers. The prompt will tell you if they should be in one of these places.

It is a error if a stimulus controller that's in the mobile folder is referenced from views not in app/views/mobile_app. And it's an error if a stimulus controller that's in the desktop folder is referenced from app/views/mobile_app.

Tests

See @.cursor/rules/testing.mdc

Database migrations

See @.cursor/rules/database-migrations.mdc

Time Zone Handling

See @.cursor/rules/time-zones.mdc

Multi-tenancy Issues

See @.cursor/rules/multi-tenancy.mdc

---
description: Testing practices in Payaus
globs: test/**/*.rb
alwaysApply: true
---
# Testing
## General Testing Guidelines
* Use `minitest` for ruby tests - do not use `spec` syntax.
* Model tests extend `ActiveSupport::TestCase`
(etc)
## Testing Best Practices
* Every test that makes a request should at least include an assertion about the response status (ok, redirect, unauthorized, etc).
* Avoid using relative time/date objects in tests (e.g. Date.yesterday, Time.zone.now). Always prefer instantiating the date or time manually: e.g. Date.new(2025, 1, 1), Time.zone.local(2025, 1, 1, 9)
(etc)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment