Agent Bricks target-state deployment fixes #15
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |