Skip to content

Record what bumps a page's version, and what a CI workflow should do about it - #155

Merged
willkg merged 3 commits into
mainfrom
page-version-semantics
Sep 13, 2026
Merged

Record what bumps a page's version, and what a CI workflow should do about it#155
willkg merged 3 commits into
mainfrom
page-version-semantics

Conversation

@willkg

@willkg willkg commented Sep 13, 2026

Copy link
Copy Markdown
Member

Documentation only. Three commits: one measured Confluence fact, and two things about running markfluence in CI that came out of the #149 design discussion.

What bumps a page's version

docs/confluence/api.md gains a section, because #149 needs the page version to mean "the content moved on" and nothing recorded whether it does.

It does. version.number tracks the body and the title and nothing else — verified against a scratch page in a personal space, created and then written to seven ways. Neither content-property write, neither label write, and an attachment upload all leave both the number and version.createdAt untouched, while a body or title change bumps both. A content property turns out to carry its own independent version counter: it went 1 → 2 across a create-then-update while the page stayed at 1.

Two consequences the design depends on. A publish advances the page by exactly one version, even though update writes width, labels and attachments after the body — so the version the body PUT returned is still the live one when the run finishes. And that returned version is what a later GET reports, so a caller recording what it published needs no re-read.

Cross-referenced from labels.md, page-width.md and attachments.md, since each is where somebody would look first for "does this write touch the page". Said explicitly what is not verified: whether the v1 move route (#10) bumps the version, whether a UI version restore does, and that this is one measurement on one Cloud instance through the gateway.

Scratch page purged.

Name the source of truth, and recommend --force

docs/github-actions.md did not say what arrangement a CI workflow assumes, and that assumption decides two things the page left unanswered.

A workflow only makes sense when the repository is the source of truth and the page is a published copy. In that model an edit made in the Confluence UI is drift, not work — the next push overwrites it. So the page now says that outright, and then says what follows:

Use --force, and for a sharper reason than "avoid the skip". Without it, publishing is non-deterministic rather than merely unprotected: git does not preserve mtimes, so a fresh checkout stamps every file with the clone time, and the skip compares that against Atlassian's clock — whether a given page publishes depends on wall-clock ordering between two unsynchronized clocks, and a page that should have synced can be silently reported skipped. The workflow example is updated to match.

Say so on the page. Since UI edits are going to be overwritten, readers should know before they spend an afternoon in the editor. A GitHub alert converts to a Confluence panel, so the mechanism already exists; linking the source file is what turns "do not edit" into something actionable. Page permissions are suggested as the mechanism a banner only approximates.

And an explicit "if Confluence is the source of truth, do not run this workflow"update has no record of which page version a local copy was made from, so two people publishing divergent copies silently resolves to whoever went last. That is #149, and until it is closed that arrangement stays manual.

Publish only the files that changed

paths: on the trigger gates whether the job runs; it does not narrow the glob. So update --force docs/**/*.md republishes every managed page on every merge, and one typo fix bumps the whole tree.

The reason that matters is not tidiness. Confluence notifies watchers on update, so republishing 200 pages emails everyone watching any of them — the cost that gets a publishing bot switched off. A run of identical new versions also makes "who changed this, and why" unanswerable in the UI, and it is N times the API calls on a rate limit shared with everyone else.

So the page now carries a git diff recipe, using plain git rather than a marketplace changed-files action, since this is the step holding the Confluence token. Two details it is easy to get wrong, both called out:

  • --diff-filter=ACMRT excludes deletions. Without it a deleted file lands in the list and fails the run, because update cannot publish a file that is not there — deleting a page is deliberately not something a publish does.
  • An empty-diff guard, because update with no FILE arguments is an error.

Plus fetch-depth: 0, since both ends of the push range have to be present, and a fallback for the all-zero github.event.before a new branch or force-push reports — falling back to publishing everything rather than failing on an unknown ref.

Notes

Measured for #149, which needs the page version to mean "the content moved
on". It does: version.number tracks the body and the title and nothing else.
Neither content property write, neither label write, and an attachment upload
all leave it -- and version.createdAt -- exactly as they were, while a body or
title change bumps both.

Two consequences the issue depends on. A publish advances the page by exactly
one version, even though update writes width, labels and attachments after the
body, so the version the body PUT returned is still the live one afterwards.
And that returned version is what a later GET reports, so a caller recording
what it published needs no re-read.

A content property also carries its own version counter -- it went 1 -> 2
across a create-then-update while the page stayed at 1 -- so a property's
version says nothing about the page's in either direction.

Cross-referenced from labels.md, page-width.md and attachments.md, since each
is a place someone would look first for "does this write touch the page".

Not verified, and said so: whether the v1 move route (#10) bumps the version,
and whether a UI version restore does.
A CI workflow only makes sense when the repository is the source of truth and
the page is a published copy, and saying so decides two things the page did not
answer.

--force, because without it publishing is non-deterministic rather than merely
unprotected. git does not preserve mtimes, so a fresh checkout stamps every
file with the clone time, and the skip compares that against Atlassian's clock
-- so whether a page publishes depends on wall-clock ordering between two
unsynchronized clocks, and one that should have synced can be reported skipped.
The mtime check exists to protect an edit made in Confluence, which is the
thing this arrangement deliberately does not want (#149).

And a callout on the page, since a UI edit here is drift that the next push
overwrites. A GitHub alert converts to a Confluence panel, so the mechanism
already exists; linking the source file is what makes "do not edit" actionable.
Permissions are suggested as the mechanism a banner only approximates.

Plus an explicit "do not run this in CI" for the inverse arrangement, where
Confluence is the source of truth: update has no record of which page version a
local copy came from, so two divergent publishes resolve to whoever went last.
paths: on the trigger gates whether the job runs, not what the glob matches, so
update --force docs/**/*.md republishes the whole tree on every merge. The
reason that matters is not tidiness: Confluence notifies watchers on update, so
one typo fix emails everyone watching any of 200 pages, and a run of identical
new versions makes the page history unanswerable.

The recipe uses plain git rather than a marketplace changed-files action, since
this is the step holding the Confluence token. Two details it gets wrong
easily: --diff-filter=ACMRT excludes deletions, which would otherwise land in
the list and fail the run because update cannot publish a file that is not
there; and an empty diff has to skip the step, because update with no FILE
arguments is an error.

The all-zero base sha from a new branch or a force-push falls back to the first
commit, so that case publishes everything rather than failing on an unknown ref.
@willkg
willkg merged commit 6b41ba1 into main Sep 13, 2026
1 check passed
@willkg
willkg deleted the page-version-semantics branch September 13, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant