Skip to content

fix(csharp): metadata(csharp): GetTables TABLE_TYPE/REMARKS/TABLE_NAME value normalization differs (Thrift REMARKS=UNKNOWN vs SEA "") (#527) #100

fix(csharp): metadata(csharp): GetTables TABLE_TYPE/REMARKS/TABLE_NAME value normalization differs (Thrift REMARKS=UNKNOWN vs SEA "") (#527)

fix(csharp): metadata(csharp): GetTables TABLE_TYPE/REMARKS/TABLE_NAME value normalization differs (Thrift REMARKS=UNKNOWN vs SEA "") (#527) #100

# 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