Skip to content

✨ Added Ghost updates within a major version (S7) - #386

Merged
acburdine merged 2 commits into
next-dockerfrom
claude/s7-ghost-update
Oct 10, 2026
Merged

acburdine merged 2 commits into
next-dockerfrom
claude/s7-ghost-update

Conversation

@acburdine

Copy link
Copy Markdown
Member

ref https://linear.app/ghost/issue/PLA-485/s7-host-driven-ghost-upgrades-ghost-docker-update

./ghost-docker update [--check] [<version> | latest] moves a site's Ghost within its major version, with a checked backup taken before Ghost's migrations run.

./ghost-docker update --check     # is there a newer Ghost 6?
./ghost-docker update             # the newest Ghost of the site's major
./ghost-docker update 6.68.0      # exactly that version

What it does

  • Target: latest (the default) pulls the site's floating major tag, <major>-<variant> (e.g. 6-next-alpine), so no registry API is needed. A named version maps to <version>-<variant>, and the image must report exactly that version.
  • Refuses another major, a downgrade, a tag that is not the version named, a GHOST_IMAGE_REF that is not the one the metadata records, a held lock, and a leftover update snapshot.
  • Order: pull while the site runs → snapshot .env + metadata → pause Ghost/ActivityPub → checked backup → write the pin and metadata together → resolve and validate → start and verify. Works for image installs and clones (only .env and metadata change).

Recovery

  • Failure before startup: files put back, paused writers resumed (restored).
  • Failure after startup was attempted: services stopped, data and the new pin kept. Unlike self-update, it does not put the old pin back, because switching Ghost back does not undo its migrations. The operator restores the named backup, or fixes the cause and runs docker compose up -d.

Shared executor for S8

The snapshot / pause / backup / startup-boundary / recovery logic moved out of self-update into manager/src/update.ts, which returns a structured outcome (done, restored, needs-operator) instead of printing. self-update runs on it with its messages unchanged (all 40 of its tests pass as before); the S8 supervisor can run the same code and record the outcome as a job state. src/legacy.ts still has its own copy and is left alone here.

Sites that follow Ghost's tag

A site can still leave GHOST_IMAGE_REF empty and upgrade with Compose alone (backup, docker compose pull ghost, up -d). Before this, restoring such a backup was refused once the tag had moved. Restore now pins GHOST_IMAGE_REF (and the metadata) to the registry digest Ghost ran, so restored data never starts on a newer Ghost than wrote it. Pinned sites are unchanged. If Ghost wasn't running when the backup was taken, there is no digest to pin and restore behaves as before.

Remaining for S7

Acceptance on a site with analytics (Tinybird sync rerun from the new image, deploy succeeds) needs a Tinybird login, so it is run by hand; the roadmap keeps only that.

Testing

  • Manager: format, lint, typecheck and 522 unit tests pass. New test/update.test.ts (16): each refusal, --check, nothing to do, backup and validation failures restored, a failed start after a migration (data and pin kept, then the leftover-snapshot refusal), services that cannot be stopped, a clone. backup.test.ts: floating-tag restore after the tag moves, and a pinned restore unchanged.
  • New required job Ghost update on Linux (tests/e2e/ghost-update.sh): installs Ghost 6.61.0 with an owner, a post and an image; an override healthy only on 6.61.0 makes the newest 6.x migrate and fail, checked to leave migrations and pin in place, then the named backup is restored; without it, a real update across the migrations keeps the post, image, sign-in and check; a downgrade is refused. Not run locally, so this PR is its first run.
  • shellcheck and shfmt clean.

🤖 Generated with Claude Code

ref https://linear.app/ghost/issue/PLA-485/s7-host-driven-ghost-upgrades-ghost-docker-update

Ghost could only change by editing GHOST_IMAGE_REF by hand, with no
backup before its migrations ran. `./ghost-docker update [version|latest]`
moves a site within its Ghost major: it pulls the target while the site
runs, pauses the writers, takes a checked backup, writes the pin and the
metadata together, then starts and verifies. latest is the site's
floating major tag, so no registry API is needed.

Once startup is attempted, the new Ghost may have migrated the data. The
update then stops the services and keeps the new pin, unlike
self-update, which puts its files back: switching Ghost back does not
undo its migrations. The operator restores the named backup or fixes the
cause and starts it.

Self-update and update now share one executor that returns its outcome
(done, restored, needs-operator), so the S8 supervisor can run the same
code and record the result instead of re-implementing recovery.

A site may still follow Ghost's tag with Compose alone. Restore now pins
such a backup to the digest Ghost ran, so after the tag has moved the
data never starts on a newer Ghost than wrote it.

- manager/src/update.ts: the shared update executor and its outcomes.
- manager/src/commands/update.ts: the update command, its refusals,
  --check, and how its failures read.
- manager/src/commands/self-update.ts: runs on the shared executor, with
  its messages unchanged.
- manager/src/restore.ts: pin a floating-tag backup to the Ghost it ran.
- manager/src/ghost.ts: tags of a site's own variant.
- manager/src/cli.ts, help, README.md: the command.
- manager/test/update.test.ts, backup.test.ts, site.ts, cli.test.ts:
  update's decisions and recovery, the floating restore, per-tag digests.
- tests/e2e/ghost-update.sh, .github/workflows/test.yml: a real update
  across Ghost's migrations, one that fails after startup and is
  restored, and a refused downgrade, as a required job.
- docs/install.md, architecture.md, configuration.md: the command, the
  executor and recovery boundary, and sites that follow the tag.
- docs/ghost-cli-replacement.md: S7 keeps only its analytics acceptance.
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 87560a9e-9799-497a-b333-a3c29f314cf6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Ghost refuses a password containing "ghost" as insecure, so setting up
the owner failed before the update was exercised.
@acburdine
acburdine merged commit 3f6f79d into next-docker Oct 10, 2026
15 checks passed
@acburdine
acburdine deleted the claude/s7-ghost-update branch October 10, 2026 01:30
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