Add a code review step which uses a short-lived provider token (#7932)

This commit is contained in:
Jack Amadeo
2026-03-24 08:57:40 -04:00
committed by GitHub
parent ce160b183d
commit 928f4ac46a
11 changed files with 1165 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
version: "1.0.0"
title: GitHub PR Code Review
description: Perform a code review of a GitHub pull request.
parameters:
- key: pr_directory
input_type: string
requirement: required
description: Path to the directory with pr.md and pr.diff
- key: instructions
input_type: string
requirement: required
description: Specific instructions for the code review.
extensions:
- type: builtin
name: developer
- type: stdio
name: code_review
cmd: uv
args:
- run
- '{{ recipe_dir }}/../scripts/pr-review-mcp.py'
prompt: |
Review the code changes downloaded from a GitHub pull request.
The PR metadata is located at {{ pr_directory }}/pr.md.
The proposed diff you are to review is located at {{ pr_directory }}/pr.diff.
The base branch is checked out in the working directory.
Use the tools you have to review the diff and examine the code changes and context.
Use the code review tools to add feedback on specific parts of the diff.
There is no need to call the finish_review tool unless absolutely necessary to add
summary content not covered by inline comments.
Be concise with your comments. Just a few sentences per comment at most, with no
extra formatting. Just the gist of the problem is enough.
** Important **
Don't add nit-pick comments and avoid matters of opinion.
Adhere closely to the code review instructions below.
Don't add add feedback outside the scope of the instructions below.
# Code review instructions
{{ instructions }}
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["mcp"]
# ///
"""MCP server for collecting PR review comments and conclusion."""
import json
import os
from pathlib import Path
from mcp.server.fastmcp import FastMCP
server = FastMCP("pr-review")
output_dir = Path(os.environ.get("REVIEW_OUTPUT_DIR", "/tmp"))
def _append_comment(comment: dict) -> int:
"""Append a comment to the comments file and return the new total."""
comments_file = output_dir / "comments.json"
comments = json.loads(comments_file.read_text()) if comments_file.exists() else []
comments.append(comment)
comments_file.write_text(json.dumps(comments, indent=2))
return len(comments)
@server.tool()
def add_comment(
path: str,
line: int,
body: str,
suggestion: str | None = None,
side: str = "RIGHT",
start_line: int | None = None,
) -> str:
"""Add a review comment on a specific line in the PR diff.
Args:
path: The relative file path in the repository (e.g. "src/main.rs").
line: The line number in the file that the comment applies to.
For added or modified lines, use the line number in the new version of the file (side=RIGHT).
For deleted lines, use the line number in the old version of the file (side=LEFT).
body: The review comment text (Markdown supported).
suggestion: Optional replacement code for the line(s). When provided, GitHub renders an
"Apply suggestion" button the author can click. The suggestion replaces the
entire line (or range if start_line is set).
side: Which version of the file the line number refers to.
"RIGHT" for the new/modified version (default), "LEFT" for the old/deleted version.
start_line: For multi-line comments, the first line of the range. When set, `line` is the last line.
"""
if suggestion is not None:
body = (
f"{body}\n\n```suggestion\n{suggestion}\n```"
if body
else f"```suggestion\n{suggestion}\n```"
)
comment = {"path": path, "line": line, "side": side, "body": body}
if start_line is not None:
comment["start_line"] = start_line
comment["start_side"] = side
total = _append_comment(comment)
return f"Comment added on {path}:{line} ({total} total)."
@server.tool()
def finish_review(body: str = "") -> str:
"""Finish the review.
Args:
body: Optional top-level review body (Markdown supported). Only include if it
contains information not already covered by inline comments. Most reviews
should leave this empty.
"""
conclusion = {"body": body, "event": "COMMENT"}
conclusion_file = output_dir / "conclusion.json"
conclusion_file.write_text(json.dumps(conclusion, indent=2))
return "Review finished."
if __name__ == "__main__":
server.run(transport="stdio")
+210
View File
@@ -0,0 +1,210 @@
name: Code Review
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: number
oidc_proxy_url:
description: 'OIDC proxy URL (overrides repo variable)'
required: false
type: string
review_instructions:
description: 'Instructions for the code review'
required: false
default: 'Review the changes for correctness'
type: string
# pull_request_target:
# types: [opened, synchronize, reopened]
concurrency:
group: code-review-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: read
steps:
- name: Mint OIDC token
id: oidc
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
TOKEN_AUDIENCE: goose-oidc-proxy
with:
script: |
const token = await core.getIDToken(process.env.TOKEN_AUDIENCE);
core.setOutput('token', token);
core.setSecret(token);
- name: Gather PR metadata and diff
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number || github.event.inputs.pr_number }}
mkdir -p /tmp/code-review
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json title --jq '.title' > /tmp/code-review/title.txt
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json body --jq '.body // "(no description)"' > /tmp/code-review/body.txt
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json headRefOid --jq '.headRefOid' > /tmp/code-review/commit_id.txt
gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" > /tmp/code-review/pr.diff
TITLE=$(cat /tmp/code-review/title.txt)
BODY=$(cat /tmp/code-review/body.txt)
printf '# %s\n\n## Description\n\n%s\n' "$TITLE" "$BODY" > /tmp/code-review/pr.md
- name: Write OIDC token to file
run: echo "$OIDC_TOKEN" > /tmp/code-review/oidc-token.txt
env:
OIDC_TOKEN: ${{ steps.oidc.outputs.token }}
- name: Upload review inputs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: code-review-inputs
path: /tmp/code-review/
retention-days: 1
review:
needs: prepare
runs-on: ubuntu-latest
permissions: {}
timeout-minutes: 15
container:
image: ghcr.io/block/goose:latest
options: --user root
env:
HOME: /tmp/goose-home
steps:
- name: Checkout base branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install tools
run: |
apt-get update
apt-get install -y ripgrep jq
- name: Download review inputs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: code-review-inputs
path: /tmp/code-review
- name: Install custom provider and run goose review
env:
GOOSE_PROVIDER: anthropic-oidc-proxy
GOOSE_MODEL: ${{ vars.GOOSE_CODE_REVIEW_MODEL || 'claude-opus-4-6' }}
OIDC_PROXY_URL: ${{ github.event.inputs.oidc_proxy_url || vars.OIDC_PROXY_URL }}
REVIEW_INSTRUCTIONS: ${{ github.event.inputs.review_instructions || 'Review the changes for correctness' }}
run: |
ANTHROPIC_OIDC_PROXY_API_KEY=$(cat /tmp/code-review/oidc-token.txt)
export ANTHROPIC_OIDC_PROXY_API_KEY
rm /tmp/code-review/oidc-token.txt
mkdir -p "$HOME/.local/share/goose/sessions"
mkdir -p "$HOME/.config/goose/custom_providers"
cp oidc-proxy/anthropic-oidc-proxy.json "$HOME/.config/goose/custom_providers/"
goose run \
--recipe .github/recipes/code-review.yaml \
--params pr_directory=/tmp/code-review \
--params instructions="$REVIEW_INSTRUCTIONS"
- name: Upload review output
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: code-review-output
path: |
/tmp/conclusion.json
/tmp/comments.json
if-no-files-found: ignore
retention-days: 30
submit:
needs: [prepare, review]
runs-on: ubuntu-latest
permissions:
pull-requests: write
if: always() && needs.review.result == 'success'
steps:
- name: Download review inputs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: code-review-inputs
path: /tmp/inputs
- name: Download review output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: code-review-output
path: /tmp/output
- name: Submit review
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
run: |
CONCLUSION="/tmp/output/conclusion.json"
COMMENTS="/tmp/output/comments.json"
COMMIT_ID=$(cat /tmp/inputs/commit_id.txt)
if [ ! -f "$COMMENTS" ]; then
echo "No comments produced, skipping review submission."
exit 0
fi
if [ ! -f "$CONCLUSION" ]; then
echo '{"body":"","event":"COMMENT"}' > "$CONCLUSION"
fi
jq -n \
--arg commit_id "$COMMIT_ID" \
--slurpfile conclusion "$CONCLUSION" \
--slurpfile comments "$COMMENTS" \
'{
commit_id: $commit_id,
event: $conclusion[0].event,
comments: $comments[0]
}
| if $conclusion[0].body != "" then .body = $conclusion[0].body else . end
' > /tmp/review-payload.json
if gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \
--input /tmp/review-payload.json; then
echo "Review submitted with inline comments."
exit 0
fi
echo "Inline comments failed, folding into review body."
jq -n \
--arg commit_id "$COMMIT_ID" \
--slurpfile conclusion "$CONCLUSION" \
--slurpfile comments "$COMMENTS" \
'{
commit_id: $commit_id,
body: (
$conclusion[0].body + "\n\n---\n\n## Inline Comments\n\n" +
([$comments[0][] |
"### `" + .path + "` (line " + (.line | tostring) + ")\n\n" + .body
] | join("\n\n---\n\n"))
),
event: $conclusion[0].event
}' \
| gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \
--input -