atlassian-cli is an independent, community open-source project, not affiliated with, endorsed by, or sponsored by Atlassian, and is not Atlassian's official CLI (acli). Product names are used only to identify compatibility.

To get MkDocs to Confluence, convert each Markdown source file into Confluence storage HTML and push it to a page with atlassian-cli, then run that script in CI so Confluence updates on every merge. There is no MkDocs plugin to install and no manual copy-paste: your Git repository stays the single source of truth, and Confluence becomes a rendered mirror that stakeholders can read, comment on, and search.

This is the docs-as-code pattern applied to a wiki. Engineers already write documentation in Markdown next to the code it describes, review it in pull requests, and version it in Git. The friction shows up when non-engineers live in Confluence. Left to humans, the two copies drift within a sprint. A pipeline removes the drift by making publication a build step instead of a chore.

How this differs from acli: atlassian-cli is an independent, community open-source project. It is not Atlassian's official CLI (acli) and is not affiliated with Atlassian. Use the official acli if you want first-party vendor support; use atlassian-cli if you want a single free Rust binary spanning Jira, Confluence, Bitbucket, and JSM that scripts cleanly into a docs pipeline.

What docs-as-code with Confluence means

Docs-as-code treats documentation like source: plain-text files, a version control history, code review, and automated publishing. MkDocs is a natural fit because its inputs are just Markdown files under a docs/ directory plus a single mkdocs.yml that declares the navigation tree. You already build a static HTML site from those files. The goal here is a second output target: Confluence.

The important design choice is what you send to Confluence. You do not want the full themed HTML page that mkdocs build produces, wrapped in a navbar and theme CSS. Confluence has its own chrome. What you want is the body of each document as Confluence storage format, a constrained subset of HTML that the API accepts. So the pipeline works from the raw Markdown sources, not the built site, converting one source file into one Confluence page body.

Before wiring anything up, confirm your credentials work against the target space, then:

# Verify the profile and that the space exists
atlassian-cli auth test --profile prod
atlassian-cli confluence space get DOCS --profile prod

Map your MkDocs project to a space

MkDocs describes structure in the nav: block of mkdocs.yml. That tree is your mapping to Confluence. Each top-level section becomes a parent, and every page nested under it becomes a child. Confluence models this with page parents and, since v0.4, dedicated folders. Create the container hierarchy once, then hang pages off it.

# A section in mkdocs.yml nav -> a Confluence folder
atlassian-cli confluence folder create \
  --space DOCS \
  --title "Guides" \
  --parent 3302031761

# A page created under a parent keeps the tree shape
atlassian-cli confluence page create \
  --space DOCS \
  --title "Getting Started" \
  --parent 3302031761 \
  --body "<p>placeholder</p>"

In practice you keep a small mapping (a text file or a few lines of shell) from each Markdown path to its parent ID. That mapping is the only Confluence-specific state you maintain, and it changes only when you reorganize the docs. Everything else is derived from the Markdown itself: the page title comes from the first # heading in each file, and the body comes from the conversion step below.

Convert and publish one page

Start with a single page so the moving parts are obvious, then wrap it in a loop. Two facts make this reliable. First, atlassian-cli confluence page create and page update both accept Confluence storage HTML through --body, so you can hand them the output of any Markdown converter. Second, you can find an existing page by exact title with a CQL search, which is what makes republishing idempotent instead of duplicative.

# 1. Convert one MkDocs source file to storage HTML
BODY=$(pandoc -f markdown -t html docs/getting-started.md)

# 2. Look the page up by title (empty if it does not exist yet)
PAGE_ID=$(atlassian-cli confluence search cql \
  "space = DOCS AND title = \"Getting Started\"" \
  --format json | jq -r '.results[0].content.id // empty')

# 3. Create the first time, update every time after
if [ -z "$PAGE_ID" ]; then
  atlassian-cli confluence page create \
    --space DOCS --title "Getting Started" \
    --parent 3302031761 --body "$BODY" --profile prod
else
  atlassian-cli confluence page update "$PAGE_ID" \
    --title "Getting Started" --body "$BODY" --profile prod
fi

Pandoc is only an example. Any tool that emits HTML Confluence accepts will do, and keeping conversion out of the CLI means you can swap it or post-process the HTML (for admonitions, code blocks, or Confluence macros) without touching the publish logic. The Markdown to Confluence guide covers the conversion details and formatting edge cases; this post focuses on turning that single-page action into a pipeline.

To generalize, loop over the files MkDocs already knows about. You can read the paths straight from mkdocs.yml, or just walk the docs/ directory. Extract the title from the first heading so titles stay in sync with the source:

# Title from the first "# heading", fallback to the filename
TITLE=$(grep -m1 '^# ' "$md" | sed 's/^# //' \
  || basename "$md" .md)

Publish the whole site from CI

The payoff is running this on merge, not by hand. In CI you do not commit credentials: atlassian-cli overrides the stored token with the ATLASSIAN_API_TOKEN environment variable, so you configure a lightweight profile in a setup step and inject the real token from your secret store at run time. Trigger the job only when documentation changes so unrelated commits do not republish.

# .github/workflows/publish-docs.yml
name: publish-docs
on:
  push:
    branches: [main]
    paths: ['docs/**', 'mkdocs.yml']

jobs:
  confluence:
    runs-on: ubuntu-latest
    env:
      ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Install tools
        run: sudo apt-get update && sudo apt-get install -y pandoc jq
      - name: Configure profile
        run: |
          atlassian-cli auth login --profile ci \
            --base-url https://mycompany.atlassian.net \
            --email bot@mycompany.com --default
      - name: Publish to Confluence
        run: ./scripts/publish-to-confluence.sh DOCS ci

The auth login step only records profile metadata (base URL, email, default). The actual secret arrives through the environment variable, which the CLI prefers over anything on disk. Bitbucket Pipelines works the same way: set the token as a repository variable, add a pipelines step that installs the same tools, and run the identical script. Because everything is one static binary plus pandoc and jq, the job needs no runtime and no language toolchain, and the same token-via-environment pattern works for any other Atlassian automation you run in CI.

Idempotency, labels, and backups

A pipeline that runs on every merge has to be safe to run repeatedly. Three habits keep it boring in the good way.

Idempotency by title lookup. Because step 2 above searches for the page before writing, a rerun updates the same page rather than creating a second one. A file whose content has not changed converts to byte-identical storage HTML, so re-publishing it is a no-op. This is what lets you rerun the whole site without fear.

Label everything the pipeline owns. Tag generated pages so humans can tell machine-published content from hand-written pages, and so you can audit or clean up later. Add a label per page during the run, and reconcile the whole space in one shot:

# Per page, during the loop
atlassian-cli confluence page add-label "$PAGE_ID" mkdocs

# Reconcile labels across the space (preview first)
atlassian-cli confluence bulk add-labels \
  --cql "space = DOCS" \
  --labels mkdocs,generated \
  --dry-run

Back up before a big republish. Before a large or first-time run, snapshot the space so a bad conversion is recoverable. The bulk export command writes every matching page to a single JSON file:

# Snapshot the space before republishing
atlassian-cli confluence bulk export \
  --cql "space = DOCS" \
  --output confluence-backup.json \
  --format json

For a fully worked script with dependency checks, title extraction, and dry-run support, the Confluence Markdown sync runbook is the reference implementation this post is based on. If you ever need to pull content back the other way, the export Confluence to Markdown guide covers the reverse direction.

Ways to get Markdown into Confluence

There is more than one route from Markdown to a Confluence page. They differ mainly in whether they can run unattended and whether re-running is safe.

Approach Runs in CI Idempotent rerun Keeps hierarchy Dry-run preview
Manual copy-paste No No Manual No
Confluence UI import No No Partial No
atlassian-cli pipeline Yes Yes (title lookup) Yes (parent / folder) Yes (bulk dry-run)

The manual and UI routes are fine for a one-off page. For living documentation that changes every sprint, only a scripted pipeline keeps Confluence honest without adding a recurring manual task. For the exact flags used above, keep the command reference open while you build your script.

Ship your docs pipeline

Install the single Rust binary and wire MkDocs to Confluence from CI in an afternoon.

Try atlassian-cli

FAQ

How do I publish MkDocs to Confluence automatically?

Convert each MkDocs source Markdown file to Confluence storage HTML (pandoc works well), then create or update the matching Confluence page with atlassian-cli confluence page create/update. Wrap that in a script and run it in CI on every merge to your docs branch. Because the script finds pages by title and overwrites the body in place, repeated runs update the same pages instead of creating duplicates.

Can I sync markdown docs to Confluence from CI?

Yes. In CI you pass the API token through an environment variable (ATLASSIAN_API_TOKEN) instead of committing credentials, and configure a lightweight profile with atlassian-cli auth login. A GitHub Actions or Bitbucket Pipelines job that triggers on changes to docs/** runs your publish script so Confluence always reflects what is in version control.

Does the MkDocs to Confluence sync create duplicate pages?

No, if you look up the page by title before writing. The pipeline runs a CQL search for space = KEY AND title = "...", updates the page when it exists, and creates it only when it does not. That makes republishing idempotent: a file whose content has not changed produces the same storage HTML, so re-running is a safe no-op.

Do I need a plugin to convert Markdown to Confluence?

No MkDocs plugin is required. atlassian-cli sends Confluence storage-format HTML through the page create and update commands, so you convert Markdown to HTML with any tool you already trust (pandoc is a common choice) and pass the result to --body. This keeps the conversion step transparent and swappable rather than hidden inside a build plugin.

Should I use atlassian-cli or Atlassian's acli for this?

atlassian-cli is an independent, community open-source project, not Atlassian's official CLI (acli), and is not affiliated with Atlassian. Use the official acli if you want first-party vendor support. Use atlassian-cli if you want a single free Rust binary that covers Confluence, Jira, Bitbucket, and JSM and scripts cleanly into a docs pipeline.

Related Resources