Skip to content

Ws transport

Ws transport #362

name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
claude-review:
# Skip review for automated "Version Packages" PRs created by changesets
if: github.event.pull_request.title != 'Version Packages'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Fetch review threads
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pre-fetch review threads so Claude doesn't have to execute GraphQL
gh api graphql -f query='
query($owner: String!, $name: String!, $pr: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
id
isResolved
path
line
comments(first: 10) {
nodes {
id
databaseId
body
author { login }
}
}
}
}
}
}
}' \
-f owner="${{ github.repository_owner }}" \
-f name="${{ github.event.repository.name }}" \
-F pr=${{ github.event.pull_request.number }} \
| jq '[.data.repository.pullRequest.reviewThreads.nodes[] |
select(.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "github-actions[bot]") |
{
threadId: .id,
isResolved: .isResolved,
path: .path,
line: .line,
firstCommentId: .comments.nodes[0].databaseId,
firstCommentBody: .comments.nodes[0].body,
allComments: [.comments.nodes[] | {author: .author.login, body: .body}]
}]' > /tmp/review-threads.json
echo "Found $(jq length /tmp/review-threads.json) Claude review threads"
cat /tmp/review-threads.json
- name: Fetch previous review context
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pre-fetch previous Claude comments
gh pr view ${{ github.event.pull_request.number }} --json comments \
--jq '[.comments[] | select(.author.login == "claude" or .author.login == "github-actions[bot]") | {author: .author.login, body: .body, createdAt: .createdAt}]' \
> /tmp/previous-comments.json
# Get commits since last push (for incremental reviews)
BEFORE="${{ github.event.before }}"
AFTER="${{ github.event.after }}"
# Check if this is the first push (before will be all zeros)
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
echo '{"isFirstPush": true, "commits": [], "historyRewritten": false}' > /tmp/review-context.json
else
# Check if history was rewritten (before commit no longer exists)
if git cat-file -t "$BEFORE" 2>/dev/null; then
HISTORY_REWRITTEN="false"
COMMITS=$(git log --oneline "$BEFORE".."$AFTER" 2>/dev/null | head -20 | jq -R -s 'split("\n") | map(select(length > 0))')
else
HISTORY_REWRITTEN="true"
COMMITS="[]"
fi
jq -n \
--argjson historyRewritten "$HISTORY_REWRITTEN" \
--argjson commits "$COMMITS" \
'{isFirstPush: false, commits: $commits, historyRewritten: $historyRewritten}' \
> /tmp/review-context.json
fi
echo "Previous comments: $(jq length /tmp/previous-comments.json)"
echo "Review context:"
cat /tmp/review-context.json
- name: Fetch PR metadata
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pre-fetch PR metadata so Claude doesn't need to query it
gh pr view ${{ github.event.pull_request.number }} \
--json title,body,baseRefName,headRefName,author \
--jq '{
title: .title,
body: .body,
baseBranch: .baseRefName,
headBranch: .headRefName,
author: .author.login
}' > /tmp/pr-metadata.json
echo "PR metadata:"
cat /tmp/pr-metadata.json
- name: Fetch PR diff
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pre-fetch the full PR diff
gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-diff.patch
echo "PR diff: $(wc -l < /tmp/pr-diff.patch) lines"
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
plugin_marketplaces: |
https://github.com/obra/superpowers-marketplace.git
plugins: |
superpowers@superpowers-marketplace
prompt: |
You are Claude reviewing PRs for the Cloudflare Sandbox SDK.
Repository: ${{ github.repository }}
PR Number: ${{ github.event.pull_request.number }}
**Read CLAUDE.md first** - it contains architecture, patterns, and coding standards.
**BE CONCISE**: Only report actual issues worth addressing. Skip generic praise and obvious observations. If PR looks good, say so briefly.
### 1. Build Context (Before Looking at Diff)
**Understand the domain first**:
- Use `mcp__cloudflare-docs__search_cloudflare_documentation` to understand relevant Cloudflare concepts related to this change
- Use Task tool with Explore subagent to understand how this area of the codebase works
- Ask: "What files/modules interact with the changed code? How is this pattern used elsewhere?"
**Then review the diff with that context.**
### 2. Check for Previous Review (Internal Context Only)
Your previous feedback has been pre-fetched in two places:
**Top-level review comments** (your main "## Claude Code Review" posts):
```bash
cat /tmp/previous-comments.json
```
**Inline code comments** (your comments on specific lines - also in step 6):
```bash
cat /tmp/review-threads.json
```
**What changed since your last review:**
```bash
cat /tmp/review-context.json
```
Read all three to understand what you've already said and what's changed.
**Don't announce "Update N"** - just write naturally, like continuing a conversation.
### 3. Gather Context
PR metadata and diff have been pre-fetched. Read these files:
```bash
cat /tmp/pr-metadata.json # title, body, baseBranch, headBranch, author
cat /tmp/pr-diff.patch # Full PR diff
```
Also:
- Read CLAUDE.md for repo-specific conventions and architecture patterns
- Use Read tool to examine key changed files
### 4. Check Documentation Impact
Use `mcp__cloudflare-docs__search_cloudflare_documentation` to check if this PR requires doc updates:
- Does it change existing documented behavior?
- Does it add new features needing documentation?
- Does it have security implications needing best practices docs?
- If yes, call out specific doc sections that need updates
### 5. Internal Analysis
Before writing your review, analyze inside <analysis> tags (do NOT include in final comment):
a. What is this PR accomplishing?
b. Which packages/layers are affected?
c. Are there architectural or security implications?
d. What tests are needed?
e. Does this need documentation updates?
### 6. Decide What to Do With Your Previous Inline Comments
You already read `/tmp/review-threads.json` in step 2. Now decide for each thread: was it fixed, or does it need follow-up?
This decision informs your new review - mention progress naturally:
- "The encoding issue is fixed, thanks!"
- "Still seeing the over-mocking - I'll follow up on that thread."
Don't duplicate thread content in your new review. Reference it instead.
### 7. Execute Review
Launch superpowers:code-reviewer subagent (Task tool) with the context gathered above:
- Full PR diff from step 3
- Relevant architecture patterns from CLAUDE.md
- Documentation requirements from step 4
- Previous review feedback (if incremental update from step 2)
- Existing review threads from step 6
The code-reviewer will analyze correctness, architecture, testing, and code quality.
### 8. Post Review
**Writing style**:
- Be direct. One clear point per issue.
- Skip praise, acknowledgments, and preambles.
- If PR is good: "Looks good" and stop.
- If issues found: State the issue, reference location, explain why it matters. Move on.
**Format**:
- **Inline comments**: Use for specific line-by-line code issues (file path, line number, comment)
- **Main comment**: Summary only - overall assessment, architectural concerns, testing strategy, verdict. Do NOT duplicate inline comments here.
**Main comment - write like a human reviewer:**
- Start with "## Claude Code Review"
- Acknowledge progress on previous feedback: "The encoding issue is fixed, nice!"
- Reference ongoing threads: "Still discussing the mocking approach in that thread."
- Raise new concerns: "New issue: error handling in container.ts doesn't cover..."
- Give a clear verdict: "Looks good to merge" or "A few things to address first"
**Step 1: Submit your new review**
Create review.json with your main comment and any NEW inline comments (don't re-comment on existing threads):
```json
{
"body": "## Claude Code Review\n\nYour overall assessment...",
"event": "COMMENT",
"commit_id": "${{ github.event.pull_request.head.sha }}",
"comments": [
{"path": "path/to/file.ts", "line": 42, "body": "New issue here"}
]
}
```
Post it:
```bash
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews \
--method POST \
--input review.json
```
**Step 2: Handle your existing inline threads**
For each thread from step 6, decide: is the issue fixed or does it need follow-up?
**If fixed** → resolve it (write thread ID to file):
```bash
echo "PRRT_xxx" >> /tmp/threads-to-resolve.txt
```
**If needs follow-up** → reply to continue the conversation (write to JSON):
```bash
echo '[{"commentId": 123456789, "body": "Still seeing this issue because..."}]' > /tmp/thread-replies.json
```
You can also reply AND resolve (e.g., "Fixed, thanks!" then resolve).
Subsequent workflow steps will submit these automatically.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
claude_args: '--allowedTools "Task,Skill,Read,Glob,Grep,Write,TodoWrite,mcp__cloudflare-docs__search_cloudflare_documentation,mcp__exa__get_code_context_exa,mcp__exa__web_search_exa,Bash(gh api:*),Bash(gh pr checks:*),Bash(gh issue view:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*),Bash(jq:*),Bash(echo:*),Bash(cat:*),Bash(mv:*)"'
- name: Resolve review threads
env:
GH_TOKEN: ${{ github.token }}
run: |
# Resolve threads that Claude marked for resolution
if [ -f /tmp/threads-to-resolve.txt ]; then
while read -r thread_id; do
if [ -n "$thread_id" ]; then
echo "Resolving thread: $thread_id"
gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread { id isResolved }
}
}' -f threadId="$thread_id" || echo "Failed to resolve $thread_id (may already be resolved)"
fi
done < /tmp/threads-to-resolve.txt
echo "Done resolving $(wc -l < /tmp/threads-to-resolve.txt | tr -d ' ') threads"
else
echo "No threads to resolve"
fi
- name: Submit thread replies
env:
GH_TOKEN: ${{ github.token }}
run: |
# Submit replies that Claude wrote to thread-replies.json
if [ -f /tmp/thread-replies.json ]; then
REPLY_COUNT=$(jq length /tmp/thread-replies.json)
if [ "$REPLY_COUNT" -gt 0 ]; then
echo "Submitting $REPLY_COUNT thread replies"
jq -c '.[]' /tmp/thread-replies.json | while read -r reply; do
COMMENT_ID=$(echo "$reply" | jq -r '.commentId')
BODY=$(echo "$reply" | jq -r '.body')
echo "Replying to comment $COMMENT_ID"
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments/"$COMMENT_ID"/replies \
--method POST \
-f body="$BODY" || echo "Failed to reply to $COMMENT_ID"
done
echo "Done submitting replies"
else
echo "No replies to submit"
fi
else
echo "No thread replies file found"
fi
- name: Minimize outdated reviews
env:
GH_TOKEN: ${{ github.token }}
run: |
# Minimize reviews that are outdated:
# 1. Reviews where ALL inline threads are resolved
# 2. Older reviews with NO threads (keep only the latest threadless review)
QUERY='query($owner: String!, $name: String!, $pr: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pr) {
reviews(first: 100) {
nodes {
id
isMinimized
createdAt
author { login }
}
}
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 1) {
nodes {
pullRequestReview { id }
}
}
}
}
}
}
}'
# Fetch data
DATA=$(gh api graphql \
-F owner="${{ github.repository_owner }}" \
-F name="${{ github.event.repository.name }}" \
-F pr=${{ github.event.pull_request.number }} \
-f query="$QUERY")
# Write jq filter to file to avoid escaping issues
cat > /tmp/minimize-filter.jq << 'JQEOF'
.data.repository.pullRequest as $pr |
# Only consider reviews from Claude bot
($pr.reviews.nodes | map(select(.author.login == "claude"))) as $claudeReviews |
# Build set of review IDs that have threads
($pr.reviewThreads.nodes | map(
select(.comments.nodes[0].pullRequestReview != null) |
.comments.nodes[0].pullRequestReview.id
) | unique) as $reviewsWithThreads |
# Build map of review_id -> { allResolved: bool }
($pr.reviewThreads.nodes | map(
select(.comments.nodes[0].pullRequestReview != null) |
{
reviewId: .comments.nodes[0].pullRequestReview.id,
isResolved: .isResolved
}
) | group_by(.reviewId) | map({
key: .[0].reviewId,
value: { allResolved: (map(.isResolved) | all) }
}) | from_entries) as $threadStatusByReview |
# Find the latest Claude review (by createdAt) that has no threads
(($claudeReviews | map(select(. as $r | $reviewsWithThreads | any(. == $r.id) | not)) | sort_by(.createdAt) | last) // null) as $latestThreadless |
($latestThreadless | if . then .id else null end) as $latestThreadlessId |
# Select Claude reviews to minimize
$claudeReviews[] |
select(.isMinimized == false) |
. as $review |
select(
# Case 1: Has threads and all are resolved
($threadStatusByReview[$review.id] != null and $threadStatusByReview[$review.id].allResolved == true) or
# Case 2: No threads and not the latest threadless review
(($reviewsWithThreads | any(. == $review.id) | not) and $review.id != $latestThreadlessId)
) |
.id
JQEOF
# Minimize each review
echo "$DATA" | jq -r -f /tmp/minimize-filter.jq | while read -r id; do
if [ -n "$id" ]; then
echo "Minimizing review: $id"
gh api graphql -f query='
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
minimizedComment { isMinimized }
}
}' -f id="$id" || true
fi
done
echo "Done minimizing outdated reviews"