Files
openchamber/.github/workflows/pr-review.yml
T

197 lines
8.2 KiB
YAML

name: pr-review
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
concurrency:
# PR conversation comments arrive as `issue_comment` events, so their PR number
# is exposed as `github.event.issue.number`. Keep comment-triggered runs in a
# separate group so skipped non-command comments do not cancel active reviews.
group: pr-review-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
jobs:
review:
if: |
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && (startsWith(github.event.comment.body, '/oc-review') || contains(github.event.comment.body, ' /oc-review'))) ||
(github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/oc-review') || contains(github.event.comment.body, ' /oc-review')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Generate review app token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.OC_REVIEW_APP_ID }}
private-key: ${{ secrets.OC_REVIEW_APP_PRIVATE_KEY }}
- name: Resolve pull request context
id: pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
run: |
pr_json="$(gh pr view "$EVENT_PR_NUMBER" --json number,url,title,body,author,baseRefName,headRefName,headRepositoryOwner,isDraft)"
if [ "$(printf '%s' "$pr_json" | jq -r '.isDraft')" = "true" ]; then
echo "draft=true" >> "$GITHUB_OUTPUT"
exit 0
fi
{
echo "draft=false"
echo "number=$(printf '%s' "$pr_json" | jq -r '.number')"
echo "url=$(printf '%s' "$pr_json" | jq -r '.url')"
echo "title=$(printf '%s' "$pr_json" | jq -r '.title')"
echo "author=$(printf '%s' "$pr_json" | jq -r '.author.login')"
echo "base_ref=$(printf '%s' "$pr_json" | jq -r '.baseRefName')"
echo "head_ref=$(printf '%s' "$pr_json" | jq -r '.headRefName')"
echo "head_repo_owner=$(printf '%s' "$pr_json" | jq -r '.headRepositoryOwner.login')"
echo "body<<EOF"
printf '%s\n' "$pr_json" | jq -r '.body // ""'
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Check review safety
if: steps.pr.outputs.draft == 'false'
id: safety
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
run: |
changed_sensitive_files="$(gh pr diff "$PR_NUMBER" --name-only | grep -E '^(\.github/workflows/pr-review\.yml|\.opencode/agent/pr-review\.md)$' || true)"
if [ -n "$changed_sensitive_files" ]; then
{
echo "safe=false"
echo "changed_sensitive_files<<EOF"
echo "$changed_sensitive_files"
echo "EOF"
} >> "$GITHUB_OUTPUT"
exit 0
fi
echo "safe=true" >> "$GITHUB_OUTPUT"
- name: Acknowledge manual review command
if: steps.pr.outputs.draft == 'false' && steps.safety.outputs.safe == 'true' && github.event_name != 'pull_request_target'
id: manual-reaction
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
EVENT_NAME: ${{ github.event_name }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
if [ "$EVENT_NAME" = "pull_request_review_comment" ]; then
endpoint="repos/${GITHUB_REPOSITORY}/pulls/comments/${COMMENT_ID}/reactions"
else
endpoint="repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}/reactions"
fi
reaction_id="$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$endpoint" \
-f content='eyes' \
--jq '.id')"
echo "endpoint=$endpoint" >> "$GITHUB_OUTPUT"
echo "reaction_id=$reaction_id" >> "$GITHUB_OUTPUT"
- name: Skip unsafe review
if: steps.pr.outputs.draft == 'false' && steps.safety.outputs.safe != 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
CHANGED_SENSITIVE_FILES: ${{ steps.safety.outputs.changed_sensitive_files }}
run: |
gh pr comment "$PR_NUMBER" --body "<h3>Code Review Skipped</h3>
Automated review was skipped because this PR changes review automation files:
\`\`\`
$CHANGED_SENSITIVE_FILES
\`\`\`
A maintainer should review those changes manually before running automated review."
- name: Install opencode
if: steps.pr.outputs.draft == 'false' && steps.safety.outputs.safe == 'true'
run: curl -fsSL https://opencode.ai/install | bash
- name: Review pull request
if: steps.pr.outputs.draft == 'false' && steps.safety.outputs.safe == 'true'
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
PR_URL: ${{ steps.pr.outputs.url }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
PR_TITLE: ${{ steps.pr.outputs.title }}
PR_BODY: ${{ steps.pr.outputs.body }}
PR_AUTHOR: ${{ steps.pr.outputs.author }}
PR_BASE_REF: ${{ steps.pr.outputs.base_ref }}
PR_HEAD_REF: ${{ steps.pr.outputs.head_ref }}
PR_HEAD_REPO_OWNER: ${{ steps.pr.outputs.head_repo_owner }}
run: |
opencode run --agent pr-review "A pull request in the OpenChamber repository needs code review.
This may be a repeated review request. Before writing a new review, inspect prior PR comments, bot comments, reviews, inline comments, and the commit timeline via GitHub. Compare prior findings against commits pushed after those comments, then only repeat findings that still exist in the current diff/current file state.
PR: $PR_URL
Number: $PR_NUMBER
Author: $PR_AUTHOR
Base: $PR_BASE_REF
Head: $PR_HEAD_REPO_OWNER:$PR_HEAD_REF
Title: $PR_TITLE
$PR_BODY"
- name: Verify manual review comment
if: steps.pr.outputs.draft == 'false' && steps.safety.outputs.safe == 'true' && github.event_name != 'pull_request_target'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
COMMAND_CREATED_AT: ${{ github.event.comment.created_at }}
REACTION_ENDPOINT: ${{ steps.manual-reaction.outputs.endpoint }}
EYES_REACTION_ID: ${{ steps.manual-reaction.outputs.reaction_id }}
run: |
review_comment_count="$(gh api \
"repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
--paginate \
--slurp \
--jq "map(.[]) | [.[] | select(.created_at > \"${COMMAND_CREATED_AT}\" and .user.login == \"openchamber-bot[bot]\" and (.body | contains(\"<h3>Code Review Summary</h3>\")))] | length")"
if [ "$review_comment_count" -lt 1 ]; then
echo "Manual /oc-review completed without creating a new OpenChamber Bot PR comment." >&2
exit 1
fi
if [ -n "$EYES_REACTION_ID" ]; then
gh api \
--method DELETE \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${REACTION_ENDPOINT}/${EYES_REACTION_ID}"
fi
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$REACTION_ENDPOINT" \
-f content='+1' >/dev/null