One Docs Site, Many Repos: How OpsMill Builds Infrahub’s Documentation on Docusaurus

|

Jun 17, 2026

In this post

Category

Keeping docs up to date is usually a game of hot potato. Every team thinks it’s another team’s job to maintain them, and, eventually, they get so stale nobody wants to touch them.

At OpsMill, docs aren’t owned by one team because they’re not stored in a single codebase.

There’s the core Infrahub project, the Python SDK, the Ansible collection, the Nornir plugin, the sync engine, a VS Code extension, an MCP server, the schema library, a handful of demo repos, and more arriving roughly every quarter.

Every one of those is owned by the team that builds it, and every one of them carries its own documentation in its own repository, right next to the code it describes. And because it all gets pushed to one place, all visitors see is a single Docusaurus site.

Below, we explain more about why we’ve gone in this direction, what the architecture looks like, and how the CI plumbing works (with a few kinks we’re still working out).

The Why

Documentation should live in the same repo as the code it documents, and ideally change in the same pull request. Because if you or an AI agent fixes a behavior and doesn’t touch the docs in the same PR, the docs are already wrong by the time you merge.

Easier said than done. At OpsMill, we were considering 3 options:

Option A: A Single Docs Monorepo

All documentation in one repository, written and reviewed in one place.

  • Pros: easy to build, only one sidebar to reason about, no syncing needed.
  • Cons: it divorces docs from code. A change to an SDK no longer touches the SDK’s docs, they’re in a different repo (the exact type of failure we were trying to solve for).

Option B: Git Submodules

Keep docs in each repo and pull them into the aggregator as submodules.

  • Pros: docs stay with their code, and the aggregator references commits.
  • Cons: submodule UX is famously unpleasant (anyone who has explained git submodule update –init –recursive to a new starter knows the look they get back), and the pinned commits go stale the moment nobody remembers to bump them. You don’t have a sync problem, but you now have a pinning problem.

Option C: Push-Based Sync

Docs live in each repo, and when they change, that repo’s CI pushes a copy into the aggregator, which then builds the combined site.

  • Pros: docs stay next to the code, the aggregator always has the latest content, and no single team owns the docs. Each repo is self-contained.
  • Cons: there is some maintenance, and the sync itself is a direct push to the aggregator’s main using a personal access token. Not the most elegant thing I’ve ever shipped, but it’s predictable, and the review already happened in the source repo where the docs were edited.

We went with C. A was never really on the table once we committed to docs-as-code, and B’s pinning problem felt worse in the long run.

The What

Once we made that decision, we started building our aggregator repo: opsmill/infrahub-docs.

It’s a single Docusaurus site, but instead of one big docs folder it registers one @docusaurus/plugin-content-docs instance per project. Each instance gets its own content folder, its own sidebar, its own route, and an editUrl that points back at the source repo. That way, when you click “Edit this page,” you end up in the right place.

A few things sit on top of all of that:

  • Algolia search. It spans every instance so one search box covers all dozen-plus projects. We have an “Ask AI” assistant wired in alongside it, too.
  • @docusaurus/theme-mermaid. Contributors can author diagrams as Mermaid in Markdown and have them render on the site.
  • A DOCS_IN_APP toggle. You can render content in two ways: standalone at docs.infrahub.app, or embedded inside the Infrahub application itself. The config flips url and baseUrl depending on the environment variable, so the docs you read in-product are the docs you read on the web.

Here’s what it looks like:

How Infrahub's docs architecture works

The How

One Plugin Instance Per Project

In docs/docusaurus.config.ts, each project is registered as its own content-docs plugin. Here’s the Python SDK’s entry, verbatim:

[
  '@docusaurus/plugin-content-docs',
  {
    id: 'docs-python-sdk',
    path: 'docs-python-sdk/python-sdk',
    routeBasePath: 'python-sdk',
    sidebarCollapsed: false,
    sidebarPath: './sidebars-python-sdk.ts',
  },
]

The id namespaces the instance, path points at the folder the sync writes into, routeBasePath gives it a clean URL prefix (/python-sdk), and sidebarPath references a sidebar file that the source repo also owns and syncs. This block repeats fifteen-plus times, once per project. When you add a new project, you add a new block.

The Sync Workflow

Every source repo carries a .github/workflows/sync-docs.yml. It only fires when documentation changes, so a normal code-only PR doesn’t churn the docs site:

on:
  push:
    branches:
      - main
    paths:
      - 'docs/docs/**'
      - 'docs/sidebars.ts'

When it runs, it checks out two repositories: the source repo it lives in, and the infrahub-docs aggregator, using a PAT_TOKEN secret for write access to the latter.

- name: Checkout source repo
  uses: actions/checkout@v6
  with:
    path: source-repo
- name: Checkout infrahub-docs
  uses: actions/checkout@v6
  with:
    repository: opsmill/infrahub-docs
    token: ${{ secrets.SOME_TOKEN }}
    path: target-repo

Then, it clears the project’s folder in the aggregator, copies the freshly-built docs and sidebar across, and pushes, but only if something truly changed.

rm -rf target-repo/docs/docs-infrahub-demo-sp/
rm -f target-repo/docs/sidebars-infrahub-demo-sp.ts
cp -r source-repo/docs/docs/. target-repo/docs/docs-infrahub-demo-sp/
cp source-repo/docs/sidebars.ts target-repo/docs/sidebars-infrahub-demo-sp.ts
cd target-repo
git add .
if ! git diff --cached --quiet; then
  git commit -m "Sync docs from infrahub-demo-sp repo"
  git push
fi

Note: this is the direct-push-to-main step I flagged earlier. Reviews happen in the source repo’s PR, where a human reads the docs change. Since the sync is mechanical, it ships without its own PR. If we ever want a gate, this is the line we’d change.

The full sequence, from an engineer’s PR to live docs, goes like this:

What our Infrahub docs CI sequence looks like

Onboarding a New Repo

Adding a project by hand means touching a workflow, a sidebar, a config, and a folder, in two repos, in the right order. Because we don’t trust ourselves to do that consistently, we built scripts/setup-docs-repo.py, which:

  • Scaffolds the documentation framework into the source repo (docs/docusaurus.config.ts, docs/package.json, docs/sidebars.ts)
  • Drops in the linting config (.vale.ini, .markdownlint.yaml, .yamllint.yml)
  • Writes the sync-docs.yml workflow with the project name substituted in

On the aggregator side, it creates the placeholder content folder and sidebars-<project>.ts.

It can’t do everything though, so there are still some human steps:

  1. The central config edit is manual. You have to add the new plugin-content-docs block and the navbar entry to docusaurus.config.ts yourself. The script generates the snippet, but a navbar belongs somewhere specific and a script guessing the location would do more harm than good.
  2. Merge order matters. You have to merge the infrahub-docs PR first, then the source repo PR. If you merge the source repo first, its sync workflow fires and pushes docs into a folder the aggregator config doesn’t know about yet. Aggregator first, source second. Get it backwards once and you’ll remember forever.

Keeping a Dozen Repos Consistent

The thing that makes a multi-repo docs site feel like one site (rather than fifteen strung together) is that every repo is held to the same standard.

Our onboarding script templates Vale (prose linting), markdownlint, and yamllint into each source repo, and those run in the source repo’s own CI.

By the time a docs change reaches the sync step, it’s already passed the same prose and formatting checks as every other project, enforcing consistency.

The Payoff

It’s not the flashiest system, but a docs site that’s always current is good for our:

  • Customers, who need the most accurate, up-to-date information
  • Engineers, who don’t have to context-switch into a separate docs repo or wait on another team to publish their changes
  • New joiners, who need to get up to speed on Infrahub quickly

We do a fair amount of sweating the unglamorous details at OpsMill. If that’s your idea of a good time, we want to hear from you. Take a peek at our open roles.

Pete Crocker, OpsMill Director Solutions Architecture

Pete Crocker | Seasoned builder of infrastructure automation strategy and early-stage tech. Comfortable hanging out from the CLI to the C-suite, connecting technical detail to business impact. Holds a full buzzword bingo card in tech and protocols. Savors two flat whites a day. Director, Solution Architecture at OpsMill.

REQUEST A DEMO

Infrahub logo

See what Infrahub can do for you

Get a personal tour of Infrahub Enterprise

Learn how we can support your infrastructure automation goals

Ask questions and get advice from our automation experts

By submitting this form, I confirm that I have read and agree to OpsMill’s privacy policy.

Fantastic! 🙌

Check your email for a message from our team.

From there, you can pick a demo time that’s convenient for you and invite any colleagues who you want to attend.

We’re looking forward to hearing about your automation goals and exploring how Infrahub can help you meet them.