Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/confluence/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,49 @@ else's content is far worse than reporting a failure that actually succeeded.
triggers on *any* error specifically so nothing depends on the answer; guessing a
status and getting it wrong would leave the recovery silently never firing.

## What bumps a page's version

A page's `version.number` tracks the **body and the title, and nothing else**.
Every other write markfluence makes to a page leaves it alone, which is what
makes it usable as a record of "has this page's content moved on?" (#149).

**Verified 2026-09-13** against a scratch page in a personal space, created at
version 1, through the gateway:

| write | route | page version |
|---|---|---|
| create a content property | `POST /wiki/api/v2/pages/{id}/properties` | **unchanged** |
| update a content property | `PUT /wiki/api/v2/pages/{id}/properties/{propId}` | **unchanged** |
| add a label | `POST /wiki/rest/api/content/{id}/label` | **unchanged** |
| remove a label | `DELETE /wiki/rest/api/content/{id}/label?name=` | **unchanged** |
| upload an attachment | `PUT /wiki/rest/api/content/{id}/child/attachment` | **unchanged** |
| change the body | `PUT /wiki/api/v2/pages/{id}` | **+1**, new `createdAt` |
| change only the title | `PUT /wiki/api/v2/pages/{id}` | **+1**, new `createdAt` |

`version.createdAt` moves with the number and stays put when it does, so the
timestamp is no more sensitive than the counter — adding a label does not
re-stamp the page.

A content property carries **its own independent version counter**: across the
create-then-update above it went 1 → 2 while the page stayed at 1. So a
property's version says nothing about the page's, in either direction.

Two consequences, both load-bearing.

**A publish advances the page by exactly one version.** `update` writes the body
and *then* applies width (two content properties), labels, and attachments, so a
naive reader might expect the live version to be several ahead of the one the
body PUT returned. It is not. Nothing after the body write touches it.

**The version in the `PUT /pages/{id}` response is the version a later GET
reports**, checked on both publishes above, so a caller recording what it
published does not need a re-read to learn the number.

**Not verified:** whether a *move* (the v1 `content/{id}/move` route, #10)
bumps the version, and whether restoring a previous version from the UI does.
Measured once, on one Cloud instance, through the gateway, with a personal
token.

## Scopes

**Derived 2026-08-20** from Atlassian's own OpenAPI documents, one lookup per
Expand Down
5 changes: 5 additions & 0 deletions docs/confluence/attachments.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,10 @@ The comment shows up under `extensions` too. Top-level keys are `ari`,
`base64EncodedAri`, `extensions`, `id`, `macroRenderedOutput`, `metadata`,
`status`, `title`, `type`, `version` — `title` being the stored attachment name.

An attachment has a version of its own, and **uploading one does not bump the
*page's* version** (verified 2026-09-13) — so a publish's attachment pass
leaves the page at the version its body write produced. The full table of what
does and does not bump it: [api.md](api.md#what-bumps-a-pages-version).

See [api.md](api.md) for how these collections paginate and how downloads
redirect.
12 changes: 12 additions & 0 deletions docs/confluence/labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ assert-exactly rule visibly, which is the better of the two available outcomes.
Found in review, after the end-to-end run below had already passed — that run
used `my:mine`, a name nothing collided with.

### A label write does not bump the page version

**Verified 2026-09-13**: adding a label and removing one both leave the page's
`version.number` *and* its `version.createdAt` exactly as they were, while a
body change bumps both. Details and the full table of what does and does not
bump it: [api.md](api.md#what-bumps-a-pages-version).

Two things follow. A label change is invisible to anything watching the page
version, so labels cannot be tracked that way. And `labels.Apply` running after
a publish does not advance the page past the version that publish produced,
which is what lets a caller record the version it published without re-reading.

### v2 is read-only for labels

| request | result |
Expand Down
6 changes: 6 additions & 0 deletions docs/confluence/page-width.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ content-appearance-draft -> ["max"]
Both set, both agreeing. This is why `pagewidth.Apply` writes the pair rather
than just the published one.

Writing either property leaves the page's `version.number` untouched, and each
property carries a version counter of its own — see
[api.md](api.md#what-bumps-a-pages-version). So a width change is invisible to
anything watching the page version, and `pagewidth.Apply` running after a
publish does not advance the page past the version that publish produced.

## The vocabulary

Authors write the UI's words in frontmatter; the property takes a different set
Expand Down
103 changes: 102 additions & 1 deletion docs/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,106 @@ make it a repository **variable** rather than a secret.

[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions

## The source of truth is external to Confluence

A CI workflow only makes sense when **the repository is the source of truth**
and the Confluence page is a published copy of it. If someone makes changes in
the Confluence UI, they will get stomped on when the CI workflow pushes a new
change.

### Use `--force`

```yaml
run: markfluence update --force docs/**/*.md
```

`update --force` prevents updates from failing in CI because someone
inadvertently edited the page in the Confluence UI. All edits are in the
Confluence history, so they can be recovered and applied to the repository
correctly.

### Publish only the files that changed

`paths:` on the trigger decides whether the *job* runs. It does not narrow the
glob, so `update --force docs/**/*.md` republishes every managed page on every
merge — one typo fix bumps the whole tree.

That is worth avoiding for reasons beyond tidiness:

- **Confluence notifies watchers on update.** Republishing 200 pages emails
everyone watching any of them, for a change to one. This is the cost that
gets a publishing bot switched off.
- **Page history stops being useful.** A run of identical new versions across
the tree makes "who changed this, and why" unanswerable in the UI.
- **It is N times the API calls**, on an instance whose rate limit is shared
with everyone else, and a correspondingly slow job.

Let git pick the files:

```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # both ends of the push range have to be present

- name: List changed docs
id: changed
run: |
base='${{ github.event.before }}'
# A new branch or a force-push reports an all-zero sha; fall back to
# the first commit so the run publishes the whole tree rather than
# failing on an unknown ref.
if [ -z "${base//0/}" ]; then
base="$(git rev-list --max-parents=0 HEAD | tail -1)"
fi
git diff --name-only --diff-filter=ACMRT "$base" '${{ github.sha }}' \
-- 'docs/**/*.md' > changed.txt
echo "count=$(wc -l < changed.txt)" >> "$GITHUB_OUTPUT"

- name: Publish
if: steps.changed.outputs.count != '0'
env:
# ... as above
run: xargs markfluence update --force < changed.txt
```

Two details that are easy to get wrong:

- **`--diff-filter=ACMRT`** (added, copied, modified, renamed, type-changed)
excludes deletions. Without it a deleted file lands in the list and fails the
run, since `update` cannot publish a file that is not there. Deleting a page
is deliberately not something a publish does.
- **The empty-list guard.** `update` with no FILE arguments is an error, so a
run where the diff comes back empty has to skip the step rather than invoke
it.

This is plain `git` rather than a marketplace changed-files action, which keeps
one less third-party dependency in the step that holds the Confluence token.

### Say so on the page

Since UI edits are going to be overwritten, the page should tell readers where
they can make edits. Put a callout at the top of the markdown — markfluence
converts a GitHub alert into a Confluence panel, so it renders as one:

```markdown
> [!NOTE]
> This page is published from [docs/deploy-runbook.md](https://github.com/ORG/REPO/blob/main/docs/deploy-runbook.md).
> Edits made here are overwritten on the next push. Open a pull request instead.
```

`NOTE`, `TIP`, `IMPORTANT`, `WARNING` and `CAUTION` are all supported and keep
GitHub's colours. Linking the source file gives a reader somewhere to go, which
is what turns "do not edit" into something actionable.

Consider restricting page permissions to the publishing account as well, if the
space allows it. A banner is a convention; permissions are a mechanism.

### If Confluence is the source of truth, do not run this workflow

If Confluence is the source of truth, you shouldn't be using a workflow to
update Confluence. markfluence has no way to discover changes that have been
made in the Confluence UI and has no mechanism for reconciling them.

## Workflow

```yaml
Expand Down Expand Up @@ -65,7 +165,8 @@ jobs:
# A variable, not a secret: the cloud ID is public. Omit it if you're
# using an unscoped personal token.
CONFLUENCE_CLOUD_ID: ${{ vars.CONFLUENCE_CLOUD_ID }}
run: markfluence update docs/**/*.md
# --force because the repository is the source of truth here; see above.
run: markfluence update --force docs/**/*.md
```

That step takes no per-file inputs, and that is the point: each file's page id
Expand Down