Skip to content
Draft
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
54 changes: 54 additions & 0 deletions .github/actions/setup-claude-sdk/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) 2025 ADBC Drivers Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.

# Install the Claude Agent SDK (pip) + the Claude Code CLI (npm) that the bots
# now spawn, via Databricks' internal JFrog mirror. The protected runner group
# is egress-blocked from pypi.org and registry.npmjs.org, so this configures
# both package managers through JFrog (same mechanism as setup-jfrog / the
# sdk-smoke workflow, which is verified working end-to-end against the gateway).
#
# Requires `id-token: write` on the calling job (for the JFrog OIDC exchange).
# Idempotent w.r.t. a prior setup-jfrog step (re-exchanging the token + re-
# setting PIP_INDEX_URL is harmless).
name: Setup Claude Agent SDK
description: Install claude-agent-sdk (pip) + Claude Code CLI (npm) via the internal JFrog mirror.

inputs:
requirements-file:
description: >-
Path to the SDK requirements file, relative to the job working directory
($GITHUB_WORKSPACE — composite `run:` steps always execute there, not at
the caller's working-directory). Defaults to scripts/requirements-sdk.txt,
correct when the repo is checked out at the workspace root (e.g. the
reviewer bots). Workflows that check the repo into a subdir (the engineer
bots use `path: internal-repo`) MUST pass the prefixed path, e.g.
internal-repo/scripts/requirements-sdk.txt.
required: false
default: scripts/requirements-sdk.txt

runs:
using: composite
steps:
- name: Setup Node (for the Claude Code CLI the SDK spawns)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '20'

- name: Configure JFrog (pip + npm internal mirror)
uses: ./.github/actions/setup-jfrog
with:
configure-pip: 'true'
configure-npm: 'true'

- name: Install claude-agent-sdk + Claude Code CLI
shell: bash
run: |
python -m pip install --upgrade pip
# Install the SDK from the documented requirements file (not a bare
# `pip install claude-agent-sdk`) so CI and local docs share one
# dependency source and future pins land in a single place. (Review)
pip install -r "${{ inputs.requirements-file }}"
npm i -g @anthropic-ai/claude-code
echo "claude CLI: $(command -v claude || echo 'NOT FOUND')"
78 changes: 78 additions & 0 deletions .github/actions/setup-jfrog/setup-jfrog/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Setup JFrog OIDC
description: Obtain a JFrog access token via GitHub OIDC and configure package managers

inputs:
configure-pip:
description: Configure pip to use JFrog PyPI proxy
default: "true"
configure-cargo:
description: Configure Cargo to use JFrog crates proxy
default: "false"
configure-npm:
description: Configure npm to use JFrog npm proxy
default: "false"

runs:
using: composite
steps:
- name: Get JFrog OIDC token
shell: bash
run: |
set -euo pipefail
ID_TOKEN=$(curl -sLS \
-H "User-Agent: actions/oidc-client" \
-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=jfrog-github" | jq .value | tr -d '"')
echo "::add-mask::${ID_TOKEN}"
ACCESS_TOKEN=$(curl -sLS -XPOST -H "Content-Type: application/json" \
"https://databricks.jfrog.io/access/api/v1/oidc/token" \
-d "{\"grant_type\": \"urn:ietf:params:oauth:grant-type:token-exchange\", \"subject_token_type\":\"urn:ietf:params:oauth:token-type:id_token\", \"subject_token\": \"${ID_TOKEN}\", \"provider_name\": \"github-actions\"}" | jq .access_token | tr -d '"')
echo "::add-mask::${ACCESS_TOKEN}"
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
echo "FAIL: Could not extract JFrog access token"
exit 1
fi
echo "JFROG_ACCESS_TOKEN=${ACCESS_TOKEN}" >> "$GITHUB_ENV"
echo "JFrog OIDC token obtained successfully"

- name: Configure pip
if: inputs.configure-pip == 'true'
shell: bash
run: |
set -euo pipefail
echo "PIP_INDEX_URL=https://gha-service-account:${JFROG_ACCESS_TOKEN}@databricks.jfrog.io/artifactory/api/pypi/db-pypi/simple" >> "$GITHUB_ENV"
echo "pip configured to use JFrog registry"

- name: Configure Cargo
if: inputs.configure-cargo == 'true'
shell: bash
run: |
set -euo pipefail
mkdir -p ~/.cargo
cat > ~/.cargo/config.toml << EOF
[source.crates-io]
replace-with = "jfrog"
[source.jfrog]
registry = "sparse+https://databricks.jfrog.io/artifactory/api/cargo/db-cargo-remote/index/"
[registries.jfrog]
index = "sparse+https://databricks.jfrog.io/artifactory/api/cargo/db-cargo-remote/index/"
credential-provider = ["cargo:token"]
EOF
cat > ~/.cargo/credentials.toml << EOF
[registries.jfrog]
token = "Bearer ${JFROG_ACCESS_TOKEN}"
EOF
echo "CARGO_REGISTRIES_JFROG_TOKEN=Bearer ${JFROG_ACCESS_TOKEN}" >> "$GITHUB_ENV"
echo "Cargo configured to use JFrog registry"

- name: Configure npm
if: inputs.configure-npm == 'true'
shell: bash
run: |
set -euo pipefail
cat > ~/.npmrc << EOF
registry=https://databricks.jfrog.io/artifactory/api/npm/db-npm/
//databricks.jfrog.io/artifactory/api/npm/db-npm/:_authToken=${JFROG_ACCESS_TOKEN}
always-auth=true
EOF
echo "npm configured to use JFrog registry"
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
34 changes: 34 additions & 0 deletions .github/workflows/reviewer-bot-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Unit tests for the migrated PR-review bot (scripts/reviewer_bot + scripts/shared).
#
# Pure-logic tests — no secrets, no model calls, no Claude Agent SDK install
# (the SDK import is guarded, so the tests run with just pytest, which the
# connector's poetry dev-deps already provide). Runs only when the bot code
# changes, so it doesn't add load to every connector PR.
name: Reviewer Bot Unit Tests

on:
pull_request:
paths:
- 'scripts/shared/**'
- 'scripts/reviewer_bot/**'
- '.github/workflows/reviewer-bot-unit-tests.yml'

permissions:
contents: read

jobs:
unit-tests:
name: Reviewer Bot Unit Tests
runs-on:
group: databricks-protected-runner-group
labels: linux-ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Poetry
uses: ./.github/actions/setup-poetry
with:
python-version: "3.11"
- name: Run reviewer-bot + shared unit tests
run: poetry run python -m pytest scripts/reviewer_bot/tests scripts/shared/tests
Loading
Loading