Skip to content

v2.12.0 β€” auto-update that actually works - #50

Merged
rancur merged 2 commits into
mainfrom
feat/auto-update-that-works
Aug 9, 2026
Merged

v2.12.0 β€” auto-update that actually works#50
rancur merged 2 commits into
mainfrom
feat/auto-update-that-works

Conversation

@rancur

@rancur rancur commented Aug 9, 2026

Copy link
Copy Markdown
Owner

auto_update_enabled has existed for a while. It could never have worked β€” three independent reasons, each sufficient on its own.

1. The version check was a string compare

update_available = latest != current and latest > current

"2.11.0" > "2.9.0" is False β€” at index 2, "1" < "9". So every x.9 β†’ x.10+ upgrade was invisible. Worse, the reverse comparison was True, meaning the check could have offered a downgrade as an update.

Present in two places. routes/admin.py was fixed in 2.11.0; tasks/auto_update.py β€” the copy that actually schedules β€” was not. Fixed here, with 7 tests pinning the behaviour.

2. Nothing applied the update

scripts/auto-update.sh had to be installed in the host's crontab by hand. Nothing shipped it, so the signal file the worker wrote was never read by anything.

3. Even when triggered, it didn't update

That script ran docker compose up -d --build against the source already on disk β€” a rebuild of the same version.


waxflow-updater

A container cannot restart itself, so applying an update needs host-side Docker access. This is the only place WaxFlow asks for it, and it is deliberately constrained:

  • network_mode: none. It never downloads anything. The worker (network, no socket) decides the target version; the Docker daemon fetches image layers when asked over the socket. Nothing with host-root access talks to the internet.
  • Rollback. Health-checks the API afterwards and restores the previous tag if the new version doesn't come up. This runs unattended at 3am by default β€” an update that half-applies and is never noticed is worse than one that never ran.
  • Untrusted input. The target tag comes from the GitHub API and is refused unless it matches semver, so nothing unexpected reaches docker pull.

Verified against a live Docker daemon:

test result
malformed tag latest; rm -rf / refused before any docker call
valid tag, image doesn't exist fails cleanly, running stack untouched

Don't want to grant socket access? Delete the service β€” everything else keeps working, and the UI still tells you when a release is available.

Published images

Updating becomes a pull, not a rebuild. The rebuild path took ~25 minutes for the worker image on a Synology NAS and wedged the Docker daemon once; that is not acceptable for an unattended 3am job.

release-images.yml publishes ghcr.io/<owner>/waxflow-{api,worker,web} for amd64 + arm64 on each release, and refuses to publish if the tag disagrees with VERSION β€” an image whose baked /app/VERSION contradicts its tag would make the update check oscillate forever.

build: blocks remain, so docker compose up -d --build still works offline and for forks (WAXFLOW_REGISTRY).

Changed

  • auto_update_enabled defaults to 1 for new installs only. INSERT OR IGNORE means existing deployments keep whatever they already had β€” nobody gets unattended updates switched on behind their back.

Tests

17 pass (10 sync_gate + 7 version compare), including the exact regression and a guard that a downgrade is never offered.

πŸ€– Generated with Claude Code

https://claude.ai/code/session_01HmUiLHPmKoz215WAWV5eHe

rancur and others added 2 commits August 9, 2026 14:06
`auto_update_enabled` has existed for a while and could never have worked. Three
independent reasons, each sufficient on its own:

1. THE VERSION CHECK USED A STRING COMPARE. "2.11.0" > "2.9.0" is False (at
   index 2, "1" < "9"), so every x.9 -> x.10+ upgrade was invisible. The reverse
   comparison was True, so the check could have offered a DOWNGRADE as an update.
   Present in both routes/admin.py (fixed in 2.11.0) and tasks/auto_update.py
   (fixed here β€” the worker's copy, which is the one that actually schedules).

2. NOTHING APPLIED THE UPDATE. scripts/auto-update.sh had to be installed in the
   host crontab by hand and nothing shipped it, so the signal file the worker
   wrote was never read.

3. EVEN WHEN TRIGGERED, IT DID NOT UPDATE. That script ran `docker compose up -d
   --build` against the source already on disk β€” a rebuild of the same version.

## waxflow-updater

A container cannot restart itself, so applying an update needs host-side Docker
access. This is the only place WaxFlow asks for it, and it is constrained:

* network_mode: none. It never downloads anything. The worker (network, no
  socket) decides the target; the DAEMON fetches image layers when asked over
  the socket. Nothing with host-root access talks to the internet.
* Rollback. Health-checks the API afterwards and restores the previous tag if the
  new version does not come up. This runs unattended at 3am by default, and an
  update that half-applies and is never noticed is worse than one that never ran.
* The target tag comes from the GitHub API and is refused unless it matches
  semver, so nothing unexpected reaches `docker pull`.

Verified against a live daemon: a malformed tag ("latest; rm -rf /") is refused
before any docker call, and a valid-but-nonexistent tag fails with the running
stack untouched.

## Published images

Updating is now a pull, not a rebuild. The rebuild path took ~25 minutes for the
worker image on a Synology NAS and wedged the Docker daemon once β€” unacceptable
for an unattended job. release-images.yml publishes amd64+arm64 images to GHCR on
each release and refuses to publish if the tag disagrees with VERSION. `build:`
blocks remain, so `--build` still works offline and for forks.

auto_update_enabled now defaults to 1 for NEW installs; INSERT OR IGNORE means
existing deployments keep whatever they already had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmUiLHPmKoz215WAWV5eHe
Auto-update pulls prebuilt images instead of rebuilding from source. Without this
workflow the compose file would reference images that do not exist, so it is a
hard dependency of the updater, not an optimisation.

Builds linux/amd64 (Synology/Intel NAS) and linux/arm64 (Apple silicon, Pi) on
each published release, and refuses to publish when the tag disagrees with the
VERSION file β€” an image whose baked /app/VERSION contradicts its tag would make
the update check oscillate forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmUiLHPmKoz215WAWV5eHe
@rancur
rancur merged commit 19cf2d3 into main Aug 9, 2026
2 checks passed
@rancur
rancur deleted the feat/auto-update-that-works branch August 9, 2026 21:09
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