Docs & Code Guide

Automate the architecture diagram in CI: what to run, and what to keep off the merge path

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

Sooner or later someone proposes it: a CI job that regenerates the architecture diagram so it can never be wrong again. The instinct is right and the usual implementation is a maintenance liability. Rendering a picture in CI and committing it back creates bot commits, binary merge conflicts and a pipeline loop, and it still does not solve the underlying problem, which is that the diagram source was hand-written in the first place. Here is what each approach actually costs.

8 min readFor platform engineers who were asked to put the diagram in the pipeline

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

126/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Three patterns, and where each one breaks

Render and commit back. A job runs on every push, renders the diagram source to an image, and commits the result. It works for a week. Then two branches both regenerate the image and you get a binary conflict that nobody can resolve by reading the diff. Then the bot commit triggers the pipeline again and you add a skip marker to break the loop. Then someone force-pushes and the image is from a commit that no longer exists. Most of all, the source is still hand-authored, so the automation guarantees the picture matches the file, not that the file matches the system.

Fail the build on staleness. A check compares a hash of the code against a hash recorded next to the diagram and fails when they diverge. This is much better in principle and noisy in practice, because almost every change to the codebase touches the hash while almost none of them change the architecture. Engineers learn to regenerate mechanically without looking, which produces a diagram that is technically current and never reviewed.

Generate out of band. A scheduled job regenerates from the default branch, and nothing about it can block a merge. Freshness drops from instant to a day, which is well inside the tolerance of any documentation reader, and the failure mode is a stale picture rather than a red pipeline on an unrelated pull request.

# The render-and-commit pattern, with the guards it eventually needs.
name: diagram
on:
  push:
    branches: [main]

jobs:
  render:
    # without this guard the bot commit below re-triggers this workflow
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/render-diagram.sh > docs/architecture.png
      - run: |
          git config user.name "ci"
          git config user.email "[email protected]"
          git add docs/architecture.png
          git diff --staged --quiet || \
            git commit -m "chore: regenerate diagram [skip ci]"
          git push

Keep the blocking checks for facts a machine can verify

CI is good at binary questions with an unambiguous answer. Does every environment variable named in the docs still exist in the config schema. Does every directory named in ARCHITECTURE.md still exist. Does every internal link resolve. Does the OpenAPI document still match the handlers. These fail rarely, and when they fail the author knows exactly what to fix.

CI is bad at judging whether a picture communicates the system. There is no assertion for that, so teams substitute a proxy such as file age or content hash, and proxies produce false failures. A false failure on a documentation check trains people to regenerate without reading, which is worse than having no check because it manufactures the appearance of review.

A practical split: block the merge on the checkable facts, and let the picture regenerate on a schedule. See docs checks in CI for the checks that are worth blocking on.

Regenerating from the repository instead of maintaining a job

The reason the CI job exists is that the diagram is downstream of the code and nobody wants to hand-carry the update. That is exactly what repository sync does without a job in your pipeline. Connect GitHub, GitLab or Azure DevOps read only, choose a branch or a tag, and the diagram plus an architecture doc are regenerated daily.

Three behaviours matter for automation specifically. Each sync hashes what feeds the diagram, the parsed Terraform draw plan or the assembled corpus, into a structure fingerprint; when the fingerprint is unchanged the run stops before generation, so a dependency bump or a comment fix costs nothing and produces no new picture for anyone to review. A node dragged more than twelve pixels from where the previous sync left it is recorded as hand-placed and restored after the redraw, so automation does not undo layout work someone did on purpose. And because the diagram is served as a live embed, no image is committed anywhere, which removes binary conflicts and bot commits entirely.

When you do want a refresh at a specific moment, such as immediately after a release tag, trigger it from an agent rather than from a shell script. The MCP server exposes thirty-six tools to Claude Code, Cursor, ChatGPT and Claude Desktop, nine of them outcome-level; repo_status reports which repository and ref a diagram tracks, when it last synced and the commit it reflects, and repo_refresh syncs it now. A release checklist step becomes a sentence instead of a maintained YAML file.

Nothing in the pipeline to maintain

No render step, no committed image, no skip-ci guard, no bot account. The diagram follows the branch you pinned. See repository sync.

If you keep the CI job anyway

Commit the diagram source, never the rendered output, and render at read time. That alone removes binary conflicts and makes every diagram change reviewable as a text diff.

Run the job on the default branch only. Regenerating on pull request branches multiplies the failure modes and produces images for commits that will be squashed away.

Pin the renderer version. Diagram layout engines change their output between releases, and an unpinned renderer produces enormous diffs that have nothing to do with your architecture, which is how teams end up ignoring the diagram change in every review.

FAQ

Should CI regenerate the architecture diagram on every pull request?

Usually not. Almost every change touches the code without changing the architecture, so per-pull-request regeneration produces noise and trains reviewers to ignore the diagram. Regenerating on the default branch on a schedule gives freshness within a day without ever blocking a merge.

What goes wrong when CI commits a rendered diagram back to the repository?

Binary merge conflicts between branches that both regenerated, a pipeline loop unless the bot commit carries a skip marker, bot commits cluttering the history, and images referring to commits that were later rewritten. The deeper issue is that it only proves the image matches the diagram source, not that the source matches the system.

What documentation checks are worth failing a build on?

Checks with an unambiguous answer: every environment variable named in the docs exists in the config schema, every directory named in the architecture file exists, every internal link resolves, the API document matches the handlers. Freshness proxies such as file age or content hashes produce false failures and get regenerated without being read.

How can a diagram stay current without a CI job at all?

By generating it from the repository outside the pipeline. A read-only connection pinned to a branch or a tag can be re-read on a daily cycle to redraw the diagram and rewrite the architecture doc, with no render step, no committed image and no bot account involved.

Can a diagram refresh be triggered at a specific moment, like after a release?

Yes. An MCP client such as Claude Code, Cursor, ChatGPT or Claude Desktop can call repo_status, which reports the tracked repository, the branch or tag, the last sync and the commit reflected, then repo_refresh to sync now. A release checklist step becomes a request in the client rather than a maintained pipeline script.

Why do generated diagrams produce huge diffs even when nothing changed?

Because layout engines change their output between versions and many of them do not produce stable node ordering. Pinning the renderer version helps. Deriving node identity from source addresses helps more, since a regeneration then updates the picture in place, and hashing the parsed structure means a run whose input did not change can skip regeneration entirely.