Confluent logoAutomation

Failing the Build Before the Consumer Breaks: A CI Drift Gate for Confluent Schema Registry

The Schema Registry's compatibility checks are necessary and not sufficient. They guarantee a new schema version can be deserialized by existing consumers — wire compatibility. They say nothing about whether `amount` is still the governed Double your downstream jobs assume, whether an enum quietly grew a symbol nobody reviewed, or whether a topic your reports depend on disappeared from the registry altogether. Those are questions about *meaning*, and meaning drift in streaming breaks things at runtime, in production, at consumer speed. This article wires the CoreModels schema audit into CI as a merge gate for schema changes, then completes the loop with the status badge, the rolling history, one-call re-audits, and the scheduled heartbeat.

Failing the Build Before the Consumer Breaks: A CI Drift Gate for Confluent Schema Registry

The Schema Registry's compatibility checks are necessary and not sufficient. They guarantee a new schema version can be deserialized by existing consumers — wire compatibility. They say nothing about whether amount is still the governed Double your downstream jobs assume, whether an enum quietly grew a symbol nobody reviewed, or whether a topic your reports depend on disappeared from the registry altogether. Those are questions about meaning, and meaning drift in streaming breaks things at runtime, in production, at consumer speed. This article wires the CoreModels schema audit into CI as a merge gate for schema changes, then completes the loop with the status badge, the rolling history, one-call re-audits, and the scheduled heartbeat.

The contract: one route, one number

The machine-to-machine surface is built for pipelines — it accepts user API keys rather than login tokens:

POST https://coremodels.example.com/v1/{PROJECT_ID}/integrations/confluent/audit

The route runs at Viewer role and is read-only, so the API key you give CI needs no write access of any kind. The response comes wrapped in the standard ApiResponse envelope, meaning the report lives under data.*, and the gate semantics are one line: data.errorCount > 0 means the change violates governed meaning — fail the build. The Error-severity drift findings are the ones that trip it: dataset-removed, field-removed, field-type-drift, and enum-narrowed (an accepted value removed while still governed). enum-widened and enum-constraint-removed surface as Warnings — visible in every report, fatal only if you gate harder. Info-tier findings like fields-no-doc never fail a build; they inform it.

Producing subjects.json in CI

The audit needs the connector's one artifact: subjects, a JSON array of GET /subjects/{subject}/versions/latest responses. In CI there are two workable sources, and both keep registry credentials out of CoreModels entirely:

  1. Export from a staging registry in a prior step — register the PR's schemas against staging first (most teams already do), then run the one-loop export against it with the registry credentials your pipeline already holds.
  2. Build the artifact from the PR itself — if .avsc files are authored in the repo, assemble the rows ({subject, version, id, schemaType, schema}) from the files without touching any registry. The schema value is the schema document as a string, and only subject and schema are mandatory per row — schemaType defaults to Avro, while version and id are optional registry bookkeeping:
# Assemble subjects.json from the repo's .avsc files (TopicName strategy, value subjects):
for f in schemas/*.avsc; do
  jq -n --arg subject "$(basename "$f" .avsc)-value" --rawfile schema "$f" \
    '{subject: $subject, schemaType: "AVRO", schema: $schema}'
done | jq -s '.' > subjects.json

One rule keeps this honest: the subject values must be the same subject names the registry serves (here, file names matching topics under the TopicName strategy) — a row under an unknown subject audits as ungoverned coverage, not as drift against the Type it was meant to check.

Either way, the file that reaches CoreModels is one the registry API (or your repo) already produces.

The gate

Repository secrets: COREMODELS_API_KEY (a Viewer-capable user API key). Repository variables: COREMODELS_API_URL, COREMODELS_PROJECT_ID. The step:

- 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 subjects subjects.json \
      '{artifacts: {subjects: $subjects}, recordHistory: true}' > audit-request.json
    curl -sS -o audit-response.json -X POST \
      "$COREMODELS_API_URL/v1/$COREMODELS_PROJECT_ID/integrations/confluent/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

The last three lines are three distinct checks, and the order matters. .success must be true first — false means the audit could not run at all (unparseable export, unknown vendor), which is a broken pipeline rather than a clean result, and it must fail loudly instead of gating on a number that never got computed. Then data.markdown — the PR-comment-ready human report — goes into the job summary, so a red build explains itself: which subject, which field, governed type versus artifact type. Only then does the actual gate run against data.errorCount.

Teams gating dbt with our GitHub Action will recognize the endpoint and envelope; the composite action's inputs are manifest-shaped, so for Confluent the curl-and-jq form above is the recipe. Same route, same semantics.

Why recordHistory: true belongs in the gate

The audit verb is read-only by default; appending the run to the project's rolling audit history is opt-in bookkeeping, and in CI you want it on. Each gated run lands in the trail with trigger ci, its three counts, its per-code totals, and the artifact content fingerprint. That turns your gate from a point-in-time check into a timeline: when field-type-drift first appeared, which runs saw an identical export (identical fingerprints), and whether a warning trend is growing or shrinking. Read it back from the interactive surface with a login token:

curl -sS "https://coremodels.example.com/graph/integrations/confluent/history/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN"

Runs arrive newest first, each carrying at, trigger (audit, reaudit, ci, or scheduled), the counts, the codes map, and the fingerprint.

The badge

The latest recorded run also drives an SVG status badge, served on both surfaces. The machine route is README-friendly with an API key:

GET https://coremodels.example.com/v1/{PROJECT_ID}/integrations/confluent/badge

It returns image/svg+xml, shields-style, labeled confluent audit: green for a clean run, yellow for warnings only, red for errors, gray when no runs are recorded yet. Gray is a feature — a project that has never recorded an audit should look unaudited, not clean. Because the badge reads recorded runs, a gate that sets recordHistory: true keeps the badge current for free.

Re-audit: the direction CI cannot see

CI guards one direction of drift — the registry moving away from the governed model. The other direction happens inside governance: someone tightens a taxonomy, retypes an element, or removes a Type, and no schema PR exists to trigger the gate. That is what re-audit is for. It replays the audit engine over the registry snapshot stored at import time against the current governed model, needs no artifacts, and always records its run:

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

Wire it into the same place governance changes happen — a webhook on model edits, a nightly job, or simply team habit after a modeling session. Note that re-audit and history live on the interactive surface (login token), while the v1 API-key surface carries audit and badge; plan your automation's credentials accordingly.

The heartbeat

Between schema PRs and governance edits, nothing triggers either check — and silence is not evidence of health. For that gap, deployments can enable the scheduled re-audit heartbeat (the Integrations:ScheduledReaudit configuration, off by default): the server periodically re-audits opted-in projects against their stored snapshots and records the runs into the history. The badge then decays honestly — if the governed model and the last-known estate diverge while everyone is busy shipping, the trail shows it without anyone remembering to ask.

One honest limit

Re-audit and the heartbeat depend on the snapshot stored at import. Very large registries whose encoded snapshot exceeds the storage cap (roughly 1.5 MB) report snapshotStored: false at import, with a lossiness record saying so. Fresh-artifact audits — including the CI gate above — work regardless; only the stored-snapshot verbs are unavailable. If your registry is at that scale, run the gate on every PR and schedule a periodic fresh export-and-audit instead of the heartbeat.

Gate on errorCount, record every run, embed the badge, re-audit on model change, heartbeat in between: five small pieces, one closed loop, and schema drift stops being something you discover from a consumer stack trace. Setup details and the export recipe are in the Confluent Schema Registry quickstart in the CoreModels docs.