diff --git a/.github/workflows/pr-assign-reviewer.yml b/.github/workflows/pr-assign-reviewer.yml new file mode 100644 index 000000000..228b9046a --- /dev/null +++ b/.github/workflows/pr-assign-reviewer.yml @@ -0,0 +1,67 @@ +name: Assign PR Reviewer (round-robin) + +# Auto-assigns one reviewer to each new external PR, so community contributions +# have a named, accountable reviewer instead of sitting in a shared queue. PRs +# authored by someone in AUTHORIZED_USERS are left unassigned. That same list is +# the reviewer pool, so there is no separate roster to maintain. +# +# Uses pull_request_target (same rationale as pr-size.yml): a fork PR's default +# token is read-only and cannot request reviewers, so we need the base-repo token. +# Safe because this workflow ONLY reads PR metadata and assigns a reviewer — it +# never checks out or executes untrusted PR code. +on: + pull_request_target: + types: [opened, ready_for_review] + branches: [main, feat/**] + +jobs: + assign-reviewer: + # Skip drafts — assign when a PR is actually ready for review. + if: github.event.pull_request.draft == false + runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + permissions: + pull-requests: write + steps: + - name: Assign a round-robin reviewer from AUTHORIZED_USERS + uses: actions/github-script@v9 + env: + AUTHORIZED_USERS: ${{ secrets.AUTHORIZED_USERS }} + with: + script: | + const pr = context.payload.pull_request; + const { owner, repo } = context.repo; + const author = pr.user.login; + const pool = (process.env.AUTHORIZED_USERS || '') + .split(',') + .map(login => login.trim()) + .filter(Boolean) + .sort(); + + if (pool.length === 0) { + console.log('No reviewers configured in AUTHORIZED_USERS.'); + return; + } + + // Only external contributions need automatic reviewer assignment. + if (pool.some(login => login.toLowerCase() === author.toLowerCase())) { + console.log(`${author} is in AUTHORIZED_USERS; leaving the PR unassigned.`); + return; + } + + // Don't re-assign if a reviewer was already requested (e.g. manual, or a re-run). + if ((pr.requested_reviewers?.length ?? 0) > 0 || (pr.requested_teams?.length ?? 0) > 0) { + console.log('Reviewer(s) already requested; leaving as-is.'); + return; + } + + // Stateless round-robin: PR number modulo pool size. Deterministic, no state + // file to keep in sync, and spreads load evenly as PR numbers increase. + const reviewer = pool[pr.number % pool.length]; + console.log(`Requesting review from ${reviewer} (pool of ${pool.length}, author ${author}).`); + + await github.rest.pulls.requestReviewers({ + owner, + repo, + pull_number: pr.number, + reviewers: [reviewer], + });