From 1e36467ef1596f7273e5c7379afcb44655278868 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sun, 10 May 2026 21:15:42 -0500 Subject: [PATCH] revert: restore .github/workflows/pr-review.yml Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-review.yml | 138 ++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .github/workflows/pr-review.yml diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 000000000..f7b1e5913 --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -0,0 +1,138 @@ +name: PR Review Agent + +on: + schedule: + # Hourly at :07 to avoid the top-of-hour cron stampede + - cron: "7 * * * *" + workflow_dispatch: + inputs: + pr_url: + description: "Optional: review a single PR URL instead of enumerating" + required: false + type: string + dry_run: + description: "If true, never submit reviews or comments" + required: false + default: "false" + type: string + force_review: + description: "If true, bypass idempotency check and re-review even if already reviewed at head SHA (use for @mention-triggered reviews)" + required: false + default: "false" + type: string + repository_dispatch: + # Triggered by the petry-projects/.github mention-listener workflow. + # Requires only Contents: write on this repo (not Actions: write). + # Payload: { pr_url: "https://github.com/...", force_review: "true" } + types: [pr-review-mention] + +permissions: + contents: read + pull-requests: write + checks: read + +concurrency: + # Scheduled and manual batch runs share one slot to prevent overlap. + # Mention-triggered runs (repository_dispatch or workflow_dispatch with pr_url) + # get per-PR slots so they don't queue behind hourly batches or each other. + group: ${{ (inputs.pr_url || github.event.client_payload.pr_url) && format('pr-review-mention-{0}', inputs.pr_url || github.event.client_payload.pr_url) || 'pr-review-scheduled' }} + cancel-in-progress: false + +jobs: + review: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + # Auth for every gh / script call in this job. The PAT must belong to BOT_USER. + GH_TOKEN: ${{ secrets.DON_PETRY_BOT_GH_PAT }} + # Single bot identity. DON_PETRY_BOT_GH_PAT authenticates as BOT_USER; that + # account owns/has-access-to the personal repos scanned by list-prs.sh, + # signs the cascade's escalation comments, and is filtered out of the + # candidate queue (it can't approve its own PRs). Human reviewers are + # routed via CODEOWNERS at escalation time, not pinned via env. + BOT_USER: ${{ vars.BOT_USER || 'don-petry-bot' }} + TARGET_ORG: ${{ vars.TARGET_ORG || 'petry-projects' }} + AGENT_REPO: ${{ github.repository }} + # Review engine: "claude" or "copilot". Controls which CLI and models are used. + REVIEW_ENGINE: ${{ vars.REVIEW_ENGINE || 'claude' }} + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Copilot engine reuses GH_PAT (a separate user PAT with Copilot subscription). + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + # Default to dry-run. workflow_dispatch can override; repository_dispatch + # (mention) always runs live; vars.LIVE_MODE=true switches scheduled runs live. + DRY_RUN: ${{ github.event_name == 'repository_dispatch' && 'false' || inputs.dry_run || (vars.LIVE_MODE == 'true' && 'false' || 'true') }} + PR_URL_OVERRIDE: ${{ inputs.pr_url || github.event.client_payload.pr_url }} + DELEGATION_ORGS: ${{ vars.DELEGATION_ORGS || vars.CLAUDE_ORGS || '' }} + MAX_REVIEW_CYCLES: ${{ vars.MAX_REVIEW_CYCLES || '3' }} + MAX_PRS: ${{ vars.MAX_PRS || '10' }} + CANDIDATE_LIMIT: ${{ vars.CANDIDATE_LIMIT || '100' }} + # Pin via vars.CLAUDE_CODE_VERSION for fully reproducible caching; default + # 'latest' caches an install indefinitely under that key — flush via the + # Actions cache UI or bump the variable to pick up new releases. + CLAUDE_CODE_VERSION: ${{ vars.CLAUDE_CODE_VERSION || 'latest' }} + # Mention-triggered reviews always force; workflow_dispatch respects the input. + FORCE_REVIEW: ${{ github.event_name == 'repository_dispatch' && 'true' || inputs.force_review || 'false' }} + + steps: + - name: Checkout agent repo + uses: actions/checkout@v5 + + - name: Cache claude-code CLI + uses: actions/cache@v4 + with: + path: ~/.npm-global + key: claude-code-${{ env.CLAUDE_CODE_VERSION }}-${{ runner.os }} + + - name: Install review engine CLIs + run: | + set -euo pipefail + gh auth status + + # Use a per-user prefix so actions/cache (above) can persist the + # install across runs without needing root. Add the bin dir to PATH + # for both this step and every subsequent step in the job. + mkdir -p "$HOME/.npm-global" + npm config set prefix "$HOME/.npm-global" + echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.npm-global/bin:$PATH" + + install_claude() { + if command -v claude >/dev/null 2>&1; then + echo "claude-code cache hit: $(claude --version)" + return 0 + fi + npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" + } + + case "$REVIEW_ENGINE" in + claude) + install_claude + if ! env GH_TOKEN="$COPILOT_GITHUB_TOKEN" gh copilot --version > /dev/null 2>&1; then + echo "::warning::gh copilot built-in unavailable with user token — rate-limit fallback will be unavailable." + fi + ;; + copilot) + gh extension install github/gh-copilot + install_claude || true + ;; + *) + echo "::error::Unknown REVIEW_ENGINE=$REVIEW_ENGINE" + exit 1 + ;; + esac + + - name: Enumerate candidate PRs + run: | + set -euo pipefail + if [ -n "${PR_URL_OVERRIDE}" ]; then + printf '%s\n' "${PR_URL_OVERRIDE}" > prs.txt + else + bash scripts/list-prs.sh > prs.txt + fi + count=$(grep -c . prs.txt || true) + echo "::notice::Candidate pool has $count PRs (no-ops will be skipped, MAX_PRS caps actual reviews)" + echo "--- candidate PRs ---" + cat prs.txt || true + + - name: Review each PR (cascade) + run: bash scripts/review-batch.sh