Confluence publisher
Skill eugenelim/agent-ready-repo/packs/atlassian/.apm/skills/confluence-publisher
Publish content to a Confluence page (Atlassian Cloud or Server/Data Center) by creating a new page or updating an existing one. Accepts Markdown (default), raw Confluence storage XHTML, or plain text. Resolves the target by page ID, URL, frontmatter `confluence_id`, or space + title lookup. Handles optimistic-locking 409s with one retry. Use when the user wants to push a report, design doc, or other content to a Confluence page they have access to.From its SKILL.md
npx -y skills add eugenelim/agent-ready-repo --skill confluence-publisherAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.7 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it
Confluence Publisher
Publish a single page to Confluence — create or update — from Markdown,
storage XHTML, or plain text. Companion to confluence-crawler: same
credentials namespace, same flavor support, opposite direction.
Instructions
You are a Confluence publishing agent. Authentication, REST mechanics,
optimistic-locking retries, and the Markdown→storage conversion live in
scripts/. Do not re-implement any of that; invoke the script with the
right flags and report the result.
Flavor support
Same as the crawler:
- Atlassian Cloud (
*.atlassian.net) — Basic auth with email + API token. Base URL must include/wiki. - Confluence Server / Data Center — Bearer auth with a Personal Access Token.
Flavor is auto-detected from the base URL; override via
CONFLUENCE_FLAVOR=cloud|server if needed.
Configuration location
Credentials are resolved by the build-projected credentials_shim.load_credentials
through Tier 1 (env) → Tier 2 (OS keyring) → Tier 3 dotfile. The
dotfile lives at ~/.agentbundle/credentials.env. The declared schema
is at references/creds-schema.toml and shares the confluence
namespace with confluence-crawler — if either skill has been
configured, this one works.
| Key | Required | Notes |
|---|---|---|
CONFLUENCE_BASE_URL | yes | Cloud: https://<site>.atlassian.net/wiki. Server: https://confluence.corp.example.com. |
CONFLUENCE_API_TOKEN | yes | Cloud API token or Server PAT. |
CONFLUENCE_EMAIL | Cloud only | Atlassian account email. |
CONFLUENCE_FLAVOR | no | cloud or server. Auto-detected from URL host. |
Populate any tier by running credential-setup skill.
Security rules (non-negotiable)
- Secrets live only in
~/.agentbundle/credentials.env(mode 0600 on POSIX; DACL-restricted on Windows), the OS keyring, or process environment variables. Never read that file, print it, or echo the token. - Never put the token on the command line. The primitive
refuses flags like
--token/--api-token/--bearer/--pat/--passwordand exits — do not work around it. - If
--checkreports missing or invalid creds, tell the user to runcredential-setupskill themselves. It's interactive — do not run it for them.
Step 1: Verify the environment
python -m pip install -r requirements.txt
python scripts/publish_page.py --check
- Exit code 0 → authenticated, proceed.
- Exit code 2 → the user must act (credentials missing/invalid/expired). Tell
the user to run
credential-setupskill themselves (interactive — they run it, not you). Stop here. - Any other non-zero → see When a request fails.
When a request fails
The CLI uses a banded exit-code contract; read the stderr message for the specific cause, then act on the band:
| Exit | Band | What to do |
|---|---|---|
| 0 | success | proceed |
| 1 | functional error — server 5xx, transport, keychain hard-fail, unexpected | surface the message to the user; don't loop or retry blindly |
| 2 | user must act — credentials (401/403), a publish conflict, or a target/input the user must fix | follow the NEED-INPUT: message: re-auth via credential-setup, resolve the conflict, or fix the target — then retry |
Tier2HardFailError (OS keyring unavailable) or an unprojected shim surface as
exit 1 with a message naming the cause.
Step 2: Decide how to identify the target page
In order of robustness — use whichever the user gave:
- By page ID or URL (preferred).
--page-id 12345or--url https://acme.atlassian.net/wiki/spaces/ENG/pages/12345/Some+Title. The page ID is parsed out of the URL. Idempotent. - By frontmatter — if the input file was produced by
confluence-crawlerit carriesconfluence_id(and optionallyversion,space_key) in YAML frontmatter.--from-frontmatterreads it. This is the round-trip case (crawl → edit → publish back). - By space + title —
--space ENG --title "My Page" [--parent-id 999]. Looks up by title; if found, updates; if not, creates. Title lookups are fragile (titles change); prefer modes 1 and 2 when an ID is available.
If none of these are supplied, the script exits 2 and asks which. Do not guess.
Step 3: Publish
Pick the form that matches the user's request:
# Update an existing page by ID, from Markdown:
python scripts/publish_page.py --page-id 12345 --input report.md
# Same, but from a Confluence URL:
python scripts/publish_page.py --url 'https://acme.atlassian.net/wiki/spaces/ENG/pages/12345/Foo' --input report.md
# Round-trip case — the markdown came from confluence-crawler:
python scripts/publish_page.py --from-frontmatter --input crawled/eng-handbook.md
# Lookup-then-upsert by title:
python scripts/publish_page.py --space ENG --title "Q2 Report" --parent-id 999 --input report.md
# Plain text body (one paragraph per line):
python scripts/publish_page.py --page-id 12345 --input - --input-format text # stdin
# Already-rendered storage XHTML:
python scripts/publish_page.py --page-id 12345 --input snippet.xhtml --input-format storage
# Dry-run — print what would be sent, do not call write APIs:
python scripts/publish_page.py --page-id 12345 --input report.md --dry-run
Flags:
| Flag | Meaning |
|---|---|
--check | Verify credentials and connectivity, then exit. |
--page-id ID | Update this page (preferred). |
--url URL | Parse page ID from a Confluence URL. |
--from-frontmatter | Read confluence_id (and optional version) from input file's YAML frontmatter. |
--space KEY --title TITLE | Lookup-then-upsert by title. --parent-id ID optional. |
--input PATH or - | Source file (or - for stdin). Required. |
--input-format | markdown (default), storage, text. |
--version-comment TEXT | Recorded on the new page version. Defaults to a generic message. |
--attach PATH (repeatable) | Upload file as a page attachment; Markdown image refs whose target filename matches an attachment get rewritten to <ac:image>. |
--label LABEL (repeatable) | Apply labels after publish. |
--dry-run | Print the rendered storage XHTML and planned operation; no writes. |
--insecure | Disable TLS verification (Server/DC w/ self-signed). User-requested only. |
--verbose | Debug logging. |
Step 4: Interpret the output
On success the script prints:
OK: <create|update> page 12345 (version 8) — https://acme.atlassian.net/wiki/spaces/ENG/pages/12345/Foo
On a 409 (someone else edited between read and write) the script re-reads the page once and retries with the new version number. If the second attempt still conflicts, it surfaces the error — tell the user a human edited concurrently and ask them to re-run.
Behavior notes
- Update vs create.
--page-id/--urlalways updates; never creates a new page at a specific ID.--from-frontmatterupdates the page named in the frontmatter.--space + --titleupdates if a page with that title exists in the space, otherwise creates one (under--parent-idif given, otherwise at the space root). - Title. On update, the title is taken from
--titleif given, the first# H1of the markdown if not (markdown input only), and the existing page title as a final fallback. On create,--titleis required (or the first H1 if--input-format markdown). Heads-up: for markdown input, the H1 overrides the existing page title even on a routine re-publish — if you don't want a rename, pass--titleexplicitly or strip the H1. - Attachment ordering. On an update of an existing page,
attachments upload before the body update so
<ac:image>references resolve immediately. On a create, attachments upload after the page is created (the page must exist first); the body's image refs render broken for the subsecond gap between create and the attachment uploads. Failure semantics are not symmetric: if an update's attachment uploads partly succeed and then raise, the body update is skipped — the page still shows the prior body but now has the new attachments orphaned on it; re-running is idempotent because Confluence dedupes attachment uploads by filename. On create, an attachment failure after a successful create leaves the page in place with the body referencing un-uploaded files. - Version comment. Recorded on the new version; helps reviewers see
why an agent edited. Default:
Published by confluence-publisher. - Markdown conversion. Renders CommonMark via
markdown-it-py, then post-processes to storage XHTML. The macro round-trip mirrorsconfluence-crawler's allowlist:info/warning/note/tip/panel/expand/code. Bold-leadin admonitions (**Note:** …,**Tip:** …,**Warning:** …,**Info:** …,**Important:** …) become the matching macro. Other Markdown is rendered as standard XHTML elements Confluence accepts. - Attachments.
--attachuploads each file as a page attachment. After upload, Markdown image references in the input whose target filename matches an attached filename are rewritten to<ac:image><ri:attachment ri:filename="…"/></ac:image>. Files not matched are uploaded anyway (the user might link them by other means). - Labels. Applied after the page write; failure to apply labels is reported but does not roll back the page write.
- Mermaid / PlantUML. Out of scope. Run the
mermaid-rendererskill first to pre-render fenced```mermaidblocks to PNGs, then pass those PNGs via--attachto this skill.
Don't
- Don't read
~/.agentbundle/credentials.envfrom skill body. - Don't print or log the token.
- Don't run
credential-setupskill non-interactively or pipe the token into it. - Don't write your own REST calls to Confluence — extend the scripts and surface the gap to the user if a flag is missing.
- Don't auto-resolve a title collision by appending suffixes — surface the ambiguity (the script does this) and ask which page to update.
- Don't assume
--insecureis safe to add by default; only when the user explicitly accepts it. - Don't pass
--forceto bypass a 409 — there is no such flag. Concurrent edits need human attention.
Edge cases
- Page moved between spaces between when the user got the URL and when you publish: the page ID still resolves; the publish targets the page in its current space.
- Title collision in lookup mode: if
GET /rest/api/content?spaceKey=X&title=Yreturns more than one result (rare but possible across page states), the script exits 2 with the list of IDs. Ask the user which to target via--page-id. - Frontmatter without
confluence_id: the script exits 2 and asks for one of the other identification flags. - Storage-format input with invalid XHTML: the API returns 400; the script surfaces the error message. Don't try to fix it client-side — ask the user.
- Network failure mid-publish. Reads (the version probe) are retried by the client. Writes are not — a failed PUT/POST means the page is in its prior state; re-run.
- Large pages. Confluence soft-caps storage at ~5 MB. Beyond that, break the content into linked sub-pages; this skill doesn't do that for you.
What ships with it: 10 files
58.7 KB alongside SKILL.md, 5 of them executable
evals/
- eval_queries.json1.5 KB
- evals.json5.0 KB
references/
- creds-schema.toml1.3 KB
scripts/
- _client.pyruns10.9 KB
- publish_page.pyruns21.5 KB
- _render.pyruns6.0 KB
- _target.pyruns4.5 KB
- test_exit_codes.pyruns6.9 KB
- manifest.json1.0 KB
- requirements.txt62 B