Skip to Content

What’s New

Release notes for acp2md, newest first. Conversion behaviour comes from the shared Climakers ADF engine, so most output changes apply to acs2md as well.


v1.0.12

Restore a page from a backup

page restore puts one page back into Confluence from a page get file (--file, with --attachments-dir) or from one page of an acs2md space backup (--backup-dir and --page-id). A page still current in the space gets its backed-up version restored, keeping its ID and history, or the backed-up body written as a new version; a trashed, archived or deleted page is created anew under its backed-up parent. Missing attachments, labels and content properties are added, and pictures in an ADF body are pointed at the restored files. A property the page has with another value is kept, since apps store state there; --overwrite-properties replaces it. The restored version’s message names the backup’s date, never a local path.

The plan is shown and confirmed first (--yes in scripts, --dry-run to only look). This is the first acp2md command that writes to Confluence, and it never deletes, trashes, archives or moves anything. page check compares a backed-up page with Confluence first: its state, version and body, and the attachments and labels it no longer has. See Restore Pages.

page get saves attachments

--attachments-dir <dir> downloads the page’s attachments into <dir>/<page-id>/ and describes them: inside the page JSON for atlas_doc_format, in <dir>/<page-id>.attachments.json for storage. Available on by-id, by-title and by-url, so a page can be backed up and restored with its files. See Saving attachments.

Every request has a deadline

The new http.client.request_timeout (default 10m, 0 disables it) bounds one request from sending it to reading the whole response, including its retries and Retry-After waits. Before, a connection that stalled hung the run until Ctrl-C. Existing config files do not have the key and get the default; doctor shows the value in a new HTTP request timeout row. The “Timed out waiting for the Confluence API” hint now names this setting instead of http.client.max_timeout, which caps the retry backoff and does not end a slow request. See Every request has a deadline.

Fixes

  • Commands no longer hang when stdin is left open. Every command that takes a page ID, title or URL read standard input to the end whenever it was not a terminal, even when the value was on the command line. From a CI runner, an ssh session or a tool that keeps stdin open, the command waited forever. Stdin is now read only when the argument is missing, so echo 123456 | acp2md page convert by-id still works.
  • Re-activating with a renewed key works. A renewed subscription keeps its key, but license activate --license-key <the same key> refused it with “license has expired” once the expiry had been seen locally. The command now asks the store and refreshes the installed license when the store reports the key active again.
  • --force does what its help promised with a key. license activate --license-key <new-key> --force replaces an active license, releasing its seat first; before, the flag only applied to license files. See Running activate again.
  • The command trees are regenerated from the binaries. The acp2md reference had fallen behind and was missing the flags of page convert, page count and page properties; the tree --short listing in this manual now shows page check and page restore too.

Under the hood

Writes go through a separate HTTP client that honours Retry-After on 429 only and never retries blindly, so a page create whose answer was lost is looked up instead of sent twice. The sync state and attachment downloads are published through a temporary file and a rename, so a download that fails part way leaves nothing behind. Built with Go 1.27.3.


v1.0.10

Knowing when there is something to install

acp2md doctor reports the newest release available to you in a new Latest version row, alongside the command that installs it; the check never fails the diagnosis and says “could not check” when offline. After an ordinary command, acp2md also asks the update service at most once a day, waits at most two seconds, and prints a single line to stderr when a newer release exists. The notice never appears in CI, under sudo, when stderr is not a terminal, or after update, license, doctor and config. Turn it off with acp2md config set update.notice false. See The daily new-version notice.

acp2md update also keeps working when the store’s daily download limit is reached — each file may be downloaded ten times a day — and beyond that fetches the same release from Climakers’ own release storage, once a day per installation.

Activation is steadier on Linux

License files are encrypted against a machine identifier, and on Linux that identifier now survives kernel and microcode updates and VM resizes. Licenses activated before this release keep working: a subscription license moves to the new identifier the first time it is opened, and a signed offline license keeps the identifier it was issued for. Nothing needs re-activating. macOS is unaffected.

Fixes

  • acp2md update sees a new release right away. Just after a release, update could report the previous version for several minutes because the update service answered from a cached copy. Installing, and update --check --force, now ask the service directly, so a release is offered within a minute of being published; a plain update --check may take up to two minutes.

v1.0.9

Fixes

  • license activate --license-key <file> says which flag takes a file. Passing a license file to --license-key answered “invalid license key format: expected UUID v4”; it now explains that a license file is activated with --license-file and prints the exact command.

v1.0.8

Nothing you use changes: the update engine moved to a shared Climakers module, which ajat now uses too, and update behaves exactly as before.


v1.0.7

Offline licenses update too

A local license issued by licensegen with a signing key carries a signature the update service verifies on its own, so offline customers get acp2md update like everyone else; their downloads come from Climakers’ release storage rather than the store. Local licenses issued before signing existed are refused with “this local license is not signed” and a hint to activate a reissued file. See Your subscription and older versions.


v1.0.6

Updates from inside the tool

acp2md update installs the newest release your license includes: it checks Climakers for a newer release, downloads it, verifies its SHA-256 checksum and replaces the running binary. On macOS it installs the signed, notarized package and runs as sudo acp2md update; on Linux it replaces the binary in place and never uses sudo. --check reports without changing anything, --version v1.1.0 installs a specific release (which is also how you go back), --yes skips the prompt and is required in CI, and --format json is for scripts. After a subscription ends, releases published before the end date stay installable. The command needs no Confluence credentials and does not count toward the license’s command limit. See Updating acp2md.

--format json output changed in two ways

JSON output is now produced by Go’s encoding/json/v2. Two visible differences, both deliberate:

  • an empty list or map prints as [] / {} instead of null, so a page with no labels is "labels": [] rather than "labels": null;
  • <, > and & in strings are no longer escaped, so a title like Q&A <draft> reads as written.

Key order, indentation and the trailing newline are unchanged, and sync state files remain readable by earlier versions. Scripts that test for null should test for an empty list instead.

Platform notes

  • macOS 13 (Ventura) or later is required. The Go toolchain this release is built with dropped support for earlier macOS releases.
  • SSL_CERT_FILE and SSL_CERT_DIR are honoured on macOS. When either is set, certificate roots are loaded from those paths instead of the platform store — the behaviour Linux always had. Corporate-proxy setups that set these variables for other tools will see them apply here too; GODEBUG=x509sslcertoverrideplatform=0 restores the previous behaviour.

v1.0.5

This release changed default Markdown output for pages containing panels, and for anyone using --include-metadata. Both actions below are no-ops if you never wrote the corresponding setting.

Action: check image.max_size_bytes

The default dropped to 2 MB, but a configuration written before this release almost certainly pins the old 10485760 — permissive enough to inline a page background and produce a file no editor will lay out. Configuration beats defaults, so it keeps winning:

acp2md config get image.max_size_bytes # 10485760 means the old value is pinned acp2md config set image.max_size_bytes 2097152

acs2md reads its own configuration, so run the same pair there if you use it.

Front matter carries display names and mentions link to people profiles. Where an export is published outside the organisation, --resolve-users=false keeps opaque account IDs instead.

Retry-After is honoured

When Confluence answers a throttled request with 429 or 503 and names a delay, the client now waits exactly that long instead of falling back to a locally guessed schedule. The server knows how long its window is; the client does not.

A delay longer than 60 seconds is surfaced rather than slept, so a command never looks hung, and waiting is interruptible.

The retry backoff defaults changed, and upgrading does not reach an existing install. They were raised from 10ms/100ms to 500ms/30s, but a config file materialises every default when it is created and configuration beats defaults. An older install keeps a 70 ms total budget across three attempts, which cannot outlast a real throttling window. Run acp2md doctor to check, then see Retries and rate limiting.

Images can be written as sidecar files

--image-sidecars saves each image to attachments/<page-id>/ beside the Markdown and links it relatively, instead of embedding a base64 data URI. The export stays self-contained either way — what changes is line length.

On one real page: 1,440,451 B and a 244,311-character line as base64, against 12,100 B and 379 characters as sidecars. Across a space it also deduplicates — an image reused on ten pages is stored once.

--obsidian turns this on, because base64 was the last thing in that preset that did not render in Obsidian.

Obsidian output

A full preset — callouts, heading anchors Obsidian resolves, and image sidecars — plus --wikilinks, --inline-tags, --panel-style and --media-dimensions. See Obsidian Vaults.

Panels render as GitHub alerts

> [!NOTE], > [!TIP], > [!WARNING], > [!CAUTION] replace the previous emoji-labelled blockquote. Alerts render on GitHub and Obsidian. Panel types with no alert equivalent keep the emoji form. --panel-style=emoji restores the old output.

Front matter is proper YAML

--include-metadata now emits deterministic, correctly quoted YAML that Obsidian reads as Properties and Jekyll, Hugo and Pandoc parse:

BeforeNow
Title, AuthorID, CreatedAt, ID, LastOwnerID, OwnerID, ParentID, ParentType, Position, SpaceID, Status, SubType, VersionAuthorID, VersionCreatedAt, VersionMessage, VersionMinorEdit, VersionNumbertitle, tags, author, created, updated, source, space, status, version, id, parent_id, owner_id

Dates are YYYY-MM-DD, tags comes from the page’s Confluence labels, source is the browser URL, and empty fields are omitted. The dropped fields were Confluence bookkeeping no Markdown consumer used.

Labels are fetched automatically

With front matter enabled, tags is populated without passing --include-labels. On page convert this rides along on the existing page request at no extra cost.

The table of contents previously emitted GitHub-style slug anchors regardless of target. --panel-style=obsidian (and therefore --obsidian) now emits anchors Obsidian resolves.

Every command reports how long it took

⏱️ Converted in 559ms

The figure covers the whole command, API calls and image downloads included. It is printed to stderr, never stdout, so acp2md page convert by-id 123 > page.md and pipelines stay clean.

A second Ctrl+C force-quits

The first interrupt cancels cleanly and lets in-flight work unwind. Previously any further Ctrl+C was swallowed and a run that looked stuck could not be stopped.

Fixes

  • --debug produced no output at all. It set the log level but left the destination at its default, which discards everything. It now logs to stderr unless --log-file says otherwise.
  • A broken log.level, log.format or unwritable log.file blocked every command — including the config commands that repair it. Those now fall back to working settings for the run and name the key to change.
  • A global flag placed before an exempt command made it demand a licence or validate the configuration. acp2md --log-file x.log doctor treated --log-file as the command name, so doctor — the command you run to diagnose licensing — refused to start on an unlicensed machine.
  • An invalid http.client.proxy is now an error rather than a warning-and-continue. Silently sending traffic direct when asked to route it is, on a corporate network, worth stopping for.

See also

Last updated on