Skip to content

Commit 79ea1ef

Browse files
committed
Add Confluence Cloud support alongside Data Center
- CloudConfluenceClient: v2 API for page get/update; inherited v1 endpoint for attachment upload (v2 has none). Auto-detected from *.atlassian.net hostnames; instance_type config field overrides for custom domains. - Cloud auth: CONFLUENCE_EMAIL + CONFLUENCE_TOKEN (API token) basic auth, not gated behind --unsafe-auth. /wiki is appended to bare site URLs and written back to config so attachment URL matching keeps working. - Attachment content reads use the REST download endpoint on Cloud; the legacy /download/attachments path 401s for API-token auth there. - Credentials may come from the environment without a .csync.env file; csync injects FIFO-served env files (e.g. 1Password) via --env-file since Docker VM file sharing cannot pass host FIFOs. - Portable BSD/GNU sed in the release Makefile. - README/CLAUDE.md/examples updated; note on IPv6-related image build failures and the Docker Desktop IPv4-only fix.
1 parent f5c1dac commit 79ea1ef

9 files changed

Lines changed: 460 additions & 45 deletions

File tree

.csync.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
# Confluence credentials
2+
3+
# --- Data Center ---
24
# Personal Access Token (Profile → Personal Access Tokens in Confluence DC)
35
CONFLUENCE_TOKEN=
46

5-
# Or basic auth — less preferred, PAT is better
7+
# Or basic auth — less preferred, PAT is better (requires --unsafe-auth)
68
# CONFLUENCE_USERNAME=user@example.com
79
# CONFLUENCE_PASSWORD=
10+
11+
# --- Cloud (yoursite.atlassian.net) ---
12+
# API token from https://id.atlassian.com/manage-profile/security/api-tokens
13+
# plus your Atlassian account email. No --unsafe-auth needed.
14+
# CONFLUENCE_TOKEN=
15+
# CONFLUENCE_EMAIL=you@example.com

.py-conf-sync.config.example.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
confluence_url: https://confluence.example.com
66
jira_url: https://jira.example.com # optional — enables Jira macro links on pull
77

8+
# For Atlassian Cloud, use your site URL instead (auto-detected from *.atlassian.net):
9+
# confluence_url: https://yoursite.atlassian.net/wiki
10+
# jira_url: https://yoursite.atlassian.net
11+
# instance_type: cloud # optional override — only needed for Cloud behind a custom domain
12+
813
pages:
914
- page_id: "123456"
1015
file_path: docs/onboarding.md

CLAUDE.md

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ This file provides context for Claude Code when working in this project.
55

66
## What this project does
77

8-
`py_conf_sync.py` is a single-file CLI tool that syncs Confluence Data Center pages
9-
with Markdown files in a git repository. It pulls pages down from Confluence (converting
8+
`py_conf_sync.py` is a single-file CLI tool that syncs Confluence pages (Data Center
9+
or Cloud) with Markdown files in a git repository. It pulls pages down from Confluence (converting
1010
storage format to Markdown), allows local editing, then pushes changes back (converting
1111
Markdown back to Confluence storage format).
1212

@@ -48,10 +48,20 @@ Dependabot regenerates them automatically on its weekly dep-bump PRs.
4848

4949
Everything lives in `py_conf_sync.py`. The main sections are:
5050

51-
- **`ConfluenceClient`** — thin `requests` wrapper around the Confluence REST API.
52-
Three methods: `get_page()`, `update_page()`, and `upload_attachment()`. Auth is
51+
- **`ConfluenceClient`** — thin `requests` wrapper around the Confluence DC REST API
52+
(v1). Three methods: `get_page()`, `update_page()`, and `upload_attachment()`. Auth is
5353
either a PAT Bearer token or basic auth, loaded from `.env` via `python-dotenv`.
5454

55+
- **`CloudConfluenceClient`** — subclass for Atlassian Cloud. Overrides `get_page()`
56+
and `update_page()` to use the v2 API (`/api/v2/pages/{id}`); inherits
57+
`upload_attachment()` because v2 has no attachment upload endpoint (Cloud still uses
58+
the v1 path). Auth is basic auth with `CONFLUENCE_EMAIL` + `CONFLUENCE_TOKEN`
59+
(Atlassian API token) — no `--unsafe-auth` needed. Selected by `_is_cloud()`:
60+
explicit `instance_type` config field wins, else hostname ends with `.atlassian.net`.
61+
`_get_client()` appends `/wiki` to the base URL if missing and writes it back to
62+
`config["confluence_url"]` (conversion code builds/matches attachment download URLs
63+
from that value).
64+
5565
- **`storage_to_markdown()`** — converts Confluence storage format (XHTML + `ac:*`/`ri:*`
5666
macros) to Markdown. Named macro patterns are applied in order before markdownify runs.
5767
Many macros have full round-trip support; unknown macros are stripped by `_MACRO_RE`.
@@ -113,6 +123,12 @@ pages:
113123
`page_id` is always treated as a string. `file_path` is relative to wherever the script
114124
is run from (intended to be the repo root).
115125

126+
For Atlassian Cloud, set `confluence_url: https://yoursite.atlassian.net/wiki` (the
127+
`/wiki` suffix is appended automatically if omitted) and `jira_url` to the bare site URL.
128+
Cloud is auto-detected from `*.atlassian.net` hostnames; the optional
129+
`instance_type: cloud | datacenter` field overrides detection (needed for Cloud behind
130+
a custom domain).
131+
116132
**`img_dir`**: On pull, if an attachment image's filename exists as `{img_dir}/{filename}`
117133
locally, the Markdown image reference uses the local relative path instead of the Confluence
118134
download URL. On push, local image references are uploaded as Confluence attachments before
@@ -121,19 +137,27 @@ converting to storage format.
121137
## Credentials (`.csync.env`)
122138

123139
```
140+
# Data Center
124141
CONFLUENCE_TOKEN=<personal access token>
125142
# or (requires --unsafe-auth flag at runtime)
126143
CONFLUENCE_USERNAME=user@example.com
127144
CONFLUENCE_PASSWORD=<password>
145+
146+
# Cloud — API token from id.atlassian.com + account email; no --unsafe-auth needed
147+
CONFLUENCE_TOKEN=<api token>
148+
CONFLUENCE_EMAIL=user@example.com
128149
```
129150
130151
Credentials are **never created by `init`** — users set them up manually by copying
131-
`.csync.env.example` to `~/.csync.env` and filling in their PAT.
152+
`.csync.env.example` to `~/.csync.env` and filling in their token.
132153
133154
The credential file is located by searching in order: `~/.csync.env` (preferred), then
134155
the current working directory, then the script's own directory.
135156
136-
Basic auth is disabled by default and requires `--unsafe-auth` to activate.
157+
Password basic auth (DC only) is disabled by default and requires `--unsafe-auth` to
158+
activate. Cloud's email + API token basic auth is not gated — it is Atlassian's
159+
recommended method for scripts. Cloud with a missing `CONFLUENCE_EMAIL` or
160+
`CONFLUENCE_TOKEN` is a hard error.
137161
138162
## Known limitations / gotchas
139163
@@ -180,10 +204,25 @@ Basic auth is disabled by default and requires `--unsafe-auth` to activate.
180204
181205
## Confluence API reference
182206
207+
Data Center (v1, base `{confluence_url}/rest/api`):
208+
183209
- Get page: `GET /rest/api/content/{id}?expand=body.storage,version,title`
184210
- Update page: `PUT /rest/api/content/{id}` with JSON body (see `update_page()`)
185211
- Upload attachment: `POST /rest/api/content/{id}/child/attachment`
186212
- Space content: `GET /rest/api/space/{KEY}/content`
187213
188-
All endpoints are Confluence DC REST API v1 (`/rest/api/`). This tool does not use the
189-
v2 API (`/api/v2/`) introduced in newer DC versions.
214+
Cloud (base `{confluence_url}` = `https://site.atlassian.net/wiki`):
215+
216+
- Get page: `GET /api/v2/pages/{id}?body-format=storage` — response carries `title`,
217+
`version.number`, and `body.storage.value` in the same shapes the v1 consumer code
218+
reads, so `cmd_pull`/`cmd_push` are client-agnostic.
219+
- Update page: `PUT /api/v2/pages/{id}` with `{id, status: "current", title, body, version}`
220+
- Upload attachment: `POST /rest/api/content/{id}/child/attachment` — still v1; the v2
221+
API has no attachment upload endpoint.
222+
- Download attachment content: `GET /rest/api/content/{id}/child/attachment/{attId}/download`
223+
(redirects to a signed media URL). The legacy `/download/attachments/{id}/{file}` path
224+
returns 401 for API-token auth on Cloud — it only accepts browser-session cookies —
225+
which is why `download_attachment_text()` is overridden in the Cloud client.
226+
227+
Atlassian is deprecating Cloud v1 content endpoints on a rolling timeline, which is why
228+
Cloud page reads/writes use v2 while DC stays on v1.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ major:
2424
$(call _release,$(NEW_VERSION))
2525

2626
define _release
27-
sed -i 's/__version__ = "$(CURRENT_VERSION)"/__version__ = "$(1)"/' py_conf_sync.py
27+
sed -i.bak 's/__version__ = "$(CURRENT_VERSION)"/__version__ = "$(1)"/' py_conf_sync.py && rm py_conf_sync.py.bak
2828
git add py_conf_sync.py
2929
git commit -m "chore: bump version to $(1)"
3030
git tag v$(1)

README.md

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ Or rebuild explicitly without running a command:
8585
./csync --rebuild status
8686
```
8787

88+
> **Image build fails downloading Chromium?** If `playwright install chromium` dies with
89+
> `Client network socket disconnected before secure TLS connection was established` on
90+
> every mirror, your network is advertising IPv6 that doesn't actually route (common on
91+
> home networks; frequently reported from Docker Desktop on Apple Silicon Macs, but it is
92+
> a network issue, not an architecture one — playwright's Node-based downloader tries IPv6
93+
> first and never falls back to IPv4). Fix: in Docker Desktop go to
94+
> **Settings → Resources → Network** and set **Default networking mode** to **IPv4 only**,
95+
> then Apply & restart and rebuild.
96+
8897
## Commands
8998

9099
```
@@ -219,9 +228,9 @@ Confluence will render correctly, and pulling that page back produces the same M
219228
### Mermaid diagrams
220229

221230
Fenced ` ```mermaid ` blocks are automatically rendered to PNG images and uploaded as
222-
Confluence attachments on push. Confluence Data Center does not natively render Mermaid,
223-
so this gives you diagrams in git-tracked Markdown that display correctly on the published
224-
page — without any Confluence app or plugin.
231+
Confluence attachments on push. Confluence (Data Center and Cloud alike) does not natively
232+
render Mermaid, so this gives you diagrams in git-tracked Markdown that display correctly
233+
on the published page — without any Confluence app or plugin.
225234

226235
**On push:** each ` ```mermaid ` block is rendered to a retina-quality PNG by headless
227236
Chromium (bundled in the Docker image via Playwright and a bundled `mermaid.min.js` — no
@@ -310,17 +319,38 @@ img_dir: assets/images
310319

311320
## Credentials
312321

322+
Credentials are **never created automatically** by `csync init`. Set them up by hand:
323+
324+
```bash
325+
cp .csync.env.example ~/.csync.env
326+
# edit ~/.csync.env and fill in your credentials (see below)
327+
```
328+
329+
### Data Center
330+
313331
Use a Personal Access Token (PAT) — it's scoped, revocable, and doesn't expose your password:
314332

315333
`Confluence → Profile → Personal Access Tokens → Create token`
316334

317-
Credentials are **never created automatically** by `csync init`. Set them up by hand:
335+
```
336+
CONFLUENCE_TOKEN=<your PAT>
337+
```
318338
319-
```bash
320-
cp .csync.env.example ~/.csync.env
321-
# edit ~/.csync.env and fill in CONFLUENCE_TOKEN
339+
### Cloud
340+
341+
Use an Atlassian API token together with your Atlassian account email:
342+
343+
`https://id.atlassian.com/manage-profile/security/api-tokens → Create API token`
344+
345+
```
346+
CONFLUENCE_TOKEN=<your API token>
347+
CONFLUENCE_EMAIL=you@example.com
322348
```
323349
350+
Cloud authenticates with HTTP basic auth (email + API token) — this is Atlassian's
351+
recommended method for scripts and does **not** require `--unsafe-auth`, which only
352+
gates password-based basic auth on Data Center.
353+
324354
### Credential file location
325355
326356
The tool searches for `.csync.env` in this order and uses the first file found:
@@ -344,13 +374,23 @@ without this flag and the command will exit with an error. PAT is strongly prefe
344374

345375
## Version support
346376

347-
This tool targets **Confluence Data Center** and uses the v1 REST API
348-
(`/rest/api/content/`). It has been tested against **DC 9.2.x**.
377+
The tool supports both **Confluence Data Center** and **Confluence Cloud**.
378+
The instance type is auto-detected from the `confluence_url` hostname
379+
(`*.atlassian.net` → Cloud); set `instance_type: cloud` or
380+
`instance_type: datacenter` in the config to override (e.g. for a Cloud site
381+
behind a custom domain).
349382

350-
The v1 API has been stable since Confluence 6.x and remains fully supported
351-
in the 9.x line, so any reasonably modern DC instance should work without
352-
changes.
353-
354-
**Confluence Cloud is not currently supported.** Cloud uses a different base
355-
URL structure and OAuth-based authentication model. Cloud support is planned
356-
once the relevant infrastructure migration is complete.
383+
| | Data Center | Cloud |
384+
|---|---|---|
385+
| `confluence_url` | `https://confluence.example.com` | `https://yoursite.atlassian.net/wiki` (`/wiki` is appended automatically if omitted) |
386+
| Auth | Bearer PAT (`CONFLUENCE_TOKEN`) | Basic auth: `CONFLUENCE_EMAIL` + `CONFLUENCE_TOKEN` (API token) |
387+
| Page read/write | v1 REST API (`/rest/api/content/`) | v2 REST API (`/api/v2/pages/`) |
388+
| Attachment upload | v1 REST API | v1 REST API (v2 has no upload endpoint) |
389+
390+
Data Center has been tested against **DC 9.2.x**. The v1 API has been stable
391+
since Confluence 6.x and remains fully supported in the 9.x line, so any
392+
reasonably modern DC instance should work without changes.
393+
394+
On Cloud, page reads and writes use the v2 API (the v1 content endpoints are
395+
being deprecated by Atlassian); attachment upload still uses the v1 endpoint
396+
because v2 does not provide one.

csync

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ else
2222
IMAGE="$LOCAL_IMAGE"
2323
fi
2424

25-
# Mount ~/.csync.env into the container at the same path so _find_env_file() can locate it
25+
# Mount ~/.csync.env into the container at the same path so _find_env_file() can locate it.
26+
# If it's a FIFO (e.g. a 1Password-served environment), Docker's VM file sharing cannot
27+
# pass it through — read it once on the host and inject the values as environment
28+
# variables instead (py_conf_sync accepts credentials from the environment). The content
29+
# is passed via process substitution on the exec line below: the fd only stays open
30+
# through exec if the substitution is part of that command, not a prior array assignment.
2631
home_env_args=()
27-
if [[ -f "$HOME/.csync.env" ]]; then
32+
env_file_content=""
33+
if [[ -p "$HOME/.csync.env" ]]; then
34+
env_file_content="$(cat "$HOME/.csync.env")"
35+
elif [[ -f "$HOME/.csync.env" ]]; then
2836
home_env_args+=(-v "$HOME/.csync.env:$HOME/.csync.env" -e "HOME=$HOME")
2937
fi
3038

@@ -36,11 +44,13 @@ for arg in "$@"; do
3644
fi
3745
done
3846

47+
# An empty env-file is a harmless no-op, so the substitution is unconditional.
3948
exec docker run --rm \
4049
--network=host \
4150
--user "$(id -u):$(id -g)" \
4251
-v "$(pwd):/workspace" \
4352
-w /workspace \
53+
--env-file <(printf '%s\n' "$env_file_content") \
4454
"${home_env_args[@]+"${home_env_args[@]}"}" \
4555
"${extra_mounts[@]+"${extra_mounts[@]}"}" \
4656
"$IMAGE" \

0 commit comments

Comments
 (0)