Architecture¶
Project Structure¶
.
├── src/
│ └── ccfm_convert/
│ ├── adf/ # Markdown → ADF converter (pure, no I/O)
│ │ ├── nodes.py # ADF node constructor functions
│ │ ├── inline.py # Inline markdown parsing
│ │ ├── blocks.py # Block markdown parsing
│ │ └── converter.py # Orchestration; convert() entry point
│ ├── deploy/ # Confluence API and deployment logic
│ │ ├── api.py # ConfluenceAPI class (REST v2 + v1 for attachments/properties)
│ │ ├── frontmatter.py # YAML frontmatter parsing
│ │ ├── orchestration.py # deploy_page(), deploy_tree(), destroy_page()
│ │ └── transforms.py # CI banner, page link resolution, attachment media nodes
│ ├── state/ # Remote state and locking
│ │ ├── backend.py # StateBackend protocol + ConfluenceBackend
│ │ ├── manager.py # StateManager — filepath → page_id mapping, content hashing
│ │ ├── lock.py # LockManager — Terraform-style deploy locking
│ │ └── init.py # init_remote_state() — one-time space setup
│ ├── config/ # Project config file loader
│ │ └── loader.py # ccfm.yaml loader with ${ENV_VAR} interpolation
│ ├── plan/ # Plan/diff mode
│ │ └── planner.py # compute_plan(), DeployPlan — terraform-style diff output
│ └── main.py # CLI entry point (argparse subcommands)
├── tests/
│ ├── smoke/ # End-to-end smoke tests (real Confluence space)
│ │ ├── conftest.py # Credentials, cleanup hook, ccfm_run fixture
│ │ ├── docs/ # Fixture markdown files deployed during smoke tests
│ │ └── test_*.py # Smoke test modules
│ └── test_*.py # Unit tests (100% coverage, all mocked)
├── ccfm.yaml # Optional project config (credentials, space, docs_root)
├── CCFM.md # Complete CCFM syntax and ADF mapping reference
├── requirements.txt # Runtime dependencies
├── requirements-test.txt # Development and test dependencies
└── pyproject.toml # Toolchain configuration (black, ruff, pytest, coverage)
src/ccfm_convert/adf/ — Pure Conversion¶
No I/O, no network calls. Entry point: convert(markdown: str) -> dict.
nodes.py— ADF node constructor functions (doc(),heading(),paragraph(), etc.)inline.py— Inline parsing: bold, italic, code, links, emoji, status badges, datesblocks.py— Block parsing: tables, lists (bullet/ordered/task), panels, expands, blockquotesconverter.py— Orchestrates the conversion; calls into blocks and inline parsers
src/ccfm_convert/deploy/ — Confluence API Interaction¶
api.py—ConfluenceAPIclass wrapping REST API v2 (v1 for attachment upload — Confluence v2 lacks a POST attachment endpoint, tracked at CONFCLOUD-77196)frontmatter.py—parse_frontmatter(content) -> (metadata, markdown)strips and parses YAMLorchestration.py—deploy_page(),deploy_tree(),ensure_page_hierarchy()coordinate the full deploy flowtransforms.py— Post-conversion ADF mutations: CI banner injection, internal page link resolution, attachment media node rewriting
src/ccfm_convert/state/ — Remote State and Locking¶
backend.py—StateBackendprotocol withload()/save()methods;ConfluenceBackendstores state as a JSON attachment on the management pagemanager.py—StateManagertracks deployed pages, computes content hashes, detects orphaned pages. Backend-agnostic viaStateBackendprotocollock.py—LockManagerimplements Terraform-style locking using Confluence content properties with optimistic concurrency (409 Conflict = race condition)init.py—init_remote_state()creates the_ccfmmanagement infrastructure
Attachment Upload Flow¶
Confluence's v2 API lacks an attachment POST endpoint, so the deploy tool uses a multi-step workaround:
- Create or update the page (attachment media nodes are placeholders at this point)
- Upload attachments via v1 API (
/rest/api/content/{id}/child/attachment) - Fetch the Media Services
fileId(UUID) via v2 API GET — the v1 upload response does not include it - Re-update the page with correct ADF
medianodes containing the realfileIdandcollection