Skip to content

Commit 92dc945

Browse files
fix(bots): rebuild reviewer on composite actions (reusable workflow won't resolve cross-repo)
Live CI showed the reviewer's reusable-WORKFLOW caller failing at parse time: "error parsing called workflow ... reviewer-bot.reusable.yml@b6205fb : workflow was not found." The reusable exists at that SHA and engine Actions access is `organization`, but cross-repo resolution of a reusable *workflow* in the internal engine repo does not succeed from here. No consumer uses that pattern; databricks-driver-test + adbc call the engine via composite ACTIONS, and driver-test's engineer bot (bot-run@<sha>) runs in production — proving cross-repo composite actions DO resolve in-org. Rebuild reviewer-bot.yml + reviewer-bot-followup.yml as own-job composite callers (matching the engineer bot + driver-test): - checkout -> bot-setup@sha -> mint engine-scoped App token -> install-bot-engine@sha (App-auth, PAT-free) -> reviewer_bot.run_review / .followup - security floor (fork gate, open-PR check), input resolution, and base-commit fetch replicated inline from the reusable - environment: azure-prod (own job can declare it — also fixes the reusable caller's inability to read env-scoped DATABRICKS_* secrets) Also add permission-contents: read to the engineer bots' engine-scoped token mints (least privilege; matches the engine reusable). All four bot workflows now use one mechanism: own job + cross-repo composite actions + engine-scoped App token. PAT-free throughout; no BOT_ENGINE_PAT. Co-authored-by: Isaac
1 parent be555b4 commit 92dc945

5 files changed

Lines changed: 228 additions & 45 deletions

File tree

.github/workflows/engineer-bot-followup.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ jobs:
103103
private-key: ${{ secrets.ENGINEER_BOT_APP_PRIVATE_KEY }}
104104
owner: databricks
105105
repositories: databricks-bot-engine
106+
permission-contents: read
106107

107108
# Product env-prep: install the connector's own deps so a fixup can be
108109
# verified with `poetry run python -m pytest` (bot-run installs only the

.github/workflows/engineer-bot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
private-key: ${{ secrets.ENGINEER_BOT_APP_PRIVATE_KEY }}
8282
owner: databricks
8383
repositories: databricks-bot-engine
84+
permission-contents: read
8485

8586
- name: Resolve issue + gather context
8687
id: ctx
Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
# Reviewer Bot — follow-up on review-comment replies.
22
#
3-
# Thin caller of the databricks-bot-engine reusable workflow. The engine owns
4-
# the security + correctness floor (fork==false, PR open), token mint, engine
5-
# install, and the agent invocation; loop-prevention (skip own reply / reconcile
6-
# / non-reply comments) lives in the engine's followup.py post-checkout. This
7-
# file is just `on:` + `uses:` + `secrets:`.
3+
# Own-job composite caller (see reviewer-bot.yml for why we don't use the engine
4+
# reusable WORKFLOW: cross-repo reusable-workflow calls to the private engine
5+
# fail "workflow was not found"; cross-repo composite ACTIONS resolve in-org).
6+
# This file owns the job and replicates the reusable followup's steps with
7+
# cross-repo action refs, PAT-free (engine-scoped App token).
88
#
9-
# Enablement policy: NO `if:` gate — engage every non-fork open PR, matching this
10-
# repo's prior behavior. (To restrict later, add a caller-job `if:`, e.g. a
11-
# `review-bot` label gate; it ANDs with the engine's security floor.)
12-
#
13-
# Engine pin: the SHA below is used in BOTH the `uses:` ref AND `engine-ref:`.
14-
# Bump both in lockstep with reviewer-bot.yml; never use @main.
9+
# Enablement: no label gate — engage every non-fork open PR (this repo's policy).
10+
# The security floor (fork==false, PR open) is the job `if:` below; loop-
11+
# prevention lives in the engine's followup.py post-checkout.
1512
name: Reviewer Bot — Follow-up
1613

1714
on:
@@ -27,14 +24,87 @@ permissions:
2724

2825
jobs:
2926
followup:
30-
uses: databricks/databricks-bot-engine/.github/workflows/reviewer-bot-followup.reusable.yml@b6205fb502566d617a456ccf3c37f3e4e3072ccd
31-
with:
32-
engine-ref: b6205fb502566d617a456ccf3c37f3e4e3072ccd
33-
# PAT-free install (see reviewer-bot.yml). Mints an engine-scoped App token
34-
# from the review-bot App creds; no BOT_ENGINE_PAT needed.
35-
engine-auth: app
36-
secrets:
37-
review-bot-app-id: ${{ secrets.REVIEW_BOT_APP_ID }}
38-
review-bot-app-private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }}
39-
databricks-token: ${{ secrets.DATABRICKS_TOKEN }}
40-
databricks-host: ${{ secrets.DATABRICKS_HOST }}
27+
# SECURITY FLOOR: never run the agent on a fork PR (keeps App + model tokens
28+
# off untrusted code); only act on OPEN PRs.
29+
if: >-
30+
github.event.pull_request.head.repo.fork == false
31+
&& github.event.pull_request.state == 'open'
32+
environment: azure-prod # DATABRICKS_HOST / DATABRICKS_TOKEN live here
33+
runs-on:
34+
group: databricks-protected-runner-group
35+
labels: [linux-ubuntu-latest]
36+
timeout-minutes: 20
37+
concurrency:
38+
group: reviewer-bot-followup-pr-${{ github.event.pull_request.number }}
39+
cancel-in-progress: false
40+
steps:
41+
# Review-bot token (posts inline replies + resolves threads).
42+
- name: Mint review-bot App token
43+
id: app-token
44+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
45+
with:
46+
app-id: ${{ secrets.REVIEW_BOT_APP_ID }}
47+
private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }}
48+
49+
- name: Checkout PR head (read-only; agent only POSTS via GH_TOKEN)
50+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
51+
with:
52+
ref: ${{ github.event.pull_request.head.sha }}
53+
fetch-depth: 0
54+
persist-credentials: false
55+
56+
# followup.py runs `git rev-list <base>..<head>` for finding-anchor SHA
57+
# verification. fetch-depth:0 usually brings the base along, but not
58+
# guaranteed. Best-effort fetch when missing; NEVER fatal — a missing base
59+
# makes the rev-list helper fail-closed to an empty allowlist (SHA
60+
# verification degrades, reconciliation still runs). The persist-
61+
# credentials:false checkout means this fetch has no auth, so it may fail
62+
# on a private repo; swallow it and warn.
63+
- name: Ensure PR base commit is present (for rev-list base..head)
64+
env:
65+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
66+
run: |
67+
if git cat-file -e "${PR_BASE_SHA}^{commit}" 2>/dev/null; then
68+
exit 0
69+
fi
70+
if ! git fetch --no-tags --depth=1 origin "$PR_BASE_SHA"; then
71+
echo "::warning::Could not fetch PR base commit ${PR_BASE_SHA}. Continuing: followup.py fail-closes to an empty SHA allowlist, so SHA verification degrades gracefully but reconciliation still runs." >&2
72+
fi
73+
74+
- name: Setup Python
75+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
76+
with:
77+
python-version: '3.11'
78+
79+
# PAT-free engine install: engine-scoped token (contents:read) from the
80+
# review-bot App creds — SEPARATE from app-token above (which is scoped to
81+
# THIS repo for PR posting).
82+
- name: Mint engine-scoped install token
83+
id: engine-token
84+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
85+
with:
86+
app-id: ${{ secrets.REVIEW_BOT_APP_ID }}
87+
private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }}
88+
owner: databricks
89+
repositories: databricks-bot-engine
90+
permission-contents: read
91+
92+
- name: Install engine + Claude SDK/CLI
93+
uses: databricks/databricks-bot-engine/.github/actions/install-bot-engine@b6205fb502566d617a456ccf3c37f3e4e3072ccd
94+
with:
95+
engine-ref: b6205fb502566d617a456ccf3c37f3e4e3072ccd
96+
engine-pat: ${{ steps.engine-token.outputs.token }}
97+
98+
- name: Run reviewer follow-up
99+
env:
100+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
101+
GITHUB_REPOSITORY: ${{ github.repository }}
102+
PR_NUMBER: ${{ github.event.pull_request.number }}
103+
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
104+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
105+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
106+
MODEL_ENDPOINT: https://${{ secrets.DATABRICKS_HOST }}/serving-endpoints/databricks-claude-opus-4-8/invocations
107+
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
108+
DRY_RUN: 'false'
109+
RUNNER_TEMP: ${{ runner.temp }}
110+
run: python -m databricks_bot_engine.reviewer_bot.followup

.github/workflows/reviewer-bot.yml

Lines changed: 101 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
# Reviewer Bot — initial PR review.
22
#
3-
# Thin caller of the databricks-bot-engine reusable workflow. The engine owns
4-
# the whole job (security gate, App-token mint, Python setup, engine install,
5-
# MODEL_ENDPOINT construction, workflow_dispatch input validation, and the
6-
# PR-head content-root dispatch security); this file is just `on:` + `uses:` +
7-
# `secrets:`. See docs/superpowers/specs/2026-07-14-onboard-bot-engine-design.md
8-
# and the databricks-bot-engine README ("Adopting the bots in your repo").
3+
# Own-job composite caller of the databricks-bot-engine actions. We deliberately
4+
# do NOT use the engine's reusable WORKFLOW (jobs.<id>.uses:
5+
# .../reviewer-bot.reusable.yml@<sha>): a cross-repo reusable-workflow call to
6+
# the private engine fails to resolve ("workflow was not found"). Cross-repo
7+
# composite ACTIONS (steps: - uses: .../.github/actions/...@<sha>) DO resolve
8+
# in-org — this is the pattern databricks-driver-test's engineer bot uses in
9+
# production, and the one the engineer-bot workflows here use too. So this file
10+
# owns the job and replicates the reusable's steps (fork gate, token mint,
11+
# install, run) with cross-repo action refs.
912
#
10-
# Engine pin: the SHA below is used in BOTH the `uses:` ref AND `engine-ref:`
11-
# so the workflow and the installed engine match. Bump both in lockstep; never
12-
# use @main (force-pushable, and the job carries secrets).
13+
# Engine pin: the SHA below pins BOTH the action refs AND engine-ref, so the
14+
# workflow and installed engine match. Bump in lockstep; never @main.
15+
#
16+
# Auth: PAT-free. We mint a short-lived, engine-scoped App installation token
17+
# from the review-bot App creds and pass it as engine-pat to install-bot-engine
18+
# (no stored BOT_ENGINE_PAT).
1319
name: Reviewer Bot
1420

1521
on:
@@ -34,17 +40,89 @@ permissions:
3440

3541
jobs:
3642
review:
37-
uses: databricks/databricks-bot-engine/.github/workflows/reviewer-bot.reusable.yml@b6205fb502566d617a456ccf3c37f3e4e3072ccd
38-
with:
39-
engine-ref: b6205fb502566d617a456ccf3c37f3e4e3072ccd
40-
# PAT-free install: the reusable mints an engine-scoped App token from the
41-
# review-bot App creds it already receives (the App is installed on the
42-
# engine repo with contents:read). No BOT_ENGINE_PAT needed.
43-
engine-auth: app
44-
pr-number: ${{ inputs.pr_number }} # empty on pull_request events
45-
dry-run: ${{ inputs.dry_run }}
46-
secrets:
47-
review-bot-app-id: ${{ secrets.REVIEW_BOT_APP_ID }}
48-
review-bot-app-private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }}
49-
databricks-token: ${{ secrets.DATABRICKS_TOKEN }}
50-
databricks-host: ${{ secrets.DATABRICKS_HOST }}
43+
# SECURITY FLOOR (was the reusable's job `if:`): manual dispatch, OR a
44+
# non-draft, non-fork PR. fork == false keeps the App + model tokens off
45+
# untrusted code.
46+
if: >-
47+
github.event_name == 'workflow_dispatch'
48+
|| (github.event.pull_request.draft == false
49+
&& github.event.pull_request.head.repo.fork == false)
50+
environment: azure-prod # DATABRICKS_HOST / DATABRICKS_TOKEN live here
51+
runs-on:
52+
group: databricks-protected-runner-group
53+
labels: [linux-ubuntu-latest]
54+
timeout-minutes: 30
55+
steps:
56+
# Checkout FIRST. Default ref = the PR merge ref on pull_request, the
57+
# default branch on dispatch. persist-credentials:false — the reviewer
58+
# reads this tree via read_paths/grep, so no token may sit in .git/config.
59+
- name: Checkout
60+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
61+
with:
62+
fetch-depth: 0
63+
persist-credentials: false
64+
65+
# Mint the review-bot token (posts findings) + set up Python.
66+
- name: Setup
67+
id: setup
68+
uses: databricks/databricks-bot-engine/.github/actions/bot-setup@b6205fb502566d617a456ccf3c37f3e4e3072ccd
69+
with:
70+
app-id: ${{ secrets.REVIEW_BOT_APP_ID }}
71+
private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }}
72+
73+
# PAT-free engine install: mint a per-run token scoped to ONLY the engine
74+
# repo (contents:read) from the review-bot App creds. Requires the
75+
# review-bot App installed on databricks-bot-engine with contents:read.
76+
- name: Mint engine-scoped install token
77+
id: engine-token
78+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
79+
with:
80+
app-id: ${{ secrets.REVIEW_BOT_APP_ID }}
81+
private-key: ${{ secrets.REVIEW_BOT_APP_PRIVATE_KEY }}
82+
owner: databricks
83+
repositories: databricks-bot-engine
84+
permission-contents: read
85+
86+
- name: Install engine + Claude SDK/CLI
87+
uses: databricks/databricks-bot-engine/.github/actions/install-bot-engine@b6205fb502566d617a456ccf3c37f3e4e3072ccd
88+
with:
89+
engine-ref: b6205fb502566d617a456ccf3c37f3e4e3072ccd
90+
engine-pat: ${{ steps.engine-token.outputs.token }}
91+
92+
- name: Resolve trigger inputs
93+
id: inputs
94+
# SECURITY: dispatcher-supplied values pass via env, never interpolated
95+
# into the script body (an inline expression would inject at assignment,
96+
# before the digits-only check). Env values expand without re-parsing.
97+
env:
98+
GH_TOKEN: ${{ steps.setup.outputs.token }}
99+
EVENT_NAME: ${{ github.event_name }}
100+
INPUT_PR: ${{ inputs.pr_number }}
101+
INPUT_DRY_RUN: ${{ inputs.dry_run }}
102+
EVENT_PR: ${{ github.event.pull_request.number }}
103+
REPO: ${{ github.repository }}
104+
run: |
105+
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
106+
RAW_PR="$INPUT_PR"; DRY_RUN="$INPUT_DRY_RUN"
107+
else
108+
RAW_PR="$EVENT_PR"; DRY_RUN="false"
109+
fi
110+
[[ "$RAW_PR" =~ ^[0-9]+$ ]] || { echo "::error::Invalid pr_number '$RAW_PR'"; exit 1; }
111+
case "$DRY_RUN" in true|false) ;; *) DRY_RUN="true" ;; esac # fail safe
112+
HEAD_SHA=$(gh pr view "$RAW_PR" --repo "$REPO" --json headRefOid -q .headRefOid)
113+
{ echo "pr_number=$RAW_PR"; echo "head_sha=$HEAD_SHA"; echo "dry_run=$DRY_RUN"; } >> "$GITHUB_OUTPUT"
114+
115+
- name: Run reviewer
116+
env:
117+
GH_TOKEN: ${{ steps.setup.outputs.token }}
118+
GITHUB_REPOSITORY: ${{ github.repository }}
119+
PR_NUMBER: ${{ steps.inputs.outputs.pr_number }}
120+
HEAD_SHA: ${{ steps.inputs.outputs.head_sha }}
121+
DRY_RUN: ${{ steps.inputs.outputs.dry_run }}
122+
MODEL_ENDPOINT: https://${{ secrets.DATABRICKS_HOST }}/serving-endpoints/databricks-claude-opus-4-8/invocations
123+
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
124+
RUNNER_TEMP: ${{ runner.temp }}
125+
# The reviewer's repo-specific ADDITIVE guidance, read from the TRUSTED
126+
# checkout (run_review enforces containment). Engine owns the base prompt.
127+
REVIEW_SYSTEM_PROMPT: .bot/prompts/review/system.md
128+
run: python -m databricks_bot_engine.reviewer_bot.run_review

docs/superpowers/specs/2026-07-14-onboard-bot-engine-design.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,36 @@ both bots. Not verifiable/doable from this repo.
253253
(admin). First live signals: a test PR (reviewer) and a labelled throwaway
254254
issue (engineer author). To be driven once the App installation is confirmed;
255255
outcomes reported honestly rather than assumed.
256+
257+
## Update (2026-07-15b): reviewer rebuilt on composite actions
258+
259+
**Finding (blocker discovered in live CI):** the reviewer's original design —
260+
a thin caller of the engine's reusable WORKFLOW via `jobs.review.uses:
261+
databricks/databricks-bot-engine/.github/workflows/reviewer-bot.reusable.yml@<sha>`
262+
— fails at parse time with **"error parsing called workflow ... : workflow was
263+
not found."** The reusable file exists at the pinned SHA and the engine's Actions
264+
access is `organization`, but cross-repo resolution of a **reusable workflow** in
265+
a private/internal repo does not succeed from this repo. No consumer uses this
266+
pattern: `databricks-driver-test` and `adbc-drivers/databricks` both call the
267+
engine via **composite actions** in their own jobs, and driver-test's engineer
268+
bot (`uses: databricks/databricks-bot-engine/.github/actions/bot-run@<sha>`) runs
269+
in production — proving cross-repo **composite actions** DO resolve in-org.
270+
271+
**Fix:** rebuilt `reviewer-bot.yml` + `reviewer-bot-followup.yml` as own-job
272+
composite callers (matching the engineer bot + driver-test): checkout →
273+
`bot-setup@<sha>` → mint engine-scoped App token → `install-bot-engine@<sha>`
274+
(App-auth, PAT-free) → `python -m databricks_bot_engine.reviewer_bot.run_review`
275+
/ `.followup`. The security floor (fork gate, open-PR check), input resolution,
276+
and base-commit fetch are replicated inline from the reusable. `environment:
277+
azure-prod` is now declarable (own job) — which also resolves friction #3 (a
278+
reusable-workflow caller couldn't read env-scoped secrets).
279+
280+
**Engine feedback:** the documented "thin reusable-workflow caller" onboarding
281+
path for the reviewer does not work cross-repo. Either the engine must make its
282+
reusable workflows cross-repo resolvable, or the docs should steer consumers to
283+
the composite-action pattern (as used by every working consumer). Filed
284+
separately as an engine issue.
285+
286+
All four bot workflows now use the SAME mechanism — own job + cross-repo
287+
composite actions + engine-scoped App token — so the onboarding is internally
288+
consistent and uses only the resolution path proven to work.

0 commit comments

Comments
 (0)