Skip to content

Agent Bricks target-state deployment fixes #15

Agent Bricks target-state deployment fixes

Agent Bricks target-state deployment fixes #15

Workflow file for this run

name: deploy
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Databricks CLI
run: curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh
- name: Validate bundle (demo)
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
DOCINTEL_WAREHOUSE_ID: ${{ vars.DOCINTEL_WAREHOUSE_ID }}
run: databricks bundle validate --strict -t demo --var "warehouse_id=$DOCINTEL_WAREHOUSE_ID"
deploy-demo:
# CI assumes steady-state: the first-ever bring-up of a workspace must be
# done locally via `./scripts/bootstrap-demo.sh`, which handles the
# foundation/consumers staging and waits for Lakebase AVAILABLE. After
# that initial bring-up, every push to main runs a full bundle deploy
# against the now-existing resources — no temp-rename trick (DAB would
# plan to DELETE any resource that disappears from config).
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
DOCINTEL_CATALOG: ${{ vars.DOCINTEL_CATALOG }}
DOCINTEL_SCHEMA: ${{ vars.DOCINTEL_SCHEMA }}
DOCINTEL_WAREHOUSE_ID: ${{ vars.DOCINTEL_WAREHOUSE_ID }}
PYTHONPATH: ${{ github.workspace }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Databricks CLI
run: curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh
- name: Install Python deps
run: pip install -r agent/requirements.txt -r evals/requirements.txt
- name: Resolve steady-state Agent Bricks endpoint
run: |
endpoint="$(./scripts/resolve-agent-endpoint.sh demo 2>/dev/null || true)"
if [ -z "$endpoint" ]; then
echo "::error::Agent Bricks supervisor for demo is missing. Run ./scripts/bootstrap-demo.sh once for first workspace bring-up, then rerun CI."
exit 1
fi
if ! databricks api get "/api/2.0/serving-endpoints/${endpoint}" >/dev/null 2>&1; then
echo "::error::Agent Bricks supervisor record points to endpoint ${endpoint}, but that serving endpoint is missing or not listable. Run ./scripts/bootstrap-demo.sh locally to repair workspace drift."
exit 1
fi
echo "AGENT_ENDPOINT_NAME=${endpoint}" >> "$GITHUB_ENV"
- name: Deploy bundle (full — consumers already exist in steady-state)
# Pin warehouse_id so the dashboard and Agent Bricks bootstrap match
# wait_for_kpis. Without --var, the bundle
# falls back to its `lookup: warehouse: Serverless Starter Warehouse`
# default and silently picks a different ID.
run: databricks bundle deploy -t demo --var "warehouse_id=$DOCINTEL_WAREHOUSE_ID" --var "agent_endpoint_name=$AGENT_ENDPOINT_NAME"
- name: Wait for Lakebase instance to be AVAILABLE
# Lakebase already exists in steady-state but a config change can
# transition it back through provisioning; the catalog/app bindings
# need it AVAILABLE before the next bundle run touches them.
run: |
python - <<'PY'
import json, os, sys, time, subprocess
name = os.environ.get('LAKEBASE_NAME') or 'docintel-demo-state-v1'
deadline = time.time() + 600
while True:
out = subprocess.run(
['databricks', 'api', 'get', '/api/2.0/database/instances', '--output', 'json'],
capture_output=True,
text=True,
)
try:
d = json.loads(out.stdout)
except Exception:
d = {}
state = next((i.get('state') for i in d.get('database_instances', []) if i.get('name') == name), 'UNKNOWN')
print(f'lakebase state: {state}')
if state == 'AVAILABLE':
sys.exit(0)
if time.time() >= deadline:
sys.exit(f'Lakebase {name} did not reach AVAILABLE within 600s (state={state})')
time.sleep(15)
PY
env:
LAKEBASE_NAME: ${{ vars.DOCINTEL_LAKEBASE_NAME || 'docintel-demo-state-v1' }}
- name: Refresh data and Agent Bricks configuration
run: |
for f in samples/*_10K_*.pdf; do
databricks fs cp "$f" \
"dbfs:/Volumes/${DOCINTEL_CATALOG}/${DOCINTEL_SCHEMA}/raw_filings/" \
--overwrite
done
databricks bundle run -t demo --var "warehouse_id=$DOCINTEL_WAREHOUSE_ID" doc_intel_pipeline
python scripts/wait_for_kpis.py --min-rows 3 --timeout 900
databricks bundle run -t demo --var "warehouse_id=$DOCINTEL_WAREHOUSE_ID" --var "agent_endpoint_name=$AGENT_ENDPOINT_NAME" index_refresh
python -m agent.document_intelligence_agent \
--target demo \
--catalog "$DOCINTEL_CATALOG" \
--schema "$DOCINTEL_SCHEMA" \
--warehouse-id "$DOCINTEL_WAREHOUSE_ID"
- name: Apply UC grants (catalog + schema; not DAB-supported)
# UC requires the full chain: USE_CATALOG → USE_SCHEMA → SELECT/EXECUTE.
# Defaults to "account users" so CI does not silently skip when the
# GitHub variable is unset.
env:
ANALYST_GROUP: ${{ vars.DOCINTEL_ANALYST_GROUP || 'account users' }}
run: |
databricks api patch \
"/api/2.1/unity-catalog/permissions/CATALOG/${DOCINTEL_CATALOG}" \
--json "{\"changes\":[{\"principal\":\"${ANALYST_GROUP}\",\"add\":[\"USE_CATALOG\"]}]}" || true
databricks api patch \
"/api/2.1/unity-catalog/permissions/SCHEMA/${DOCINTEL_CATALOG}.${DOCINTEL_SCHEMA}" \
--json "{\"changes\":[{\"principal\":\"${ANALYST_GROUP}\",\"add\":[\"USE_SCHEMA\",\"SELECT\",\"EXECUTE\"]}]}" || true
- name: Apply app config + restart
# Databricks Apps deploy docs:
# https://docs.databricks.com/aws/en/dev-tools/databricks-apps/deploy
# `bundle deploy` alone uploads code but doesn't apply config/restart.
run: databricks bundle run -t demo --var "warehouse_id=$DOCINTEL_WAREHOUSE_ID" --var "agent_endpoint_name=$AGENT_ENDPOINT_NAME" analyst_app
- name: Verify app auth mode and endpoint grants
run: |
databricks apps get doc-intel-analyst-demo --output json > /tmp/app.json
app_obo_required="$(python -c "import yaml; d=yaml.safe_load(open('databricks.yml')); default=d.get('variables',{}).get('app_obo_required',{}).get('default','true'); value=d.get('targets',{}).get('demo',{}).get('variables',{}).get('app_obo_required', default); print(str(value).lower())")"
lakebase_name="$(python -c "import yaml; d=yaml.safe_load(open('databricks.yml')); print(d.get('targets',{}).get('demo',{}).get('variables',{}).get('lakebase_instance','docintel-demo-state-v1'))")"
python -c "import json; app=json.load(open('/tmp/app.json')); vals=[str(app.get(k)) for k in ('service_principal_client_id','service_principal_name','service_principal_id') if app.get(k) is not None]; print('\n'.join(dict.fromkeys(v for v in vals if v)))" > /tmp/app-sp-candidates.txt
db_granted=0
while IFS= read -r principal; do
grant_json="$(python -c "import json, sys; print(json.dumps({'access_control_list':[{'service_principal_name':sys.argv[1],'permission_level':'CAN_USE'}]}))" "$principal")"
if databricks permissions update database-instances "$lakebase_name" --json "$grant_json"; then
db_granted=1
break
fi
done < /tmp/app-sp-candidates.txt
test "$db_granted" = "1"
if [ "$app_obo_required" = "true" ]; then
# `bundle run` may wipe user_api_scopes (documented destructive-update
# behavior). Fail loudly if required user scopes are missing.
python -c "import json; app=json.load(open('/tmp/app.json')); scopes=set(app.get('user_api_scopes') or []); required={'serving.serving-endpoints','sql'}; missing=required-scopes; assert not missing, f'OBO scopes missing: {sorted(missing)} (got {sorted(scopes)})'"
else
python -c "import json; app=json.load(open('/tmp/app.json')); scopes=app.get('user_api_scopes'); assert not scopes, f'demo App-SP mode expected no user_api_scopes, got {scopes}'"
endpoint_id="$(databricks serving-endpoints get "$AGENT_ENDPOINT_NAME" --output json | python -c "import json, sys; e=json.load(sys.stdin); print(e.get('id') or e.get('name'))")"
granted=0
while IFS= read -r principal; do
grant_json="$(python -c "import json, sys; print(json.dumps({'access_control_list':[{'service_principal_name':sys.argv[1],'permission_level':'CAN_QUERY'}]}))" "$principal")"
if databricks permissions update serving-endpoints "$endpoint_id" --json "$grant_json"; then
granted=1
break
fi
done < /tmp/app-sp-candidates.txt
test "$granted" = "1"
fi
- name: CLEARS evaluation gate
run: python evals/clears_eval.py --endpoint "$AGENT_ENDPOINT_NAME" --dataset evals/dataset.jsonl