Databricks logoAutomation

Fail the Build, Not the Dashboard: A CI Drift Gate for Databricks Unity Catalog

Somewhere in your lakehouse, a notebook is about to rebuild a table with a widened column type, and nothing will fail until a dashboard does — days later, in front of the wrong audience. The cheapest place to catch that is a pull request. This article turns the CoreModels schema audit into a merge-blocking CI gate for a Unity Catalog estate, then builds out the rest of the operational loop most drift setups forget: the status badge, the rolling history, the one-call re-audit for when the governed model itself changes, and the scheduled heartbeat that keeps watch between releases.

Fail the Build, Not the Dashboard: A CI Drift Gate for Databricks Unity Catalog

Somewhere in your lakehouse, a notebook is about to rebuild a table with a widened column type, and nothing will fail until a dashboard does — days later, in front of the wrong audience. The cheapest place to catch that is a pull request. This article turns the CoreModels schema audit into a merge-blocking CI gate for a Unity Catalog estate, then builds out the rest of the operational loop most drift setups forget: the status badge, the rolling history, the one-call re-audit for when the governed model itself changes, and the scheduled heartbeat that keeps watch between releases.

The endpoint CI calls

CI should never hold an interactive login, so the gate targets the machine-to-machine surface, which accepts CoreModels user API keys:

POST https://coremodels.example.com/v1/$PROJECT_ID/integrations/databricks/audit

The audit runs at Viewer role and never writes, so the key in your CI secrets needs no write access to anything. The response comes wrapped in the standard ApiResponse envelope: the report lives under data, and the gate keys on one number, data.errorCount.

The semantics decide when builds go red, so they are worth being precise about. Errors mean the extract contradicts governed meaning — a governed field removed (field-removed), a column retyped against the model (field-type-drift), an enum narrowed (enum-narrowed). Warnings are the connector's best-practice rules; for Databricks that is key-column-undeclared, a key-shaped column with no declared informational constraint. Info findings, such as table-no-comment, are documentation nudges. Failing on errorCount > 0 and letting warnings through keeps the signal crisp: red means meaning was violated, not that someone forgot a comment. Gate harder only once your estate is clean enough that warnings are actionable.

Getting the extract into CI

The audit needs one artifact: information_schema, the JSON rows of the documented Unity Catalog query. Two patterns work.

  • Commit the extract with schema-change PRs. The PR then carries both the intended change and the evidence, and the audit runs on exactly what is proposed.
  • Re-extract in a prior CI step against a staging catalog — for instance with databricks-sql-cli — so the pipeline always audits current reality.

Either way, the audit step itself needs nothing but curl and jq.

The gate, in full

name: coremodels-schema-audit
on:
  pull_request:
    paths: ["schemas/**"]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # A prior step places information_schema.json in the workspace —
      # committed with the PR, or re-extracted against a staging catalog.

      - name: CoreModels Schema Audit
        env:
          COREMODELS_API_URL: ${{ vars.COREMODELS_API_URL }}
          COREMODELS_API_KEY: ${{ secrets.COREMODELS_API_KEY }}
          COREMODELS_PROJECT_ID: ${{ vars.COREMODELS_PROJECT_ID }}
        run: |
          set -euo pipefail
          jq -n --rawfile info information_schema.json \
            '{artifacts: {information_schema: $info}, recordHistory: true}' > audit-request.json
          curl -sS -o audit-response.json -X POST \
            "$COREMODELS_API_URL/v1/$COREMODELS_PROJECT_ID/integrations/databricks/audit" \
            -H "Authorization: Bearer $COREMODELS_API_KEY" -H "Content-Type: application/json" \
            --data-binary @audit-request.json
          test "$(jq -r '.success' audit-response.json)" = "true"
          jq -r '.data.markdown' audit-response.json >> "$GITHUB_STEP_SUMMARY"
          test "$(jq -r '.data.errorCount' audit-response.json)" -eq 0

Reading it line by line: jq -n --rawfile wraps the extract as a string value inside the request body, escaping included. The first test distinguishes "the audit could not run" from "the audit ran and found problems" — a transport failure or unusable payload should fail the build loudly, not masquerade as a clean report. The markdown field goes to the job summary, so reviewers see the human-readable report — findings grouped by section, coded and subject-lined — without leaving the PR. The final test is the gate itself: any error finding fails the step.

recordHistory: true matters more than it looks. Every gated run is appended to the project's rolling audit trail with trigger ci, which is what makes the next two pieces work.

One note if you already use our composite GitHub Action elsewhere: its artifact inputs are manifest-shaped, built for build-artifact vendors. For a Unity Catalog extract the curl-and-jq step above is the recipe — same endpoint, same envelope, same gate semantics.

The badge

The same machine-to-machine surface serves an SVG badge rendered from the latest recorded run:

curl -sS -H "Authorization: Bearer $COREMODELS_API_KEY" \
  "https://coremodels.example.com/v1/$PROJECT_ID/integrations/databricks/badge" \
  -o databricks-audit.svg

Green means the last recorded run was clean, yellow means warnings only, red means errors, gray means nothing recorded yet. The route authenticates with the API key, so for a public README the practical pattern is the one above: fetch the badge in a scheduled job and publish the SVG wherever your docs live. Because the badge reflects recorded runs, a gate that records on every PR keeps it truthful for free.

The rolling history

The badge answers "now"; the history answers "lately":

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/databricks/history/$PROJECT_ID"
{ "success": true, "vendor": "databricks",
  "projects": [ { "projectName": "main", "runs": [
    { "at": "2026-08-03T09:12:44.6183920+00:00", "trigger": "ci",
      "errorCount": 0, "warningCount": 2, "infoCount": 3,
      "codes": { "key-column-undeclared": 2, "table-no-comment": 3 },
      "fingerprint": "9f2c4a1b0e77d3aa" },
    { "at": "2026-08-01T16:40:02.1170455+00:00", "trigger": "reaudit",
      "errorCount": 1, "warningCount": 2, "infoCount": 3,
      "codes": { "field-type-drift": 1, "key-column-undeclared": 2, "table-no-comment": 3 },
      "fingerprint": "9f2c4a1b0e77d3aa" } ] } ] }

Runs come back newest first, and trigger is one of four lowercase values: audit, ci, reaudit, scheduled. The trail is a rolling window — the most recent 50 runs per estate — so treat it as a drift signal, not an archive: if you need audit records kept forever, store the full audit response your gate already downloaded as a build artifact. Two further details make this trail genuinely useful. The fingerprint is a content hash of the audited artifacts: identical fingerprints with different finding counts mean the governed model moved, not the estate — exactly the situation in the sample above, where a field-type-drift error appeared and then cleared between two runs of the same extract. And codes gives you drift trends per rule without parsing findings. Note the surface boundary: history (and re-audit, next) live on the interactive surface with a login token; the API-key surface carries audit and badge.

Re-audit: the gate for the other direction

CI catches the estate drifting from the model. But models change too — someone tightens a type or removes a governed field in CoreModels. Re-audit checks the current governed model against the estate snapshot stored at import time, no fresh extract required:

curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "https://coremodels.example.com/graph/integrations/databricks/reaudit/$PROJECT_ID" \
  -d '{}'

The run is always recorded in the history (trigger reaudit), so governed-side changes leave the same audit trail estate-side changes do. The dependency to know about: re-audit needs the snapshot stored at import. Very large estates can exceed the snapshot storage cap (roughly 1.5 MB encoded), in which case import reports snapshotStored: false and re-audit has nothing to run against — fresh-artifact audits still work as usual.

The heartbeat

PR gates only fire when someone opens a PR. For the quiet weeks, CoreModels deployments have a scheduled server-side re-audit — a background worker, off by default and enabled by the operator through deployment configuration, per opted-in project — that periodically re-audits those projects against their stored snapshots and records the runs into the same history under trigger scheduled. No credentials, no vendor API calls, no CI minutes: it replays the last-known estate against the current model on a schedule, and a project with no stored snapshot is simply skipped. Combined with the PR gate you get both directions covered continuously: estate-side drift is caught at merge time, governed-side drift is caught by the heartbeat, and both write to one trail behind one badge.

The loop, assembled

One secret (a Viewer-scoped API key), one committed or re-extracted JSON file, and a five-line audit step buy you: a merge gate on governed meaning, a PR-visible report, a truthful badge, a queryable drift history with content fingerprints, and a scheduled heartbeat for the weeks nobody touches the schema. None of it holds a Databricks credential.

The extraction queries and the interactive-surface equivalents of these calls are in the Databricks quickstart in the CoreModels documentation.