mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 09:57:14 +00:00
f6bfcfe759
claude-bot.yml ran three jobs: the pull-request review, an @claude mention responder, and a conflict resolver that committed and pushed to contributor branches. Only the review is wanted, so the other two are gone and the file is renamed to say what is left. Consequences worth knowing: - secrets.CLAUDE_BOT_PAT is no longer referenced by any workflow. It was the only push credential handed to an agent in this repository and can now be deleted from the repository settings. - @claude goes unanswered everywhere. claude-issue-analyst.yml deliberately excludes mentions (!contains(body, '@claude')) so the two jobs would not both reply; with the mention job gone, only `@claude review` on a pull request still reaches anything. Dropping that clause from the analyst would restore mention answering on issues. - The workflow display name changes, so a branch protection rule keyed on "Claude Bot / review" has to become "Claude PR Review / review". The job name, which is what statusCheckRollup reports, is unchanged. The review job itself is byte-identical. The workflow-level permission drops to issues: read, which is all the remaining job needs - it already declares its own.
205 lines
11 KiB
YAML
205 lines
11 KiB
YAML
name: Claude PR Review
|
|
|
|
on:
|
|
issue_comment:
|
|
types: [created]
|
|
pull_request_target:
|
|
types: [opened, ready_for_review]
|
|
|
|
permissions:
|
|
contents: read
|
|
issues: read
|
|
pull-requests: write
|
|
id-token: write
|
|
|
|
jobs:
|
|
review:
|
|
if: >-
|
|
(github.event_name == 'pull_request_target'
|
|
&& github.event.pull_request.user.type != 'Bot'
|
|
&& !github.event.pull_request.draft)
|
|
|| (github.event_name == 'issue_comment'
|
|
&& github.event.issue.pull_request
|
|
&& github.event.issue.state == 'open'
|
|
&& startsWith(github.event.comment.body, '@claude review')
|
|
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
concurrency:
|
|
group: claude-review-${{ github.event.pull_request.number || github.event.issue.number }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: read
|
|
id-token: write
|
|
steps:
|
|
- name: Record when this run started
|
|
id: started
|
|
run: echo "at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
|
# A custom prompt puts the action in agent mode, which never reacts on its
|
|
# own, so the requester gets no sign the run started.
|
|
- name: Acknowledge the request
|
|
if: github.event_name == 'issue_comment'
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
COMMENT_ID: ${{ github.event.comment.id }}
|
|
run: gh api "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" -f content=eyes
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
# An `@claude review` vouches for the head that existed when it was typed;
|
|
# a push after it would swap the code out from under that approval.
|
|
- name: Pin the head this run reviews
|
|
id: pinned-sha
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
PAYLOAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
COMMENT_AT: ${{ github.event.comment.created_at }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -n "$PAYLOAD_SHA" ]; then
|
|
echo "sha=${PAYLOAD_SHA}" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
head=$(gh api "repos/${REPO}/pulls/${PR}" --jq '"\(.head.sha) \(.head.repo.pushed_at // "")"')
|
|
HEAD_SHA=${head%% *}
|
|
HEAD_PUSHED_AT=${head#* }
|
|
if [ -z "$HEAD_PUSHED_AT" ]; then
|
|
gh pr comment "$PR" --repo "$REPO" --body "The head repository of this pull request is gone, so the code to review cannot be verified. Nothing was reviewed."
|
|
echo "::error::The head repository is unavailable; refusing to check it out."
|
|
exit 1
|
|
fi
|
|
if [ "$(date -d "$HEAD_PUSHED_AT" +%s)" -gt "$(date -d "$COMMENT_AT" +%s)" ]; then
|
|
gh pr comment "$PR" --repo "$REPO" --body "The head branch was pushed to at ${HEAD_PUSHED_AT}, after this review was requested at ${COMMENT_AT}, so the code that would be checked out here is not the code the request vouched for. Nothing was reviewed. Ask again to review the current head."
|
|
echo "::error::The head moved after the request; refusing to check it out."
|
|
exit 1
|
|
fi
|
|
echo "sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
|
|
# An automatic re-review of a head that already has one spends a whole run
|
|
# to reach the same conclusion, so settle it here rather than in the model.
|
|
- name: Skip a head that already has a review
|
|
id: reviewed
|
|
if: github.event_name == 'pull_request_target'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
PR: ${{ github.event.pull_request.number }}
|
|
HEAD_SHA: ${{ steps.pinned-sha.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
posted=$(gh api "repos/${REPO}/issues/${PR}/comments" --paginate \
|
|
--jq "[.[] | select(.user.login == \"github-actions[bot]\") | select((.body | contains(\"Reviewed head:\")) and (.body | contains(\"${HEAD_SHA}\")))] | length")
|
|
if [ "$posted" != "0" ]; then
|
|
echo "done=true" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::#${PR} already carries a review of ${HEAD_SHA}; nothing to review."
|
|
fi
|
|
# Read-only, and pinned to one immutable commit: this job holds a
|
|
# write-scoped token, so running anything out of pr-head/ would be a pwn-request.
|
|
- uses: actions/checkout@v7
|
|
if: steps.reviewed.outputs.done != 'true'
|
|
with:
|
|
ref: ${{ steps.pinned-sha.outputs.sha }}
|
|
path: pr-head
|
|
persist-credentials: false
|
|
allow-unsafe-pr-checkout: true
|
|
# The skill reads CLAUDE.md on its own but not REVIEW.md, and knows nothing
|
|
# of pr-head/ or this run's head: the brief is the only way both reach it.
|
|
- name: Brief the reviewer
|
|
if: steps.reviewed.outputs.done != 'true'
|
|
env:
|
|
REPO: ${{ github.repository }}
|
|
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
HEAD_SHA: ${{ steps.pinned-sha.outputs.sha }}
|
|
TRIGGER: ${{ github.event_name }} / ${{ github.event.action }}
|
|
run: |
|
|
set -euo pipefail
|
|
{
|
|
cat .github/claude/review-job.md
|
|
printf '\n## This run\n\n'
|
|
printf -- '- Repository: %s\n' "$REPO"
|
|
printf -- '- Pull request: #%s\n' "$PR"
|
|
printf -- '- Head under review, checked out read-only in pr-head/: %s\n' "$HEAD_SHA"
|
|
printf -- '- Trigger: %s\n' "$TRIGGER"
|
|
printf -- '- CI on that head: gh api repos/%s/commits/%s/check-runs\n' "$REPO" "$HEAD_SHA"
|
|
} > "$RUNNER_TEMP/review-brief.md"
|
|
- uses: anthropics/claude-code-action@v1
|
|
id: review
|
|
if: steps.reviewed.outputs.done != 'true'
|
|
# A refused run fails this step exactly like a real defect would, so the
|
|
# job classifies the failure below instead of going red on both alike.
|
|
continue-on-error: true
|
|
with:
|
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
allowed_non_write_users: "*"
|
|
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
|
|
plugins: "code-review@claude-code-plugins"
|
|
prompt: "/code-review:code-review max --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number || github.event.issue.number }}"
|
|
# allowedTools only pre-approves; it denies nothing. Only the deny
|
|
# list stops the review executing what it just checked out.
|
|
claude_args: |
|
|
--model claude-opus-5
|
|
--effort xhigh
|
|
--max-turns 100
|
|
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh api:*),Bash(gh pr diff:*),Bash(grep:*),Bash(rg:*),Bash(ls:*),Bash(find:*),Bash(sed:*),Bash(git log:*),Bash(git show:*),Bash(git diff:*),Bash(go doc:*),Bash(go env:*),Read,Glob,Grep,WebFetch,WebSearch"
|
|
--disallowedTools "Bash(go build:*),Bash(go run:*),Bash(go test:*),Bash(go generate:*),Bash(go install:*),Bash(make:*),Bash(npm:*),Bash(npx:*),Bash(pnpm:*),Bash(yarn:*),Bash(node:*),Bash(bash:*),Bash(sh:*),Bash(docker:*),Bash(chmod:*),Edit,Write,NotebookEdit"
|
|
--append-system-prompt-file ${{ runner.temp }}/review-brief.md
|
|
- name: Upload the run transcript
|
|
if: always()
|
|
env:
|
|
NODE_OPTIONS: ""
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: claude-review-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/claude-execution-output.json
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
# An exhausted usage window or an overloaded API is not a broken workflow.
|
|
# Say so where the maintainer will see it, and leave the job green.
|
|
- name: Report a review the API refused to run
|
|
id: throttled
|
|
if: ${{ !cancelled() && steps.review.outcome == 'failure' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
TRANSCRIPT: ${{ runner.temp }}/claude-execution-output.json
|
|
run: |
|
|
set -euo pipefail
|
|
[ -f "$TRANSCRIPT" ] || exit 0
|
|
if jq -e 'any(.[]; .type == "rate_limit_event" and .rate_limit_info.status == "rejected")' "$TRANSCRIPT" >/dev/null 2>&1; then
|
|
reason="the account's usage limit was already spent when this run started"
|
|
elif jq -e 'any(.[]; .subtype == "api_retry" and .error_status == 529)' "$TRANSCRIPT" >/dev/null 2>&1; then
|
|
reason="the API stayed overloaded through every retry"
|
|
else
|
|
exit 0
|
|
fi
|
|
echo "skipped=true" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::No review of #${PR}: ${reason}."
|
|
gh pr comment "$PR" --repo "$REPO" --body "No review ran on this head: ${reason}. Nothing in this pull request was examined. A maintainer can ask for one with \`@claude review\`."
|
|
- name: Fail if the review posted nothing
|
|
if: ${{ !cancelled() && steps.pinned-sha.outcome == 'success' && steps.reviewed.outputs.done != 'true' && steps.throttled.outputs.skipped != 'true' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
STARTED_AT: ${{ steps.started.outputs.at }}
|
|
run: |
|
|
set -euo pipefail
|
|
head=$(gh api "repos/${REPO}/pulls/${PR}" --jq '.head.sha')
|
|
# updated_at, not created_at: the skill may update its existing sticky comment.
|
|
# "Reviewed head:" as well as the SHA — the bot's other comments quote SHAs too.
|
|
posted=$(gh api "repos/${REPO}/issues/${PR}/comments" --paginate \
|
|
--jq "[.[] | select(.user.login == \"github-actions[bot]\") | select((.updated_at >= \"${STARTED_AT}\") or ((.body | contains(\"Reviewed head:\")) and (.body | contains(\"${head}\"))))] | length")
|
|
inline=$(gh api "repos/${REPO}/pulls/${PR}/comments" --paginate \
|
|
--jq "[.[] | select(.user.login == \"github-actions[bot]\") | select(.updated_at >= \"${STARTED_AT}\")] | length")
|
|
if [ "$posted" = "0" ] && [ "$inline" = "0" ]; then
|
|
echo "::error::The review run ended without posting a review of ${head} on #${PR}. Read the uploaded transcript before re-running."
|
|
exit 1
|
|
fi
|