-
Notifications
You must be signed in to change notification settings - Fork 3.8k
[chore](ci) refactor code review workflow to run on PR events with comment-triggered dispatch #62302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[chore](ci) refactor code review workflow to run on PR events with comment-triggered dispatch #62302
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||||||||||
| name: Code Review Comment Dispatch | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||
| issue_comment: | ||||||||||||||||||||||||||||||||||||||||||
| types: [created] | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||||||||||
| actions: write | ||||||||||||||||||||||||||||||||||||||||||
| pull-requests: write | ||||||||||||||||||||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||||||||||||||||||||
| issues: write | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||||
| resolve-pr: | ||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||
| if: >- | ||||||||||||||||||||||||||||||||||||||||||
| github.event.issue.pull_request && | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This dispatcher is callable by any user who can comment on a public PR. Because
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||
| contains(github.event.comment.body, '/review') | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+18
|
||||||||||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+19
|
||||||||||||||||||||||||||||||||||||||||||
| pr_number: ${{ steps.pr.outputs.pr_number }} | ||||||||||||||||||||||||||||||||||||||||||
| head_sha: ${{ steps.pr.outputs.head_sha }} | ||||||||||||||||||||||||||||||||||||||||||
| base_sha: ${{ steps.pr.outputs.base_sha }} | ||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||
| - name: Get PR info | ||||||||||||||||||||||||||||||||||||||||||
| id: pr | ||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||
| PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}) | ||||||||||||||||||||||||||||||||||||||||||
| HEAD_SHA=$(echo "$PR_JSON" | jq -r '.head.sha') | ||||||||||||||||||||||||||||||||||||||||||
| BASE_SHA=$(echo "$PR_JSON" | jq -r '.base.sha') | ||||||||||||||||||||||||||||||||||||||||||
| echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||
| echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||
| echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| code-review: | ||||||||||||||||||||||||||||||||||||||||||
| needs: resolve-pr | ||||||||||||||||||||||||||||||||||||||||||
| if: >- | ||||||||||||||||||||||||||||||||||||||||||
| github.event.issue.pull_request && | ||||||||||||||||||||||||||||||||||||||||||
| contains(github.event.comment.body, '/review') | ||||||||||||||||||||||||||||||||||||||||||
| uses: ./.github/workflows/opencode-review-runner.yml | ||||||||||||||||||||||||||||||||||||||||||
| secrets: inherit | ||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||
| pr_number: ${{ needs.resolve-pr.outputs.pr_number }} | ||||||||||||||||||||||||||||||||||||||||||
| head_sha: ${{ needs.resolve-pr.outputs.head_sha }} | ||||||||||||||||||||||||||||||||||||||||||
| base_sha: ${{ needs.resolve-pr.outputs.base_sha }} | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| refresh-required-check: | ||||||||||||||||||||||||||||||||||||||||||
| needs: | ||||||||||||||||||||||||||||||||||||||||||
| - resolve-pr | ||||||||||||||||||||||||||||||||||||||||||
| - code-review | ||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||
| if: ${{ always() && needs.resolve-pr.result == 'success' && needs.code-review.result != 'skipped' }} | ||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||
| - name: Rerun pull_request Code Review workflow for current head | ||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||
| REPO: ${{ github.repository }} | ||||||||||||||||||||||||||||||||||||||||||
| HEAD_SHA: ${{ needs.resolve-pr.outputs.head_sha }} | ||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||
| RUNS_JSON=$(gh api repos/${REPO}/actions/workflows/opencode-review.yml/runs --paginate -f event=pull_request -f head_sha=${HEAD_SHA}) | ||||||||||||||||||||||||||||||||||||||||||
| RUN_ID=$(printf '%s' "$RUNS_JSON" | jq -r ' | ||||||||||||||||||||||||||||||||||||||||||
| .workflow_runs | ||||||||||||||||||||||||||||||||||||||||||
| | sort_by(.created_at) | ||||||||||||||||||||||||||||||||||||||||||
| | reverse | ||||||||||||||||||||||||||||||||||||||||||
| | map(select(.head_sha != null)) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+66
|
||||||||||||||||||||||||||||||||||||||||||
| run: | | |
| RUNS_JSON=$(gh api repos/${REPO}/actions/workflows/opencode-review.yml/runs --paginate -f event=pull_request -f head_sha=${HEAD_SHA}) | |
| RUN_ID=$(printf '%s' "$RUNS_JSON" | jq -r ' | |
| .workflow_runs | |
| | sort_by(.created_at) | |
| | reverse | |
| | map(select(.head_sha != null)) | |
| PR_NUMBER: ${{ needs.resolve-pr.outputs.pr_number }} | |
| run: | | |
| RUNS_JSON=$(gh api repos/${REPO}/actions/workflows/opencode-review.yml/runs --paginate -f event=pull_request) | |
| RUN_ID=$(printf '%s' "$RUNS_JSON" | jq -r --arg head_sha "$HEAD_SHA" --argjson pr_number "$PR_NUMBER" ' | |
| .workflow_runs | |
| | map( | |
| select( | |
| .head_sha == $head_sha and | |
| any(.pull_requests[]?; .number == $pr_number) | |
| ) | |
| ) | |
| | sort_by(.created_at) | |
| | reverse |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| name: Code Review Runner | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| pr_number: | ||
| required: true | ||
| type: string | ||
| head_sha: | ||
| required: true | ||
| type: string | ||
| base_sha: | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
| contents: read | ||
| issues: write | ||
|
|
||
| jobs: | ||
| code-review: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ inputs.head_sha }} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking out |
||
|
|
||
| - name: Install ripgrep | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get install -y ripgrep | ||
|
|
||
| - name: Install OpenCode | ||
| run: | | ||
| for attempt in 1 2 3; do | ||
| if curl -fsSL https://opencode.ai/install | bash; then | ||
| echo "$HOME/.opencode/bin" >> $GITHUB_PATH | ||
| exit 0 | ||
| fi | ||
| echo "Install attempt $attempt failed, retrying in 10s..." | ||
| sleep 10 | ||
| done | ||
| echo "All install attempts failed" | ||
| exit 1 | ||
|
|
||
| - name: Configure OpenCode auth | ||
| run: | | ||
| mkdir -p ~/.local/share/opencode | ||
| cat > ~/.local/share/opencode/auth.json <<EOF | ||
| { | ||
| "github-copilot": { | ||
| "type": "oauth", | ||
| "refresh": "${CODE_REVIEW_ZCLLL_COPILOT_OPENCODE_KEY}", | ||
| "access": "${CODE_REVIEW_ZCLLL_COPILOT_OPENCODE_KEY}", | ||
| "expires": 0 | ||
| } | ||
| } | ||
| EOF | ||
| env: | ||
| CODE_REVIEW_ZCLLL_COPILOT_OPENCODE_KEY: ${{ secrets.CODE_REVIEW_ZCLLL_COPILOT_OPENCODE_KEY }} | ||
|
|
||
| - name: Configure OpenCode permission | ||
| run: | | ||
| echo '{"permission":"allow"}' > opencode.json | ||
|
|
||
| - name: Prepare review prompt | ||
| run: | | ||
| cat > /tmp/review_prompt.txt <<'PROMPT' | ||
| You are performing an automated code review inside a GitHub Actions runner. The gh CLI is available and authenticated via GH_TOKEN. You can comment on the pull request. | ||
|
|
||
| Context: | ||
| - Repository: PLACEHOLDER_REPO | ||
| - PR number: PLACEHOLDER_PR_NUMBER | ||
| - PR Head SHA: PLACEHOLDER_HEAD_SHA | ||
| - PR Base SHA: PLACEHOLDER_BASE_SHA | ||
|
|
||
| Before reviewing any code, you MUST read and follow these repository instructions from the checked-out workspace: | ||
| 1. Read `.claude/skills/code-review/SKILL.md` in full. | ||
| 2. Read the repository root `AGENTS.md` in full. | ||
| 3. For each changed source area, read any nearest relevant `AGENTS.md` in the corresponding source directory before drawing conclusions. | ||
|
|
||
| During review, you must strictly follow those instructions. In particular: | ||
| - Review the complete invocation chain and runtime behavior, not only the local diff. | ||
| - Give explicit conclusions for each applicable critical checkpoint. | ||
| - Do not leave uncertainty when the code context can resolve it. | ||
| - Use comments only for real issues. If something is safe because of upstream or downstream guarantees, state that clearly instead of speculating. | ||
|
|
||
| ## Submission | ||
| - After completing the review, you MUST provide a final summary opinion based on the rules defined in AGENTS.md and the code-review skill. The summary must include conclusions for each applicable critical checkpoint. | ||
| - You MUST submit exactly one final GitHub review for the current PR head SHA. | ||
| - The final review state MUST be either APPROVE or REQUEST_CHANGES. Do not leave the PR with only comments. | ||
| - If no blocking issues are found, submit: gh pr review PLACEHOLDER_PR_NUMBER --approve --body "<summary>" | ||
| - If blocking issues are found and no inline comments are needed, submit: gh pr review PLACEHOLDER_PR_NUMBER --request-changes --body "<summary>" | ||
| - If blocking issues are found and inline comments are needed, use GitHub Reviews API to submit a single REQUEST_CHANGES review with inline comments: | ||
| - Inline comment bodies may include GitHub suggested changes blocks when you can propose a precise patch. | ||
| - Prefer suggested changes for small, self-contained fixes (for example typos, trivial refactors, or narrowly scoped code corrections). | ||
| - Do not force suggested changes for broad, architectural, or multi-file issues; explain those normally. | ||
| - Build a JSON array of comments like: [{ "path": "<file>", "position": <diff_position>, "body": "..." }] | ||
| - Submit via: gh api repos/PLACEHOLDER_REPO/pulls/PLACEHOLDER_PR_NUMBER/reviews --input <json_file> | ||
| - The JSON file should contain: {"event":"REQUEST_CHANGES","body":"<summary>","comments":[...]} | ||
| - Do not submit a separate summary comment instead of the final review. | ||
| PROMPT | ||
| sed -i "s|PLACEHOLDER_REPO|${REPO}|g" /tmp/review_prompt.txt | ||
| sed -i "s|PLACEHOLDER_PR_NUMBER|${PR_NUMBER}|g" /tmp/review_prompt.txt | ||
| sed -i "s|PLACEHOLDER_HEAD_SHA|${HEAD_SHA}|g" /tmp/review_prompt.txt | ||
| sed -i "s|PLACEHOLDER_BASE_SHA|${BASE_SHA}|g" /tmp/review_prompt.txt | ||
| env: | ||
| REPO: ${{ github.repository }} | ||
| PR_NUMBER: ${{ inputs.pr_number }} | ||
| HEAD_SHA: ${{ inputs.head_sha }} | ||
| BASE_SHA: ${{ inputs.base_sha }} | ||
|
|
||
| - name: Run automated code review | ||
| id: review | ||
| timeout-minutes: 55 | ||
| continue-on-error: true | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| PROMPT=$(cat /tmp/review_prompt.txt) | ||
|
|
||
| set +e | ||
| opencode run "$PROMPT" -m "github-copilot/gpt-5.4" 2>&1 | tee /tmp/opencode-review.log | ||
| status=${PIPESTATUS[0]} | ||
| set -e | ||
|
|
||
| last_log_line=$(awk 'NF { line = $0 } END { print line }' /tmp/opencode-review.log) | ||
|
|
||
| failure_reason="" | ||
| if printf '%s\n' "$last_log_line" | rg -q -i '^Error:|SSE read timed out'; then | ||
| failure_reason="$last_log_line" | ||
| elif [ "$status" -ne 0 ]; then | ||
| failure_reason="OpenCode exited with status $status" | ||
| fi | ||
|
|
||
| if [ -n "$failure_reason" ]; then | ||
| { | ||
| echo "failure_reason<<EOF" | ||
| printf '%s\n' "$failure_reason" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Verify submitted review decision | ||
| if: ${{ steps.review.outcome == 'success' }} | ||
| id: review_state | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPO: ${{ github.repository }} | ||
| PR_NUMBER: ${{ inputs.pr_number }} | ||
| HEAD_SHA: ${{ inputs.head_sha }} | ||
| run: | | ||
| REVIEWS=$(gh api repos/${REPO}/pulls/${PR_NUMBER}/reviews) | ||
| review_state=$(printf '%s' "$REVIEWS" | jq -r --arg head_sha "$HEAD_SHA" ' | ||
| [ .[] | ||
| | select(.user.login == "github-actions[bot]") | ||
| | select(.commit_id == $head_sha) | ||
| | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED") | ||
| ] | ||
| | sort_by(.submitted_at) | ||
| | last | ||
| | .state // "" | ||
| ') | ||
|
|
||
| echo "review_state=$review_state" >> "$GITHUB_OUTPUT" | ||
|
|
||
| if [ "$review_state" = "APPROVED" ]; then | ||
| echo "Automated review approved the PR." | ||
| exit 0 | ||
| fi | ||
|
|
||
| if [ "$review_state" = "CHANGES_REQUESTED" ]; then | ||
| failure_reason="Automated review requested changes." | ||
| else | ||
| failure_reason="No final approve/request-changes review was submitted for head SHA ${HEAD_SHA}." | ||
| fi | ||
|
|
||
| { | ||
| echo "failure_reason<<EOF" | ||
| printf '%s\n' "$failure_reason" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| exit 1 | ||
|
|
||
| - name: Comment PR on review failure | ||
| if: ${{ always() && (steps.review.outcome != 'success' || (steps.review_state.outcome != 'success' && steps.review_state.outputs.review_state == '')) }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REVIEW_FAILURE_REASON: ${{ steps.review.outputs.failure_reason }} | ||
| STATE_FAILURE_REASON: ${{ steps.review_state.outputs.failure_reason }} | ||
| REVIEW_OUTCOME: ${{ steps.review.outcome }} | ||
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | ||
| run: | | ||
| error_msg="${REVIEW_FAILURE_REASON:-${STATE_FAILURE_REASON:-Review step was $REVIEW_OUTCOME (possibly timeout or cancelled)}}" | ||
| gh pr comment "${{ inputs.pr_number }}" --body "$(cat <<EOF | ||
| OpenCode automated review failed and did not complete. | ||
|
|
||
| Error: ${error_msg} | ||
| Workflow run: ${RUN_URL} | ||
|
|
||
| Please inspect the workflow logs and rerun the review after the underlying issue is resolved. | ||
| EOF | ||
| )" | ||
|
|
||
| - name: Fail workflow if review failed | ||
| if: ${{ always() && (steps.review.outcome != 'success' || steps.review_state.outcome != 'success') }} | ||
| env: | ||
| REVIEW_FAILURE_REASON: ${{ steps.review.outputs.failure_reason }} | ||
| STATE_FAILURE_REASON: ${{ steps.review_state.outputs.failure_reason }} | ||
| REVIEW_STATE: ${{ steps.review_state.outputs.review_state }} | ||
| REVIEW_OUTCOME: ${{ steps.review.outcome }} | ||
| run: | | ||
| if [ "$REVIEW_STATE" = "CHANGES_REQUESTED" ]; then | ||
| error_msg="${STATE_FAILURE_REASON:-Automated review requested changes.}" | ||
| else | ||
| error_msg="${REVIEW_FAILURE_REASON:-${STATE_FAILURE_REASON:-Review step was $REVIEW_OUTCOME (possibly timeout or cancelled)}}" | ||
| fi | ||
| echo "OpenCode automated review failed: ${error_msg}" | ||
| exit 1 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added sentence has awkward repeated wording (“actually understand all actually possible…”), which reads like a copy/edit mistake. Please rephrase to remove the repetition so the guideline is clear.