From 7ab351b6725196611cf99c37600d969d6475377a Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 4 Jun 2026 01:04:20 -0700 Subject: [PATCH] feat(bots): reviewer-bot live workflows (review + follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third of the stacked reviewer-bot migration. Adds the live workflows that run the bot on PRs: - reviewer-bot.yml — reviews on pull_request (opened/synchronize/reopened/ ready_for_review) + manual workflow_dispatch (dry-run capable). Fork-guarded; protected runner; mints a peco-review-bot App token; setup-claude-sdk for the SDK/CLI install. Reads/explores the PR's own checkout (no driver clone). - reviewer-bot-followup.yml — responds to pull_request_review_comment with the cheap pre-checkout filter + the marker-based loop guards. Adapted from the driver-test workflows: removed the driver-repo clone auth (INTEGRATION_TEST_APP_TOKEN — N/A here) and made MODEL_ENDPOINT a secret rather than a hardcoded workspace URL. PREREQS (these workflows stay inert until provided): - peco-review-bot GitHub App installed on this repo (Pull requests / Issues / Contents: Read & Write). - Secrets: REVIEW_BOT_APP_ID, REVIEW_BOT_APP_PRIVATE_KEY, MODEL_ENDPOINT; DATABRICKS_TOKEN authorized for that serving endpoint. Co-authored-by: Isaac Signed-off-by: Eric Wang --- .github/workflows/reviewer-bot-followup.yml | 117 ++++++++++++++++++ .github/workflows/reviewer-bot.yml | 126 ++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 .github/workflows/reviewer-bot-followup.yml create mode 100644 .github/workflows/reviewer-bot.yml diff --git a/.github/workflows/reviewer-bot-followup.yml b/.github/workflows/reviewer-bot-followup.yml new file mode 100644 index 000000000..79779bafc --- /dev/null +++ b/.github/workflows/reviewer-bot-followup.yml @@ -0,0 +1,117 @@ +name: Reviewer Bot — Follow-up + +on: + pull_request_review_comment: + types: [created] + +permissions: + # The workflow GITHUB_TOKEN is not used to interact with the PR — we mint a + # dedicated peco-review-bot App installation token and use that everywhere. + # Required App permissions on the installation (NOT this workflow): + # Pull requests: Read & Write — posting inline replies + # Issues: Read & Write — comment plumbing + # Contents: Read & Write — resolveReviewThread mutation + # (Pull-requests:write is NOT sufficient for the resolve mutation; + # GitHub gates it behind Contents.) + contents: read + id-token: write # JFrog OIDC exchange for the SDK/CLI install (setup-claude-sdk) + +jobs: + followup: + # SECURITY: skip fork PRs — keep DATABRICKS_TOKEN out of untrusted code's + # reach. Mirrors the guard in reviewer-bot.yml. + if: github.event.pull_request.head.repo.fork == false && github.event.pull_request.state == 'open' + runs-on: + group: databricks-protected-runner-group + labels: [linux-ubuntu-latest] + timeout-minutes: 10 + steps: + - name: Mint review-bot App token + id: app-token + uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + with: + app-id: ${{ secrets.REVIEW_BOT_APP_ID }} + private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }} + + - name: Cheap pre-checkout filter + id: filter + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + TRIGGER_ID: ${{ github.event.comment.id }} + IN_REPLY_TO: ${{ github.event.comment.in_reply_to_id }} + COMMENT_USER: ${{ github.event.comment.user.login }} + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + # Cheap filters first — skip the expensive checkout / python setup + # when the event is already known to be irrelevant. The Python entry + # point repeats these checks (defense in depth), so being slightly + # over-permissive here is safe. + # + # Filter 1: must be a reply to another inline comment. + if [ -z "$IN_REPLY_TO" ] || [ "$IN_REPLY_TO" = "null" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "reason=no in_reply_to_id (top-level review comment, not a thread reply)" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Filter 2: skip our own follow-up AND reconcile replies (loop + # prevention). MARKER-based — never login-based. + if printf '%s' "$COMMENT_BODY" | grep -q ''; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "reason=trigger comment is itself a bot reconcile reply (loop prevention)" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + + - name: Announce skip in step summary + if: steps.filter.outputs.skip == 'true' + run: | + { + echo "## Reviewer Bot — Follow-up" + echo "" + echo "**Skipped:** ${{ steps.filter.outputs.reason }}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Checkout + if: steps.filter.outputs.skip != 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + # The followup reads this checkout via read_paths/grep, so the + # persisted GITHUB_TOKEN must NOT sit in .git/config. The followup + # only POSTS replies via the minted App token. + persist-credentials: false + + - name: Setup Python + if: steps.filter.outputs.skip != 'true' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.11' + + - name: Setup Claude Agent SDK + CLI + if: steps.filter.outputs.skip != 'true' + uses: ./.github/actions/setup-claude-sdk + + - name: Run follow-up agent + if: steps.filter.outputs.skip != 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + TRIGGER_COMMENT_ID: ${{ github.event.comment.id }} + # PR SHA range — used by followup.py to restrict `git show` to commits + # actually in this PR (allowlist for SHA-diff verification). + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} + MODEL_ENDPOINT: ${{ secrets.MODEL_ENDPOINT }} + DRY_RUN: 'false' + RUNNER_TEMP: ${{ runner.temp }} + run: | + python -m scripts.reviewer_bot.followup diff --git a/.github/workflows/reviewer-bot.yml b/.github/workflows/reviewer-bot.yml new file mode 100644 index 000000000..ce5109544 --- /dev/null +++ b/.github/workflows/reviewer-bot.yml @@ -0,0 +1,126 @@ +name: Reviewer Bot + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to review' + required: true + type: string + dry_run: + description: 'Print what would be posted instead of posting' + required: false + default: 'true' + type: string + +permissions: + # The workflow GITHUB_TOKEN is not used to interact with the PR — we mint a + # dedicated peco-review-bot App installation token and use that everywhere. + # Required App permissions on the installation (NOT this workflow): + # Pull requests: Read & Write — posting findings + replies + # Issues: Read & Write — review status updates + # Contents: Read & Write — resolveReviewThread mutation + # (Pull-requests:write is NOT sufficient for the resolve mutation; + # GitHub gates it behind Contents.) + contents: read + id-token: write # JFrog OIDC exchange for the SDK/CLI install (setup-claude-sdk) + +jobs: + review: + # SECURITY: the fork == false guard keeps DATABRICKS_TOKEN + the App token + # out of untrusted fork code. Do not remove without alternative isolation. + if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false) + runs-on: + group: databricks-protected-runner-group + labels: [linux-ubuntu-latest] + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + # The reviewer reads this checkout via its read_paths/grep tools, so + # the persisted GITHUB_TOKEN must NOT sit in .git/config (it would be + # readable + leakable into a posted review). The bot posts via the + # minted App token, not the checkout's git creds. + persist-credentials: false + + - name: Mint review-bot App token + id: app-token + uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + with: + app-id: ${{ secrets.REVIEW_BOT_APP_ID }} + private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }} + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.11' + + - name: Resolve trigger inputs + id: inputs + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + RAW_PR="${{ inputs.pr_number }}" + DRY_RUN="${{ inputs.dry_run }}" + else + RAW_PR="${{ github.event.pull_request.number }}" + DRY_RUN="false" + fi + if ! [[ "$RAW_PR" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid pr_number '$RAW_PR'"; exit 1 + fi + HEAD_SHA=$(gh pr view "$RAW_PR" --repo "${{ github.repository }}" \ + --json headRefOid -q .headRefOid) + echo "pr_number=$RAW_PR" >> "$GITHUB_OUTPUT" + echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" + echo "dry_run=$DRY_RUN" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + - name: Setup Claude Agent SDK + CLI + uses: ./.github/actions/setup-claude-sdk + + - name: Checkout PR head into a SEPARATE dir for exploration (workflow_dispatch) + # On `pull_request` the initial checkout is already refs/pull/N/merge, so + # the reviewer's read_paths/grep explore the files under review (and the + # fork guard gates untrusted code). On `workflow_dispatch` the initial + # checkout is the dispatched ref (default branch) while the review targets + # inputs.pr_number — so check the PR head into a SEPARATE `pr-head/` dir + # and read its CONTENT via REVIEW_CONTENT_ROOT below. + # + # SECURITY: do NOT re-point the primary tree. `Run review bot` still runs + # scripts/reviewer_bot/* from the primary (trusted, default-branch) + # checkout; only the PR's file *content* is read out of pr-head/. The + # job's if: exempts workflow_dispatch from the fork guard, so swapping + # the primary tree would execute a fork PR's own bot code with secrets in + # scope. Reading content is safe (read_paths/grep enforce + # path-escape/.git/symlink guards); executing its code is not. + if: github.event_name == 'workflow_dispatch' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.inputs.outputs.head_sha }} + fetch-depth: 0 + persist-credentials: false + path: pr-head + + - name: Run review bot + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ steps.inputs.outputs.pr_number }} + HEAD_SHA: ${{ steps.inputs.outputs.head_sha }} + # Where the bot READS PR-head content from (repo rules, read_paths/grep). + # Set ONLY on workflow_dispatch (primary tree = trusted default branch, + # PR head in pr-head/). On `pull_request` it's empty, so the bot reads + # its own merge-ref checkout (fork-gated). Bot code always runs from + # the primary trusted checkout. + REVIEW_CONTENT_ROOT: ${{ github.event_name == 'workflow_dispatch' && format('{0}/pr-head', github.workspace) || '' }} + DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} + MODEL_ENDPOINT: ${{ secrets.MODEL_ENDPOINT }} + DRY_RUN: ${{ steps.inputs.outputs.dry_run }} + RUNNER_TEMP: ${{ runner.temp }} + run: | + python -m scripts.reviewer_bot.run_review