CI drift check

Keep your diagram honest in CI

Check every pull request against your Modudraft architecture diagram, so drift between the code and the diagram shows up before merge. The check reads files in your repo (compose, Kubernetes, Terraform, OpenAPI, Prisma, SQL and more), compares them with a diagram, and reports what was added, removed or changed. You do not need the MCP integration or an AI agent to use it.

You need a Modudraft Pro account, a diagram, and an API key (Account, then API Keys, at app.modudraft.com/account). Store the key as a CI secret.

Overview

The same engine runs in three places: the check command (any CI system), the modudraft/drift-check GitHub Action (a wrapper around the command that comments on pull requests), and the sync_project tool in the MCP server. A run is deterministic: the same repo and the same diagram always give the same change set, and the output lists every file that was checked.

What is compared

  • Architecture: docker-compose.
  • Structurizr: workspace.dsl (and files it includes).
  • Cloud: Kubernetes YAML, Terraform, CloudFormation.
  • DB schema: SQL DDL, Prisma, DBML, Rails schema.rb, TypeORM, SQLAlchemy, dbt.
  • API: OpenAPI 2/3, GraphQL SDL, .proto, Postman, Insomnia v4, AsyncAPI.

Not covered: routes or service calls that exist only in application code. Code wins for the fields it owns; positions, notes, hand-added items and cross-tab links are kept. A file that fails to parse is reported as an error and removes nothing. A deleted source file is reported, and its items are removed only with --remove-missing (refused if every recorded source is missing).

Where the files come from

In order: the --files option, modudraft.json, the files recorded on the diagram's imported items (this repo only), then a scan (preview only; TypeORM and SQLAlchemy files are only picked up when listed). Paths are relative to the repo root (outside git: the project root containing --dir). Symlinks pointing outside the repo are refused.

Without modudraft.json or --files, the check uses the files the diagram's items were imported from. That works with no setup, but CI is more predictable with the list written down: see modudraft.json.

modudraft.json

An optional file in the repo root that names the diagram and the files to check. It is never required: --diagram and --files (or the Action's diagram input) do the same job and win when both are given. It holds no secrets, so commit it.

modudraft.json
{
  "$schema": "https://modudraft.com/schema/modudraft.json",
  "diagram": "3f1c9a52-7b4e-4d2a-9c61-0e8b5d7a4f19",
  "sources": [
    "docker-compose.yml",
    "infra/**/*.tf",
    "db/schema.prisma",
    { "path": "api/openapi.yaml", "node": "Orders API" }
  ]
}

Add "$schema": "https://modudraft.com/schema/modudraft.json" for autocomplete and validation in VS Code and JetBrains; sync_project with write_config: true adds it.

Fields

FieldTypeMeaning
diagramstring, optional The diagram id: the part after /d/ in its URL. --diagram overrides it.
sourcesarray, optional The files to check. Each entry is a string or an object (below). Missing or empty: the files recorded on the diagram are used.

Other keys are ignored and kept when the file is rewritten. The file must be a JSON object under 256 KB.

String or object entries

Most files map onto the diagram by themselves: docker-compose.yml becomes architecture nodes, Terraform and Kubernetes files become cloud resources. Those are plain strings.

DB schema and API files (SQL, Prisma, DBML, OpenAPI, GraphQL, .proto and so on) describe one service, so their tables or endpoints belong to a node on the architecture tab. The object form says which node:

KeyTypeMeaning
pathstring, required The file or glob, the same as a string entry.
nodestring, optional The architecture node that owns the file's tables or endpoints: its id, or its label (not case-sensitive). Ignored for other files.

A DB or API file can stay a plain string when it was imported into exactly one node before: the check finds that node from the earlier import. Otherwise it fails for that file with "needs a node", and a label that matches several nodes fails with their ids, so you can name one by id. "orders.sql" and { "path": "orders.sql" } mean the same thing.

Paths and globs

  • Relative to the repo root, with forward slashes. Absolute paths and .. are rejected.
  • * matches within one folder, ** matches any depth, ? matches one character. A node on a glob applies to every file it matches.
  • Globs skip node_modules, .git, dist, build, vendor, .terraform and similar folders.
  • A glob that matches nothing is a warning. If no entry matches anything, the check stops with exit 2 rather than report "in sync".
  • A listed file that no longer exists is reported, and its items are removed from the diagram only with --remove-missing.
  • --files replaces sources for that run, but keeps the node of a matching entry.

Creating it

Write it by hand, or ask your agent to run sync_project with write_config: true: it writes the diagram id and the files it used, with the nodes it found, sorted by path.

The check command

Runs anywhere Node 18+ is available:

Terminal
MODUDRAFT_API_KEY=mdft_your_key npx @modudraft/mcp check \
  [--diagram <id>] [--dir .] [--files a,b] \
  [--on-drift report|fail|update-diagram] \
  [--format text|markdown|json] [--remove-missing]
  • Auth: MODUDRAFT_API_KEY (and optionally MODUDRAFT_API_URL).
  • --on-drift report prints the drift, fail also exits 1 when there is any, update-diagram writes the change to the diagram. update-diagram updates the diagram, never your code.
  • The change set goes to stdout, diagnostics to stderr, so --format json pipes cleanly.
  • If the API answers 429 (rate limited), the command retries up to three attempts in total, waiting for the Retry-After time (at most 30 seconds) or 2 and 4 seconds, then exits 2.

Exit codes

  • 0: in sync, report mode, or update succeeded.
  • 1: drift with --on-drift fail.
  • 2: error (parse errors, config, sources that match no files, nothing checked, auth, network, rate limit, a refused apply, or a read-only key with update-diagram). Exit 1 always means drift, so a crash never looks like one.

Read-only and write keys

report and fail only read the diagram, so a read-only key is enough and is the safer choice for pull request workflows. update-diagram needs a key with write scope; with a read-only key it exits 2 with a clear message.

GitHub Action

modudraft/drift-check@v1 inputs: api-key (required), diagram, dir, on-drift (default fail), comment, remove-missing, allow-update-on-pr, mcp-version (default: the version the Action release was tested with) and github-token. On pull_request it posts one sticky comment (needs permissions: pull-requests: write); otherwise it writes a job summary. update-diagram runs only on push, workflow_dispatch or schedule unless allow-update-on-pr is set.

Example workflows

Check on pull requests, then update the diagram after merge:

.github/workflows/drift-pr.yml
on: [pull_request]
permissions: { contents: read, pull-requests: write }
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: modudraft/drift-check@v1
        with:
          api-key: ${{ secrets.MODUDRAFT_API_KEY }}
          diagram: <diagram-id>
          on-drift: fail
.github/workflows/drift-main.yml
on:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: modudraft/drift-check@v1
        with:
          api-key: ${{ secrets.MODUDRAFT_API_KEY }}
          diagram: <diagram-id>
          on-drift: update-diagram

Fork pull requests

GitHub does not give secrets to workflows triggered by pull requests from forks. When the API key is not available, the Action skips the check with a warning instead of failing the run.

Versions

Each release of the Action pins the @modudraft/mcp version it was tested with, so a new MCP release never reaches your CI by surprise. Set mcp-version (for example mcp-version: 0.27.1) to run another version.