-
Notifications
You must be signed in to change notification settings - Fork 13
243 lines (230 loc) · 13 KB
/
Copy pathengineer-bot-followup.yml
File metadata and controls
243 lines (230 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Engineer-bot — follow-up: responds to inline review comments on the bot's
# (or any `engineer-bot`-labeled) PRs — pushes fix commits + replies on threads.
# This is what closes the reviewer -> engineer loop: reviewer-bot posts findings,
# this workflow makes the engineer address them.
#
# ⚠️ DRAFT / NOT YET FUNCTIONAL. Requires (see .bot/SETUP.md):
# - secrets BOT_ENGINE_PAT, ENGINEER_BOT_APP_ID/PRIVATE_KEY (DATABRICKS_HOST + DATABRICKS_TOKEN already exist in adbc CI)
# - the engineer-bot App's login must match `peco-engineer-bot` (the loop-guard
# below + .bot/config.yaml `bot_login_prefix`); rename both if your App differs
# - the `engineer-bot` label: PRs the bot may take over carry it (the bug-fix
# bot's own fix PRs, or a maintainer applies it to a human PR)
#
# Self-contained (a cross-org `uses:` of the hub's internal composite action does
# NOT work) — installs the engine via the PAT + the Claude SDK/CLI inline.
# Supply-chain pin: ENGINE_REF in the install step is pinned to an immutable commit SHA
# (never @main); claude-agent-sdk / @anthropic-ai/claude-code are unpinned to match the hub.
# When the engine repo goes public, drop the PAT (anonymous install) but keep ENGINE_REF pinned.
name: Engineer Bot — Follow-up
on:
pull_request_review_comment:
types: [created]
# Fire when the `engineer-bot` label is applied, so the bot picks up inline
# comments posted BEFORE the label. followup.py's catch-up mode then scans the
# PR for ALL unaddressed threads regardless of which event woke it.
pull_request:
types: [labeled]
permissions:
contents: write # the App token pushes fix commits to the PR branch
pull-requests: write # post inline replies on review threads
id-token: write # model OIDC, if your endpoint uses it; drop otherwise
concurrency:
# One followup per PR; queue (don't cancel) — catch-up mode means the survivor
# sweeps ALL open threads, so cancelled queued runs are harmless.
group: engineer-bot-followup-pr-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
followup:
# SECURITY: skip forks (LLM/model secrets). Operate only on non-fork, open
# PRs that carry the `engineer-bot` label (opt-in: "engineer-bot may take
# over this PR"). Two event paths, gated on event_name:
# 1. review_comment (created): a new inline comment. Gate on the COMMENTER —
# humans must be OWNER/MEMBER/COLLABORATOR (any GitHub user can comment on
# a public repo, and this job runs the build with secrets); bots are
# allowed ONLY by an explicit login allowlist (reviewer-bot, Copilot —
# author_association NONE by design, so we can't gate them on association).
# A bare `user.type == 'Bot'` check would let ANY installed App (Dependabot,
# a third-party App) trigger a privileged run, so we match by login prefix.
# engineer-bot's own comments + reviewer reconcile replies are filtered
# (loop prevention).
# 2. pull_request (labeled): the `engineer-bot` label was applied — wake
# catch-up mode. Gated on the label name AND a non-Bot human labeler
# (applying a label needs triage+; forecloses an auto-labeller looping).
if: >-
github.event.pull_request.head.repo.fork == false
&& github.event.pull_request.state == 'open'
&& (
(
github.event_name == 'pull_request_review_comment'
&& contains(github.event.pull_request.labels.*.name, 'engineer-bot')
&& !startsWith(github.event.comment.user.login, 'peco-engineer-bot')
&& !contains(github.event.comment.body, '<!-- pr-review-bot:v1 reconcile -->')
&& (
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|| (
github.event.comment.user.type == 'Bot'
&& (
startsWith(github.event.comment.user.login, 'peco-review-bot')
|| startsWith(github.event.comment.user.login, 'copilot-pull-request-reviewer')
|| startsWith(github.event.comment.user.login, 'Copilot')
)
)
)
)
|| (
github.event_name == 'pull_request'
&& github.event.action == 'labeled'
&& github.event.label.name == 'engineer-bot'
&& github.event.sender.type != 'Bot'
&& !startsWith(github.event.sender.login, 'peco-engineer-bot')
)
)
runs-on: [self-hosted, Linux, X64, peco-driver]
# 55 min: the App installation token expires at 60; PRs with many threads need headroom
# (the agent pushes fixes + posts replies at the END of the run).
timeout-minutes: 55
steps:
# Cheap pre-checkout filter — bail before minting a token / checkout if the
# trigger comment is our own marker-bearing loopback. Only meaningful on the
# review_comment path (the labeled path has no comment). followup.py re-checks
# per thread, so this is purely a cost short-circuit + defense in depth.
- name: Cheap pre-checkout filter
if: github.event_name == 'pull_request_review_comment'
id: filter
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
if printf '%s' "$COMMENT_BODY" | grep -q '<!-- engineer-bot-csharp-bugfix:v1'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "reason=trigger carries our marker (loop)" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Announce skip in step summary
if: steps.filter.outputs.skip == 'true'
run: |
{
echo "## Engineer Bot — Follow-up"
echo ""
echo "**Skipped:** ${{ steps.filter.outputs.reason }}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Mint engineer-bot App token
if: steps.filter.outputs.skip != 'true'
id: app-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
with:
app-id: ${{ secrets.ENGINEER_BOT_APP_ID }}
private-key: ${{ secrets.ENGINEER_BOT_PRIVATE_KEY }}
- name: Checkout PR branch
if: steps.filter.outputs.skip != 'true'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }} # so the fix push authenticates as the bot
# csharp/test depends on the csharp/hiveserver2 submodule (PUBLIC) to build;
# the followup re-runs dotnet test to verify fixes. Mirrors e2e-tests.yml.
submodules: recursive
# SECURITY — deliberate asymmetry with the reviewer workflows (they set
# persist-credentials: false, keeping their token out of .git/config so a
# prompt-injected PR can't read it). Here the App token is Contents+PR
# WRITE and the engine pushes the fix branch via git's persisted creds (same
# as the author phase), so persisting is inherent — pushing env-only would
# require engine support we don't rely on. The exposure (a writable token in
# .git/config while the agent runs over untrusted PR diff/comments) is
# mitigated by the engine's tool sandbox, NOT by hiding the token:
# - .bot/config.yaml denied_subpaths includes `.git`, so the engine's
# safe_path-enforced read_file/grep/glob cannot read .git/config;
# - bash_allowlist pins git to `diff HEAD` / `diff --cached` / `status`
# (no `cat`/`ls`, no `git config`), so no in-sandbox path dumps the token.
# If those sandbox guarantees ever loosen, revisit (push env-only at run end).
persist-credentials: true
- name: Configure bot git identity
if: steps.filter.outputs.skip != 'true'
env:
# NOTE: the noreply attribution format is <USER_ID>+<login>@users.noreply.github.com
# where USER_ID is the NUMERIC user id of the bot account (peco-engineer-bot[bot]),
# NOT the App's app-id (those are different numbers). Resolve it at runtime so
# commits link to the bot's profile and show as verified.
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
bot_login="peco-engineer-bot[bot]"
bot_id="$(gh api "/users/${bot_login}" --jq .id)"
git config user.name "${bot_login}"
git config user.email "${bot_id}+${bot_login}@users.noreply.github.com"
- name: Setup Python
if: steps.filter.outputs.skip != 'true'
shell: bash
# actions/setup-python can't provision on this self-hosted runner
# (no hosted tool-cache). The runner is provisioned with system
# python3.11; use it via a venv so pip installs stay isolated and
# `pip` / `python -m` resolve to 3.11 for the later steps.
run: |
python3.11 -m venv "$RUNNER_TEMP/bot-venv"
echo "$RUNNER_TEMP/bot-venv/bin" >> "$GITHUB_PATH"
- name: Setup .NET
if: steps.filter.outputs.skip != 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '8.0.x'
- name: Install bot engine (interim PAT git-install) + Claude SDK/CLI
if: steps.filter.outputs.skip != 'true'
env:
ENGINE_PAT: ${{ secrets.BOT_ENGINE_PAT }}
# SUPPLY-CHAIN PIN: the engine is pinned to an immutable commit SHA — never
# install from the force-pushable `@main` ref in a secret-bearing job. Bump
# ENGINE_REF to adopt engine changes (prefer a release tag once the engine
# repo cuts one). claude-agent-sdk / @anthropic-ai/claude-code are left
# unpinned to match the hub (databricks-driver-test); pin them to exact
# versions here if you want full reproducibility.
ENGINE_REF: 76a6e5902b217606b72a9476636df0d761260846
run: |
set -euo pipefail
: "${ENGINE_REF:?pin the engine to a tag/SHA before activation (refusing @main)}"
python -m pip install --upgrade pip
# Drop the `x-access-token:${ENGINE_PAT}@` prefix once the engine repo is public; ENGINE_REF stays pinned.
pip install "databricks-bot-engine @ git+https://x-access-token:${ENGINE_PAT}@github.com/databricks/databricks-bot-engine@${ENGINE_REF}"
pip install claude-agent-sdk
# Global npm dir (/usr/local/lib) is root-owned on the self-hosted
# runner; install to a per-job prefix the runner user owns and put
# it on PATH so later steps find the `claude` CLI the SDK shells to.
export NPM_CONFIG_PREFIX="$RUNNER_TEMP/npm-global"
mkdir -p "$NPM_CONFIG_PREFIX/bin"
npm install -g @anthropic-ai/claude-code
echo "$RUNNER_TEMP/npm-global/bin" >> "$GITHUB_PATH"
# WORKFLOW -> followup.py env contract: all required vars come from
# github.event.pull_request.* / github.repository (populated on BOTH event
# paths). TRIGGER_COMMENT_ID is the only comment-derived var — empty on the
# labeled path, which is intentional (followup.py treats it as logging-only;
# it always runs catch-up mode and re-scans the PR for unaddressed threads).
- name: Run engineer-bot follow-up
if: steps.filter.outputs.skip != 'true'
env:
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
TEST_REPO_ROOT: ${{ github.workspace }} # single-repo: agent edits THIS checkout
GH_TOKEN: ${{ steps.app-token.outputs.token }}
# Model serving on the same Databricks workspace as adbc's CI: DATABRICKS_HOST is
# the bare hostname hosting the databricks-claude-opus-4-8 serving endpoint;
# DATABRICKS_TOKEN authenticates it (Bearer). Both already exist as adbc CI secrets.
MODEL_ENDPOINT: https://${{ secrets.DATABRICKS_HOST }}/serving-endpoints/databricks-claude-opus-4-8/invocations
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
run: python -m databricks_bot_engine.engineer_bot.run --phase followup --bot .bot