This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
markfluence is a Go CLI (cobra + net/http + goldmark + lipgloss) for publishing markdown to Confluence. It was originally a Python tool; the Go rewrite is now the sole implementation.
Tasks run through a Makefile (run make with no target for the annotated rule list).
make build— build./bin/markfluence(version stamped via ldflags)make test—go test ./...(also runs thego vetsubset)make lint— golangci-lint (installs the pinned v2.6.0 into./bin; enables the default set pluslllat 120)make vet/make fmtmake regen-regressions— regenerate the converter's golden outputs (go test ./internal/convert -run TestRegression -update)- Run a single package/test:
go test ./internal/frontmatter -run TestReadValues
Run make test && make lint && make vet before considering work done.
The CLI needs a base URL, a username, and an API token. Each resolves with the precedence flag > environment variable > .env file:
| Setting | Flag | Env / .env |
|---|---|---|
| base URL | --url |
CONFLUENCE_URL |
| username | --username |
CONFLUENCE_USERNAME |
| API token | (none) | CONFLUENCE_TOKEN |
markfluence reads a .env from the working directory itself (a minimal built-in parser — no shell expansion), or an explicit path via the persistent --env-file flag (a missing explicit path is an error; a missing default ./.env is not); .env.example is the template. The API token is deliberately never a command-line flag. internal/client.Resolve is the single place this is read and validated.
Commits must follow Conventional Commits 1.0.0: type(optional-scope): description, e.g. feat(convert): rewrite anchor links, fix(client): handle 404 on missing page. Common types: feat, fix, docs, refactor, test, chore, build.
Do not add AI attribution to commits or pull requests — no Co-Authored-By: Claude trailers, no "Generated with Claude Code" lines, no similar attribution.
Module github.com/mozilla/markfluence (go 1.25). main.go is a shim to cmd.Execute().
cmd/root.go— the cobra root:--url/--username/--debug/--no-colorpersistent flags, version frominternal/buildinfo, and registration of the four subcommands.Execute()prints cobra-generated errors (bad args/flags) but notui.ErrSilent, which marks a failure a command already reported.cmd/{update,create,fix,info}/— one package per command (each exportsCmd), orchestrating theinternalpackages andinternal/uioutput.createis two-phase and transactional (validate all, then create parents-first in topological order);fixis read-only on the server.internal/client—ConfluenceClientovernet/httpwith basic auth. Pages are Confluence v2; attachment writes and the user lookup are v1 (/wiki/rest/api/...). TypedHTTPError, per-attempt context timeouts, centralized retry/backoff insend(429 for any method honoringRetry-After, plus 502/503/504 and network errors for idempotent methods only; exponential backoff capped),SetContentPropertyretry-once on top (recovers a lost create-POST response),SyncAttachments(SHA-256-in-comment skip/update),_links.nextpagination.config.goholdsResolveand the.envreader.internal/convert— the converter (the crux).MdToConfluence(md *frontmatter.MarkdownFile, baseURL, spaceKey, version string) (*ConfluencePage, error). It parses with goldmark (GFM) and renders through a customstorageRendererregistered at priority 100 (below the default HTML=1000 and table=500 renderers) that emits Confluence storage format.shield.gorenames rawac:/ri:tags to colon-free sentinels around the goldmark step so pasted storage passes through;callouts.gois an AST transformer + blockquote renderer for GitHub alerts;images.go,links.go(sibling-file scans, GitHub/Confluence slugs, doc-link + anchor rewriting), andrenderer.go(code macros, text soft-break→space, images, links) do the rest. The<!-- confluence-toc -->and<!-- markfluence-version -->token substitutions happen insideMdToConfluence.internal/frontmatter— flat YAML frontmatter parse/quote/UpdateField, and theMarkdownFiletype (Parse/ParseFile, exportedFilename/Content/Frontmatter/Body, andTitle/PageID/Space/Parentaccessors that normalize missing/blank/"null").internal/pagewidth— thepage_widthWidthenum (narrow/wide/max, defaultmax),Declared, the vocab↔content-property maps,WidthFromProperties, andApply/Readagainst the client.internal/buildinfo—Version(set via ldflags),CommitDate(from thevcs.timebuild setting), andStamp.internal/ui— lipgloss output helpers (coloredHeader/Success/Warn/Error, errors to stderr,NO_COLOR/piped detection) and theErrSilentsentinel.
The design target is semantic (not byte-for-byte) equivalence to valid Confluence storage. Order matters in the pipeline: shield before parse / unshield after render; anchor rewriting must precede doc-link rewriting (link/anchor rewriting reads sibling .md files for their page_id/title and heading anchors). Go's regexp is RE2, so slug/link code avoids lazy quantifiers and uses \p{L} etc. for Unicode.
internal/convert/testdata/regression/<case>/ is a golden-file suite over MdToConfluence, one directory per case: a primary main.md (frontmatter allowed), optional sibling *.md and assets/ image stubs, an optional test.input (JSON: filename/base_url/space_key defaulting to main.md/https://wiki.example.net/ENG, plus an enforced files manifest), and a generated test.output golden. regression_test.go runs each case, redacts attachment paths to <ROOT>, and exact-matches the golden; regenerate with make regen-regressions.
Makefile (build/install/lint/vet/fmt/test/regen-regressions), .golangci.yml (default linters + lll at 120), and .goreleaser.yaml (darwin+linux × arm64+amd64, CGO_ENABLED=0, -s -w, version into internal/buildinfo.Version, tar.gz archives, a Homebrew cask). _plans/ holds the port's design/step history.
Each markdown file is one page. Frontmatter carries title, page_id, space (a key), parent (null / a .md path / a page id), and page_width. update requires a page_id (from frontmatter or --page-id) and errors without one; --title/--page-id override the frontmatter (single FILE only), --page-width overrides too (batch allowed), and update never writes back to files. It asserts page_width only when set via flag or frontmatter (otherwise the live width is left alone), and skips a file whose mtime predates the page's last version unless --force. create takes --title/--page-width overrides (--title single-FILE only; --page-width batch-ok, default max) and, unless --no-persist is given, writes title/space/parent/page_id/page_width back after creating; fix reconciles all of these (plus page_width) from the live page. Commands process multiple files (except info, single-arg) and exit non-zero if any fail.