Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .github/workflows/reviewer-bot-followup.yml
Original file line number Diff line number Diff line change
@@ -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 '<!-- pr-review-bot:v1 followup'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "reason=trigger comment is itself a bot follow-up (loop prevention)" >> "$GITHUB_OUTPUT"
exit 0
fi
if printf '%s' "$COMMENT_BODY" | grep -q '<!-- pr-review-bot:v1 reconcile -->'; 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
126 changes: 126 additions & 0 deletions .github/workflows/reviewer-bot.yml
Original file line number Diff line number Diff line change
@@ -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
Loading