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.
Step by step Check pull requests for drift
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.
{
"$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
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:
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. Anodeon a glob applies to every file it matches.- Globs skip
node_modules,.git,dist,build,vendor,.terraformand 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. --filesreplacessourcesfor that run, but keeps thenodeof 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:
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 optionallyMODUDRAFT_API_URL). --on-drift reportprints the drift,failalso exits 1 when there is any,update-diagramwrites the change to the diagram.update-diagramupdates the diagram, never your code.- The change set goes to stdout, diagnostics to stderr, so
--format jsonpipes cleanly. - If the API answers 429 (rate limited), the command retries up to three attempts in total, waiting for the
Retry-Aftertime (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 withupdate-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:
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 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.