Back to Blog
Stop Deploying Broken Contracts

Stop Deploying Broken Contracts

·15 min read
Software EngineeringOpen SourceArtificial IntelligenceDevOpsCI/CD

Team A updates a protobuf definition. Team B's copy goes stale. Nobody notices until a service starts returning 500s in production and three engineers spend a Friday night bisecting commits across repositories.

I got tired of this happening.


The Problem

Every multi-repo organization has shared contracts. API schemas, protobuf files, JSON schemas, config files, type definitions. Files that exist as copies in multiple repos that are supposed to stay in sync.

They don't stay in sync.

Monorepos solve this by construction. But most organizations don't have monorepos. They have 5, 50, or 500 separate repositories with informal copy-paste contracts between them, maintained by convention and hope.

The failure mode is always the same. Someone changes a file in one repo. The corresponding copy in another repo doesn't get updated. Nothing in the development workflow catches the divergence. It ships. Something breaks.


The Two-Part Problem

Contract drift has two challenges, and most teams only think about the second one.

Discovery. Which files, across which repos, are supposed to be in sync? In any non-trivial organization, nobody has the complete map. Contracts accumulate organically — a schema copied here, a config mirrored there — and the knowledge of what depends on what lives in people's heads, not in code.

Detection. Once you know what to monitor, how do you catch drift before production? Hash comparison, schema compatibility checks, CI gates. Straightforward once you know what to check.

But detection is useless without a complete inventory. And building that inventory manually is tedious, error-prone, and never up to date.


So I Built a Thing

repo-drift is an open-source CLI tool that solves both parts, in that order.

The init command points LLMs at your repositories and runs a five-phase pipeline:

repo-drift init \
  --repos myorg/api-service myorg/web-app myorg/shared-schemas
  1. Snapshot — shallow-clones each repo
  2. Analyze — deep scan of each repo: manifests, frameworks, endpoints, contract files
  3. Map — LLM builds a cross-repo architecture map: relationships, contract expectations, risk levels
  4. Discover — scans and classifies contract files, matches pairs across repos
  5. Generate — produces two files:
    • ARCHITECTURE.md — a comprehensive map with mermaid diagrams, relationships, risk-assessed contracts
    • .repo-drift.json — the drift config, ready for CI

Then run does deterministic drift checks — no AI needed, just file hashing, schema compatibility, and semantic comparison:

repo-drift run --config .repo-drift.json --snapshot

Exits non-zero if contracts have diverged. That's your CI gate.


Why LLMs for Discovery

The manual alternative is: get everyone in a room, ask "what files are shared between repos?", watch people struggle to remember, write it down, miss half of them, and repeat in six months when things have changed.

I tried that. The list was always incomplete.

LLMs are good at this kind of analysis. Give them the file trees, the manifests, the schemas, and they can identify contract relationships that humans forget about. They're not perfect — but they're faster and more thorough than a whiteboard session.

The key insight: use AI for the fuzzy discovery part, then use deterministic checks for enforcement. The LLM finds the contracts. Deterministic checks — hash comparison, schema compatibility, semantic equality — catch the drift. The AI doesn't need to be in the CI loop.


What It Actually Found

I ran it against seven repos at work. Two models in consensus mode:

repo-drift init \
  --repos org/sdk org/gateway org/web-app org/event-processor \
    org/auth org/infra org/ops \
  --models claude-opus-4.6,claude-sonnet-4.6

The architecture map identified quite a few contract expectations across the repos. The most critical: a protobuf definition that exists in both the SDK and the gateway service with no sync mechanism. Both sides assume it's identical. Nothing enforces it.

We knew about some of these contracts. We didn't know about all of them.


What the Output Looks Like

init produces two files. Here's what they actually look like (trimmed for the blog, but representative).

ARCHITECTURE.md

A full architecture map with a system overview, per-repo details, a mermaid relationship diagram, and risk-assessed contract expectations:

# Architecture Map

> Auto-generated by `repo-drift init` on 2026-04-08 20:04 UTC
> 7 repositories | 27 contracts | 20 relationships

## System Overview

| Repository         | Type            | Owns                               | Consumes                     |
|--------------------|-----------------|-------------------------------------|------------------------------|
| **sdk**            | library         | transport-messages, data-schema     | gateway-api, grpc-service    |
| **gateway**        | api             | grpc-service, gateway-events        | transport-messages           |
| **web**            | monorepo        | rest-api, graphql-schema            | auth-config, gateway-events  |
| **event-processor**| event-processor | status-events          | gateway-events, notifications|
| **auth**           | infrastructure  | auth-config, auth-actions           | infra-environments           |
| **infra**          | infrastructure  | aws-resources, infra-environments   | account-definitions          |
| **ops**            | infrastructure  | account-definitions, sso-permissions| -                            |

The relationships section generates a diagram like this:

Cross-repo contract relationship diagramCross-repo contract relationship diagram

Then the contract expectations table — this is where the real value is. Each contract has a risk level, an owner, consumers, and a rationale:

DomainTypeOwnerConsumersRiskRationale
transport-messagesprotobufsdkgatewayCRITICALCore communication protocol between SDK and gateway. Any breaking change disrupts all operations. Both repos maintain separate copies with no sync mechanism.
grpc-serviceprotobufgatewaysdk, web-appCRITICALThe gRPC service definition consumed by the SDK and indirectly by the web app. Breaking changes halt the platform.
auth-configconfigauthsdk, web-appCRITICALAuth actions define JWT claim shapes consumed by the web app and SDK. Changes break authentication platform-wide.
gateway-eventsjson-schemagatewayevent-processor, web-appHIGHEvents consumed by two downstream services with different processing needs.
rest-apiopenapiweb-appsdkHIGHThe REST API consumed by the SDK for registration and coordination.

The notes section is where the LLM earns its keep — observations it made while analyzing the architecture that weren't obvious from file structure alone:

"Critical protobuf drift risk: messages.proto exists in both the SDK and the gateway service. These are likely derived from the same source but maintained separately. There is no visible shared proto package or build-time synchronization mechanism."

"The event pipeline relies entirely on implicit TypeScript type agreements. No Avro, JSON Schema, or Protobuf schema governs the event stream format."

The full output was 260 lines across seven repos. It found contracts I'd forgotten about and relationships I hadn't mapped.

.repo-drift.json

The config file that drives the deterministic CI checks. Each check is a pair of files that should stay in sync:

{
  "version": 1,
  "repos": [
    { "id": "sdk", "url": "https://github.com/myorg/sdk" },
    { "id": "gateway", "url": "https://github.com/myorg/gateway" },
    { "id": "web-app", "url": "https://github.com/myorg/web-app" }
  ],
  "checks": [
    {
      "id": "transport-proto-sync",
      "type": "file_pair_hash",
      "severity": "critical",
      "params": {
        "left": "repos/sdk/src/transport/protos/messages.proto",
        "right": "repos/gateway/src/messaging/messages.proto"
      }
    },
    {
      "id": "rest-api-compat",
      "type": "openapi_compat",
      "severity": "high",
      "params": {
        "left": "repos/web-app/packages/api/specs/openapi.yaml",
        "right": "repos/sdk/specs/openapi.yaml"
      }
    },
    {
      "id": "ingest-contract-sync",
      "type": "json_semantic_equal",
      "severity": "high",
      "params": {
        "left": "repos/web-app/packages/api/specs/ingest.swagger.yaml",
        "right": "repos/web-app/packages/api/generated/merged.swagger.yaml"
      }
    }
  ],
  "policy": {
    "fail_on": [
      { "severity": "critical", "status": "fail" },
      { "severity": "high", "status": "fail" }
    ]
  }
}

Ten checks total from the seven-repo run. One critical (the protobuf), two high, four medium, three low. The policy section controls what breaks your CI — here, critical and high drift cause failures.

This config is what repo-drift run and repo-drift check consume. No AI involved at this point — just deterministic file comparison, schema compatibility checks, and semantic equality analysis.


Overrides: Customizing Without Losing Regeneration

The generated config is a starting point. You'll want to adjust it — promote a severity, add a check the LLM missed, change policy thresholds.

You could edit .repo-drift.json directly. But the next time you run init, your edits get overwritten.

Instead, create a .repo-drift-overrides.json:

{
  "checks": [
    {
      "id": "custom-internal-schema-sync",
      "type": "file_pair_hash",
      "source_repo": "myorg/schemas",
      "source_path": "api/v2.json",
      "target_repo": "myorg/consumer",
      "target_path": "schemas/api-v2.json",
      "severity": "critical"
    }
  ],
  "severity_overrides": {
    "ingest-contract-sync": "critical"
  },
  "policy": {
    "fail_on": ["critical", "high", "medium"]
  }
}

Overrides survive regeneration. When init runs again, it merges your overrides on top of the fresh config. Repos and checks are unioned by ID. Severity overrides are applied after merge. Unknown keys are passed through.

This means you can re-run init weekly on a cron — pick up new repos, new contracts, new relationships — without losing your manual tuning.


Multi-Model Consensus

One model hallucinates. Two models agreeing is signal.

repo-drift supports running multiple models and merging their results by agreement:

repo-drift init \
  --repos myorg/api myorg/web \
  --models claude-sonnet-4.6,gpt-4o

Each model independently analyzes the repos. Results are merged — entries found by multiple models get higher agreement scores. The output includes agreement_count so you can filter by confidence.

You can also add an orchestrator model that synthesizes the merged results into a refined final output:

repo-drift init \
  --repos myorg/api myorg/web \
  --models claude-sonnet-4.6,gpt-5.4 \
  --orchestrator claude-opus-4.6

Is this overkill for most setups? Probably. Does it catch things a single model misses? Yes.


The CI Gate

The part that actually prevents broken deployments is the check command — fast, deterministic, no AI:

git diff --name-only | repo-drift check --config .repo-drift.json

It looks at which files changed in your PR, checks if any of them are contract files, and returns a verdict. In GitHub Actions:

- uses: bristena-op/repo-drift@v0
  with:
    mode: check
    config_path: .repo-drift.json

Exit 0: no contracts affected. Exit 1: you're about to break a contract. The output tells you which checks failed and at what severity.

This is the part that runs on every PR. Fast, predictable, no LLM costs.


AI-Powered PR Review with Agentic Workflows

The check command gives you pass/fail. But sometimes you want more — a detailed review comment explaining what changed, which contracts are affected, and what the downstream team should do about it.

That's what generate-aw does. It generates a GitHub Agentic Workflows template — a markdown file that tells an AI agent how to review PRs against your contract config:

repo-drift generate-aw --config .repo-drift.json \
  --output .github/workflows/drift-review.md

The generated template includes:

  • Checkout frontmatter that pulls in all sibling repos from the config
  • The full contract list with file pairs and severities
  • Agent instructions to diff the PR, check each changed file against contracts, read both sides of affected pairs, and post a structured review comment

The output looks something like this (trimmed):

---
description: Cross-repo contract drift review powered by repo-drift
checkout:
  - repo: myorg/sdk
    path: repos/sdk
  - repo: myorg/gateway
    path: repos/gateway
tools:
  - github
safe-outputs:
  - add-comment
---

# Cross-Repo Contract Drift Review

## Monitored Contracts

- **transport-proto-sync** (file_pair_hash, severity: critical):
  `repos/sdk/src/transport/protos/messages.proto` <->
  `repos/gateway/src/messaging/messages.proto`

## Your Task

1. Read the PR diff to identify which files changed
2. Check each changed file against the contract pairs listed above
3. For each affected contract:
   - Read the source file and the counterpart file
   - Assess whether the change would cause drift or breakage
4. Post a review comment summarizing affected contracts,
   risk levels, and recommended actions

The AI agent reads your ARCHITECTURE.md for full context, so it can explain not just that a contract was affected but why it matters — which services consume it, what breaks if they diverge.

This is complementary to the check gate. check blocks the merge. The agentic workflow explains what to do about it.


Setting It Up for Your Org

Here's the practical adoption path. It takes about 30 minutes.

1. Create a central drift repo

Make a new repo — something like myorg/repo-drift-config. This is where your ARCHITECTURE.md, .repo-drift.json, and .repo-drift-overrides.json live. One repo that monitors all the others.

mkdir repo-drift-config && cd repo-drift-config
git init
pip install repo-drift

2. Run init locally to bootstrap

Point it at your repos and let the LLM do the discovery:

repo-drift init \
  --repos myorg/sdk myorg/gateway myorg/web-app myorg/auth \
  --models claude-sonnet-4.6

Review the output. Tweak severities in .repo-drift-overrides.json if needed. Commit both files.

3. Add a cron job to keep it fresh

Teams add repos. Contracts change. You want init to re-run periodically so the architecture map and config stay current.

Add a workflow to your drift repo that re-runs discovery on a schedule and opens a PR with any changes:

# .github/workflows/refresh-contracts.yml
name: Refresh contract map
on:
  schedule:
    - cron: "0 9 * * 1"  # Every Monday at 9am
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  refresh:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - uses: bristena-op/repo-drift@v0
        with:
          mode: discover
          repos: myorg/sdk myorg/gateway myorg/web-app myorg/auth
          api_key: ${{ secrets.COPILOT_API_KEY }}
          model: claude-sonnet-4.6
          app_id: ${{ vars.REPO_DRIFT_APP_ID }}
          app_private_key: ${{ secrets.REPO_DRIFT_PRIVATE_KEY }}

      - name: Open PR if config changed
        run: |
          git diff --quiet && exit 0
          git checkout -b refresh-contracts-$(date +%Y%m%d)
          git add ARCHITECTURE.md .repo-drift.json
          git commit -m "chore: refresh contract map"
          git push -u origin HEAD
          gh pr create --title "Refresh contract map" \
            --body "Auto-generated by weekly repo-drift discovery run."
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Your overrides file is never touched — it merges on top of whatever init generates. So the cron picks up new repos and contracts without losing your manual tuning.

4. Add the CI gate to each monitored repo

In each repo that has contract files, add a workflow that runs check on every PR:

# .github/workflows/drift-check.yml
name: Contract drift check
on: pull_request

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

      # Fetch the config from your central drift repo
      - uses: actions/checkout@v4
        with:
          repository: myorg/repo-drift-config
          path: drift-config
          token: ${{ secrets.CROSS_REPO_READ_TOKEN }}

      - uses: bristena-op/repo-drift@v0
        with:
          mode: check
          config_path: drift-config/.repo-drift.json

This is fast — no cloning, no AI, just checking whether the PR's changed files touch any contract. A few seconds per run.

5. (Optional) Add the agentic workflow for AI review

If you want detailed review comments on contract-touching PRs, generate the agentic workflow template and drop it into each repo:

repo-drift generate-aw --config .repo-drift.json \
  --output .github/workflows/drift-review.md

Copy that .md file into each monitored repo's .github/workflows/. The AI agent will post a comment on any PR that affects contracts, explaining the risk and recommending actions.

Auth for private repos

One thing you'll need: a GitHub App with read access to all monitored repos. Create it once at the org level, install it on the repos, and set REPO_DRIFT_APP_ID and REPO_DRIFT_PRIVATE_KEY as org-level secrets. Every repo inherits them — no per-repo secret management.

The whole setup is: one central repo with a cron, one workflow per monitored repo, one GitHub App for auth. After that it runs itself.


What It Doesn't Do (Yet)

Honesty section.

The tool currently supports five check types:

  • file_pair_hash — SHA-256 hash comparison for files that should be identical
  • json_schema_compat — JSON Schema compatibility checking (additive vs. breaking changes)
  • openapi_compat — OpenAPI spec compatibility (endpoint changes, field removals, type changes)
  • json_semantic_equal — JSON semantic equality (ignores key ordering and formatting)
  • yaml_semantic_equal — YAML semantic equality (same idea, for YAML files)

That covers most file-based contracts. What it doesn't catch:

  • Implicit contracts — JWT claim shapes, event schemas that exist at runtime but not as files on disk. The architecture map identifies these (it found 27 contract expectations in my repos, but only 10 had concrete file pairs to monitor). Bridging that gap is the hard problem.
  • Protobuf compatibilityprotobuf_compat is the obvious next check type. Wire-format compatibility analysis, not just file identity.
  • Cross-format contracts — a TypeScript type definition that should match a JSON Schema. Different formats, same contract.

The Boring Details

Ships as a Python package and a Go binary. Both produce identical JSON output.

pip install repo-drift

Python is recommended (more battle-tested). Go is beta. Zero external dependencies in both — Python uses only stdlib, Go uses only the standard library.

Supports GitHub Copilot and OpenAI as LLM providers. Copilot is the default — it starts an OAuth flow in your browser if no key is set. No API key management for the common case.

Full docs: github.com/bristena-op/repo-drift/docs


Why Open Source

This problem isn't unique to my company. Every multi-repo org has contract drift. The tools that exist are either proprietary, tied to specific schema formats, or require manual config that nobody maintains.

The combination I wanted — AI discovery + deterministic enforcement + zero dependencies + works with any file type — didn't exist. So I built it.

If your organization has more than three repos that share files, you probably have drift you don't know about. Point repo-drift at them and find out.


What's your contract drift story? I'd love to hear what's broken in production because two repos fell out of sync.