Skip to content

Git is the Source. Confluence is the Search Layer. Here's What Connects Them.

Banner image Banner image

Your engineers write markdown in Git. Your ops team lives in Confluence. Your AI agents query Confluence via MCP during incidents. You've been choosing between these audiences every time you write documentation — or maintaining two copies and watching them diverge.

There's a build step that eliminates the choice. This post shows you how to wire it up, and explains the one frontmatter block that makes every published page queryable by an AI agent without relying on fuzzy text search.

If you came here from HolmesGPT Knows What Your Runbooks Are Missing, this is the infrastructure layer that makes that loop possible. If not, you don't need that context — this post stands alone.


Quick takeaways

  • python-mkdocs-to-confluence publishes your entire MkDocs site to Confluence on every CI build — no manual copy-paste, no divergence.
  • Admonitions, code blocks, collapsible sections, and task lists all render as native Confluence macros.
  • confluence_properties: frontmatter creates a Confluence Page Properties macro — the same one used by Page Properties Reports. This makes pages queryable by CQL, not just searchable by text.
  • Any Confluence MCP client (HolmesGPT, Atlassian Rovo, Cursor) can now run property["Service"]="payments-api" to find the exact page, not guess from body text.
  • The whole thing is ~15 lines of mkdocs.yml config and one CI environment variable.

The split that makes documentation painful

Every platform team has a version of this problem:

  • Git is the source of truth. Runbooks, ADRs, and operational guides live alongside the code they describe. PRs, reviews, history, blame — all available. Engineers trust it.
  • Confluence is where operations happens. On-call engineers look there during incidents. Management tracks it. AI agents query it via the Confluence REST API.
  • They diverge immediately. The moment you copy-paste from one to the other, you've created two documents with no enforcement that they stay in sync.

Some teams solve this by committing to one. "Everything in Confluence" means losing Git history, PR reviews, and the ability to co-locate docs with code. "Everything in Git" means your on-call engineer is searching GitHub at 2am during an incident.

The plugin makes the question moot. Git is the authoring surface. Confluence is the rendering surface. The build publishes both.


How the plugin works

The python-mkdocs-to-confluence plugin hooks into the MkDocs build lifecycle. When you run mkdocs build with the plugin enabled and MKDOCS_TO_CONFLUENCE=1 set, it:

  1. Processes your markdown through MkDocs's normal pipeline
  2. Converts the output to Confluence storage format
  3. Publishes each page to the Confluence space and parent you've configured
  4. Preserves your folder hierarchy as a Confluence page tree

Markdown maps onto Confluence's own macro set:

Markdown Confluence output
!!! note admonition Info/Note/Tip/Warning macro
??? collapsible Expand macro
Fenced code block Code macro with syntax highlighting
- [x] task list ✅ checkbox
=== "Tab" tabbed content Expand panels
confluence_properties: frontmatter Page Properties macro
toc: true frontmatter Table of Contents macro

That's the difference from a plain markdown-to-HTML dump: someone reading the page in Confluence gets the same admonitions and macros they'd get if a person had built the page by hand in the editor.


Wiring it into your MkDocs config

Add the plugin to mkdocs.yml:

plugins:
  - awesome-nav:
      filename: ".nav.yml"
  - confluence:
      host_url: https://your-org.atlassian.net/wiki/rest/api/content
      space: RUNBOOKS
      parent_page_name: 'Platform / Runbooks'
      git_base_url: "https://github.com/your-org/markdown-pol-docs/blob/main"
      enable_header: true
      enable_footer: true
      header_text: "Auto-updated  {edit_link}"
      footer_text: "Auto-updated  {edit_link}"
      enabled_if_env: MKDOCS_TO_CONFLUENCE
      dryrun: false
      debug: false

Set enabled_if_env: MKDOCS_TO_CONFLUENCE — this means local builds (mkdocs serve) never touch Confluence. Only your CI pipeline publishes.

Install:

pip install mkdocs_confluence_plugin

The CI step

A single step in your pipeline, triggered on merge to main:

# .github/workflows/docs.yml
- name: Publish to Confluence
  env:
    CONFLUENCE_USERNAME: ${{ secrets.CONFLUENCE_USERNAME }}
    CONFLUENCE_PASSWORD: ${{ secrets.CONFLUENCE_API_TOKEN }}
    MKDOCS_TO_CONFLUENCE: "1"
  run: mkdocs build

That's it. Every merge to main publishes the full site. The edit_link in the header points back to the GitHub file — so readers in Confluence can find the source, and contributors know where to make changes.


The part that makes it agent-readable: confluence_properties

Standard runbook publishing gives you full-text search. It's fine for humans. It's not great for AI agents querying Confluence via MCP during an incident — fuzzy text search across hundreds of pages introduces ambiguity when you want precision.

The plugin's confluence_properties: frontmatter key changes this. Add it to any page's YAML frontmatter:

---
title: 'RB - payments-api OOMKill'
toc: true
confluence_properties:
  Service: payments-api
  Failure-Mode: OOMKill
  Alert-Name: KubePodOOMKilled
  Severity: P1-P2
  MTTR: 15m
  Owner: platform-team
  Status: Approved
  Last-Tested: 2026-06-28
labels: [runbook, payments, critical]
---

The plugin renders this as a native Confluence Page Properties macro — the same one used by Page Properties Reports across a space. Once published, the page is queryable via CQL:

type=page
AND space=RUNBOOKS
AND label=runbook
AND property["Service"]="payments-api"
AND property["Failure-Mode"]="OOMKill"

Compare that to a full-text search query:

type=page AND space=RUNBOOKS AND text ~ "payments-api OOMKill"

The CQL property query returns one page. The text search returns every page that mentions either term, in any context. For a human browsing Confluence, the difference is a ranked list. For an AI agent making a tool call in the middle of an incident response, the difference is finding the right runbook on the first query vs. reading six pages and guessing.


What your runbook frontmatter should include

Not every page needs the full property set. Runbooks and operational guides do — anything an on-call engineer or AI agent might need to retrieve under pressure.

A minimal useful set:

confluence_properties:
  Service: [service name — matches what your alert labels use]
  Failure-Mode: [the failure class — OOMKill, CrashLoop, HighLatency, etc.]
  Alert-Name: [exact Prometheus/Alertmanager alert name]
  Owner: [team or Slack handle]
  Status: Approved  # or Draft — makes it easy to filter out unreviewed pages

Extended set for runbooks that are part of an automated loop (e.g. HolmesGPT gap fill):

confluence_properties:
  Service: payments-api
  Failure-Mode: OOMKill
  Alert-Name: KubePodOOMKilled
  Severity: P1-P2
  MTTR: 15m
  Owner: platform-team
  Status: Approved
  Last-Tested: 2026-06-28
  Source: holmesgpt-gap-analysis  # or human-authored

The Source field lets you filter to only human-approved runbooks, or query the agent-drafted ones specifically:

property["Status"]="Approved" AND property["Source"]!="holmesgpt-gap-analysis"

The full pipeline

C4 Architecture — MkDocs to Confluence AI-readable pipeline C4 Architecture — MkDocs to Confluence AI-readable pipeline

End to end:

  1. Engineer writes markdown — runbook, ADR, operational guide. Lives in Git, reviewed as a PR, co-located with the code it documents.
  2. PR merges to main — CI picks it up.
  3. MkDocs builds — the plugin publishes the page to Confluence with the confluence_properties frontmatter rendered as a Page Properties macro.
  4. Confluence is the operational surface — on-call engineers find runbooks there, Atlassian Rovo reads it, HolmesGPT queries it via the Confluence MCP.
  5. AI agent queries precisely — CQL on property["Alert-Name"]="KubePodOOMKilled" returns the exact page, not a ranked guess.

The Git source never changes. Confluence stays current. Agents get structured data, not prose.


Configuring the Confluence MCP for HolmesGPT

If you're running HolmesGPT, wire the Confluence MCP to your space:

# holmes-config.yaml
mcpServers:
  - name: confluence
    command: uvx
    args: ["mcp-confluence@latest"]
    env:
      CONFLUENCE_URL: https://your-org.atlassian.net/wiki
      CONFLUENCE_TOKEN: ${CONFLUENCE_API_TOKEN}
      CONFLUENCE_SPACES: "RUNBOOKS"  # limit to the space the plugin publishes to

HolmesGPT will now prefer CQL property queries when it has enough signal from the alert (service name, alert name) — and fall back to text search when it doesn't. The structured frontmatter makes the preferred path hit more often.

Limit the MCP to your runbook space

Setting CONFLUENCE_SPACES to just your runbook space reduces noise. Holmes doesn't need to search your HR space or marketing wiki during an incident.


Handling the navigation hierarchy

The plugin preserves your MkDocs navigation structure as a Confluence page tree. If you use mkdocs-awesome-nav, the .nav.yml file controls the hierarchy:

# docs/technical-practices/monitoring-observability/runbooks/.nav.yml
nav:
  - Runbook Template: rb_template_ai_optimized.md
  - AWS Health Dashboard: rb_aws_health_dashboard_runbook.md
  - payments-api OOMKill: rb_payments_api_oomkill.md
  - s3-events-exporter:
      - s3-events-exporter/**/*.md

This becomes a matching Confluence page hierarchy under Platform / Runbooks / Monitoring-Observability / Runbooks.

Navigation matching is smart, not brittle

The plugin uses semantic matching to map MkDocs nav entries to Confluence pages — abbreviation expansion (ADRsArchitecture Design Records), context-aware matching for nested directories, and fuzzy fallback for edge cases. You don't need exact title matches.


Running it locally without touching Confluence

Because the plugin is gated by enabled_if_env: MKDOCS_TO_CONFLUENCE, your local workflow is unchanged:

# Local — normal MkDocs serve, no Confluence publish
mkdocs serve

# Dry run — see what would be published without touching anything
MKDOCS_TO_CONFLUENCE=1 mkdocs build  # with dryrun: true in mkdocs.yml

# Full publish — what CI does
CONFLUENCE_USERNAME=your@email.com \
CONFLUENCE_PASSWORD=your-api-token \
MKDOCS_TO_CONFLUENCE=1 \
mkdocs build

What changes for your team

Before this pipeline: your runbook is in Git. To share it with on-call engineers in Confluence, someone copy-pastes it. Six weeks later, the Git version has been updated and the Confluence version hasn't.

After: every merge to main is the publish step. There's no copy-paste. The edit link in the Confluence header points back to GitHub, so readers know where to contribute changes. The confluence_properties frontmatter means every published page is precisely queryable by any Confluence MCP client, not just searchable by keyword.

The knowledge base your AI agents read is now exactly as current as your Git history.


FAQ

Does this replace the MkDocs-built static site?

No — both outputs are independent. mkdocs build still produces the static site (your docs portal or GitHub Pages). The Confluence plugin publishes alongside that. You get both: a static docs site for public or team-wide consumption and a Confluence integration for the teams that live in Atlassian tooling.

What happens if I rename or move a file in Git?

The plugin uses smart navigation matching to find the existing Confluence page by title and update it in place. If you rename the markdown file and update the nav entry, the plugin should match and update the existing page rather than creating a duplicate. Enable debug: true and run with dryrun: true to verify the matching before a real publish.

Can I control which pages publish to Confluence?

Yes — the enabled_if_env flag controls the whole plugin. For page-level control, you can set draft: true in frontmatter (standard MkDocs Material behaviour — these pages are excluded from the build entirely). Pages excluded from your .nav.yml are also excluded.

Does the confluence_properties approach work with Atlassian Rovo?

Yes. Rovo's Confluence search uses the same CQL interface. The Page Properties macro is a standard Confluence feature, not specific to HolmesGPT or any particular MCP client.

How do I backfill confluence_properties on existing runbooks?

Add the frontmatter block to your markdown files, commit, and let CI publish. The next build updates the existing Confluence pages with the Page Properties macro. You don't need to recreate pages — the plugin updates in place.


For the runbook gap-detection loop that generates drafts for missing runbooks, see HolmesGPT Knows What Your Runbooks Are Missing. For the AI-optimised runbook template that defines the confluence_properties fields used throughout this post, see the runbook template in the Polarpoint docs.