-
Notifications
You must be signed in to change notification settings - Fork 148
275 lines (265 loc) · 15.6 KB
/
Copy pathengineer-bot.yml
File metadata and controls
275 lines (265 loc) · 15.6 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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# Engineer Bot — author.
#
# A maintainer labels an ISSUE with `engineer-bot`; the bot addresses it and opens
# a PR. The author FLOW is chosen from the issue's TYPE (a GitHub org-level Issue
# Type): `Bug` ⇒ bug-fix (reproduce with a failing test, then fix); any other type
# / none ⇒ the repo's .bot/config.yaml `flow:` default (also bug-fix). Only bug-fix
# is selectable because this repo's .bot/prompts/engineer/ prompts are written
# exclusively for it; a task-flavored flow would need matching prompts first. Set
# the Type BEFORE `engineer-bot` — it's read live when the run starts. Driven by
# the selected flow + this repo's .bot/ (config + prompts).
#
# The engineer builds and runs THIS repo's tests, so it needs the connector's
# deps installed (via setup-poetry) in addition to the engine. Shared setup
# (mint tokens + Node + install the pinned engine, PAT-free) is factored into the
# local ./.github/actions/bot-prelude composite; this file owns checkout,
# setup-poetry, and the author/publish/comment tail. The engine is installed in
# REF mode pinned to a SHA and authenticated PAT-free by an engine-scoped App
# token (minted inside bot-prelude).
#
# SECURITY: the trigger is a label only maintainers can apply — issue BODIES are
# untrusted input, so the gate is who-can-label, not the content. The issue body
# is written to a file and inlined into the agent prompt; it never reaches a shell.
name: Engineer Bot
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to fix'
required: true
type: string
permissions:
contents: write # push the fix branch
pull-requests: write # open the fix PR
issues: write # comment back on the tracking issue
id-token: write # JFrog OIDC exchange for the engine/SDK/CLI install
jobs:
author:
# Manual dispatch, OR the `engineer-bot` label was just applied. Applying a
# label requires triage+ on the repo, so this is the maintainer-only gate.
# The `sender.type != 'Bot'` guard is defense-in-depth (matching the followup
# gate): a GitHub App with `issues: write` could apply the label without a
# human, so the triage+ argument doesn't fully cover automation.
if: >-
github.event_name == 'workflow_dispatch'
|| (github.event.label.name == 'engineer-bot'
&& github.event.sender.type != 'Bot')
environment: azure-prod # DATABRICKS_HOST / DATABRICKS_TOKEN live here
runs-on:
group: databricks-protected-runner-group
labels: [linux-ubuntu-latest]
timeout-minutes: 45
concurrency:
# One author run per issue; a re-label while a run is in flight queues.
group: engineer-bot-issue-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: false
steps:
# Checkout FIRST (remote action) so the local `./` composites resolve.
# publish sets its own authenticated push remote for the fix branch, so
# persist-credentials:false is fine.
- name: Checkout (default branch)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
# Python + connector deps via the repo's OWN setup-poetry action: it
# configures poetry against the internal JFrog mirror (the protected runner
# is egress-blocked from pypi.org), runs `poetry lock` (tolerating a stale
# lock), and installs the connector + its deps. This is the maintained,
# egress-safe way this repo installs itself — the agent then runs
# `poetry run python -m pytest tests/unit` to self-verify its red→green fix.
# (The engineer builds/runs the connector, so unlike the reviewer it needs
# the deps, not just an interpreter.)
- name: Setup Poetry + connector deps (for pytest self-verify)
uses: ./.github/actions/setup-poetry
with:
python-version: '3.11'
# Match code-coverage.yml's e2e job: install optional extras (notably
# pyarrow, declared optional=true behind the `pyarrow` extra) so the
# agent's REQUIRED live E2E repro test runs against the same runtime as
# the e2e suite it mirrors. Without this, arrow-touching e2e tests
# ModuleNotFoundError/skip instead of failing red-for-the-right-reason.
install-args: "--all-extras"
# setup-poetry runs `poetry lock` (to reconcile the lock with the internal
# JFrog source it injects), which REWRITES tracked poetry.lock / pyproject.toml
# in the working tree. The venv is already built, so the tree no longer needs
# that churn — revert it, else publish's leftover safety-net sees these
# bot-untouched dirty files and refuses to open the PR (they aren't in the
# agent's touched_files). Keep ONLY the agent's edits in the tree.
- name: Revert poetry lock/pyproject churn (keep only the agent's edits)
# No `|| true`: `git checkout -- <tracked-file>` exits 0 for both the
# clean and the reverted-churn cases, so a non-zero exit means the revert
# genuinely failed (e.g. the files went untracked/missing) and the dirty
# tree would otherwise resurface as a confusing publish-time error. Fail
# loudly here instead of masking it.
run: git checkout -- poetry.lock pyproject.toml
# Shared prelude: mint the engineer-bot token (pushes the fix branch,
# comments) + the engine-scoped token, set up Node, install the pinned
# engine (PAT-free). `token` output is used by the steps below.
- name: Bot prelude (tokens + Node + engine install)
id: prelude
uses: ./.github/actions/bot-prelude
with:
app-id: ${{ secrets.ENGINEER_BOT_APP_ID }}
private-key: ${{ secrets.ENGINEER_BOT_APP_PRIVATE_KEY }}
# Engine pin lives in ONE place: bot-prelude's `engine-ref` default.
# Bump it there to move all four bots in lockstep; override here only
# to run this workflow against a different engine commit.
- name: Resolve issue + gather context
id: ctx
# SECURITY: the issue number is validated as digits-only before use; the
# issue body + title are UNTRUSTED. The body goes to a file (never a
# shell or env interpolation); the title is exported through $GITHUB_ENV
# with a random heredoc delimiter (a title can contain newlines, or a
# chosen delimiter, which would otherwise inject extra env vars). The
# number/url are safe shapes and exported directly.
env:
GH_TOKEN: ${{ steps.prelude.outputs.token }}
EVENT_NAME: ${{ github.event_name }}
INPUT_ISSUE: ${{ inputs.issue_number }}
EVENT_ISSUE: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then RAW="$INPUT_ISSUE"; else RAW="$EVENT_ISSUE"; fi
[[ "$RAW" =~ ^[0-9]+$ ]] || { echo "::error::Invalid issue number '$RAW'"; exit 1; }
# `gh issue view --json` does NOT expose the issue TYPE field, so fetch
# via the REST API (which carries `.type.name`). Body/title/url still
# come from the same JSON. Issue Types are an ORG-level feature; a repo
# without them configured returns `.type == null` (⇒ no --flow ⇒ the
# engine uses the .bot/config.yaml `flow:` default).
gh api "repos/$REPO/issues/$RAW" > "$RUNNER_TEMP/issue.json"
# The REST issues endpoint resolves PR numbers too (PRs are issues in
# the REST model), unlike `gh issue view <pr-number>` which errored.
# Keep rejecting PR numbers cleanly: real issues have `.pull_request == null`.
[ "$(jq -r '.pull_request // "null"' "$RUNNER_TEMP/issue.json")" = "null" ] \
|| { echo "::error::#$RAW is a pull request, not an issue"; exit 1; }
jq -r '.body // ""' "$RUNNER_TEMP/issue.json" > "$RUNNER_TEMP/issue_body.txt"
TITLE="$(jq -r '.title // ""' "$RUNNER_TEMP/issue.json")"
URL="$(jq -r '.html_url // ""' "$RUNNER_TEMP/issue.json")"
{
echo "ISSUE_NUMBER=$RAW"
echo "ISSUE_URL=$URL"
} >> "$GITHUB_ENV"
echo "issue_number=$RAW" >> "$GITHUB_OUTPUT"
# Derive the author flow from the issue's TYPE (read LIVE here, so a type
# set AFTER this fetch is not seen — set the type BEFORE `engineer-bot`).
# The type→flow MAPPING is pinned (not "use the type name as the flow")
# so an arbitrary custom org type can never select an engine flow:
# Bug ⇒ bug-fix (red→green TDD)
# Only `bug-fix` is mapped: this repo's .bot/prompts/engineer/{system,user}.md
# are written EXCLUSIVELY for the bug-fix flow ("reproduce the bug with a
# failing test, then fix"). Mapping Task/Feature ⇒ task would run the
# engine's single-pass `task` flow under a prompt that mandates red→green
# bug reproduction — a flow/prompt contradiction. Until flow-aware prompts
# exist, every run uses bug-fix.
# Any other type or no type ⇒ emit nothing ⇒ the author step omits
# --flow ⇒ the engine uses .bot/config.yaml `flow:` default (bug-fix).
TYPE="$(jq -r '.type.name // ""' "$RUNNER_TEMP/issue.json")"
case "$TYPE" in
Bug) echo "flow=bug-fix" >> "$GITHUB_OUTPUT" ;;
esac
DELIM="GHEOF_$(date +%s%N)${RANDOM}"
if printf '%s' "$TITLE" | grep -qF "$DELIM"; then
echo "::error::title delimiter collision — refusing to write \$GITHUB_ENV"; exit 1
fi
{ echo "ISSUE_TITLE<<$DELIM"; printf '%s\n' "$TITLE"; echo "$DELIM"; } >> "$GITHUB_ENV"
- name: Run author
id: author
# Run from RUNNER_TEMP so the engine-rendered prompt's context file
# (issue_body.txt, resolved against cwd) is read from there and the
# checkout stays clean — publish's leftover check fails on any untracked
# path in the repo. REPO_ROOT points the agent's working tree (and
# the .bot/ lookup) at the checkout; ISSUE_NUMBER/TITLE/URL come from
# $GITHUB_ENV (the bot config's author.env_tokens).
#
# The author FLOW is chosen from the issue TYPE by the ctx step above
# (Bug ⇒ bug-fix; any other/none ⇒ config default). We pass --flow ONLY
# when that step derived one: an empty --flow would be rejected by the CLI's
# `choices`, and omitting it lets the engine fall back to the
# .bot/config.yaml `flow:` default (no/other type, or type-set-late case).
working-directory: ${{ runner.temp }}
env:
MODEL_ENDPOINT: https://${{ secrets.DATABRICKS_HOST }}/serving-endpoints/databricks-claude-opus-4-8/invocations
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
# Live-warehouse connection env for the agent's REQUIRED E2E repro test
# (see .bot/prompts/engineer/system.md — the bug-fix flow reproduces the
# bug via a tests/e2e test against a real warehouse; a mocked unit test
# alone cannot prove the live server behaves correctly). Mirrors the
# e2e job in code-coverage.yml; the job already runs in `environment:
# azure-prod`, so these secrets are in scope.
DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }}
DATABRICKS_CATALOG: peco
DATABRICKS_USER: ${{ secrets.TEST_PECO_SP_ID }}
REPO_ROOT: ${{ github.workspace }}
RUNNER_TEMP: ${{ runner.temp }}
FLOW: ${{ steps.ctx.outputs.flow }}
run: |
args=(
--phase author
--system-prompt "$GITHUB_WORKSPACE/.bot/prompts/engineer/system.md"
--user-prompt "$GITHUB_WORKSPACE/.bot/prompts/engineer/user.md"
)
[ -n "$FLOW" ] && args+=(--flow "$FLOW")
python -m databricks_bot_engine.engineer_bot.run "${args[@]}"
- name: Open / update fix PR
id: publish
if: steps.author.outputs.outcome == 'success'
# publish stages exactly the agent-touched files, commits as the bot,
# pushes the fix branch, and opens/updates the PR. The push authenticates
# via the App token embedded in the remote URL (checkout ran with
# persist-credentials: false, so this is the only push credential).
env:
GH_TOKEN: ${{ steps.prelude.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
REPO_ROOT: ${{ github.workspace }}
RUNNER_TEMP: ${{ runner.temp }}
ENGINEER_BOT_APP_ID: ${{ secrets.ENGINEER_BOT_APP_ID }}
TOUCHED_FILES: ${{ steps.author.outputs.touched_files }}
TOUCHED_COUNT: ${{ steps.author.outputs.touched_count }}
SUMMARY: ${{ steps.author.outputs.summary }}
RED_GREEN_TESTS: ${{ steps.author.outputs.red_green_tests }}
PLAN: ${{ steps.author.outputs.plan }}
OUT_OF_SCOPE: ${{ steps.author.outputs.out_of_scope }}
# ISSUE_NUMBER / ISSUE_TITLE / ISSUE_URL come from $GITHUB_ENV.
run: |
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
python -m databricks_bot_engine.engineer_bot.publish --repo-dir "$GITHUB_WORKSPACE"
- name: Comment outcome on issue
# Always close the loop on the tracking issue, whatever happened. The
# agent SUMMARY / reason is LLM-generated — write it to a file and pass
# via --body-file so nothing reaches a shell.
if: always() && steps.ctx.outputs.issue_number != ''
env:
GH_TOKEN: ${{ steps.prelude.outputs.token }}
REPO: ${{ github.repository }}
ISSUE: ${{ steps.ctx.outputs.issue_number }}
OUTCOME: ${{ steps.author.outputs.outcome }}
# `coverage_pr_url` is the engine publish module's PR-URL output key for
# ALL flows (bug-fix included), not just coverage — the name is legacy.
# engineer_bot.publish emits this key regardless of flow, so it is correct here.
PR_URL: ${{ steps.publish.outputs.coverage_pr_url }}
REASON: ${{ steps.author.outputs.reason }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
BODY="$RUNNER_TEMP/issue_comment.md"
# $OUTCOME is the engine's emitted outcome (steps.author.outputs.outcome),
# NOT the agent's internal enum. The engine maps the agent's
# `no_change_needed` (what the prompt asks the agent to report) to the
# `$GITHUB_OUTPUT` value `no_change` (run_author.py). So `no_change)` below
# is the correct arm — do not "fix" it to `no_change_needed)`.
case "$OUTCOME" in
success)
if [ -n "$PR_URL" ]; then
printf '🤖 engineer-bot opened a fix PR: %s\n\nReview before merge.\n' "$PR_URL" > "$BODY"
else
printf '🤖 engineer-bot produced a fix but could **not** open the PR (push or PR creation failed). See the [workflow run](%s) for details.\n' "$RUN_URL" > "$BODY"
fi ;;
no_change)
{ printf '🤖 engineer-bot looked into this but made **no change** — the behaviour appears already correct.\n\n'; printf '> %s\n' "$REASON"; } > "$BODY" ;;
*)
{ printf '🤖 engineer-bot could **not** complete an automated fix. See the [workflow run](%s) for details.\n' "$RUN_URL"; } > "$BODY" ;;
esac
gh issue comment "$ISSUE" --repo "$REPO" --body-file "$BODY"