Skip to content

In-app updates and self-hosted image builds - #8

Merged
kroqdotdev merged 2 commits into
mainfrom
feat/in-app-updates
Nov 9, 2026
Merged

kroqdotdev merged 2 commits into
mainfrom
feat/in-app-updates

Conversation

@kroqdotdev

@kroqdotdev kroqdotdev commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Admins get Settings > Updates: the installed version and build, newer releases from GitHub with their notes, and a one-click Update to x.y.z when the new updater sidecar runs. Without it the page shows the compose commands to run by hand.

Release images are now built on the self-hosted runner (amd64) and GitHub's native arm runner (arm64), then merged into multi-arch manifests. No more QEMU.

How the update works

browser ── POST /api/updates/apply ──▶ app ── POST /update ──▶ updater (sidecar, Docker socket)
                                                                   │ starts a helper container from its own image,
                                                                   │ with the project dir mounted at its host path
                                                                   ▼
                                                 pin STOWAGE_VERSION in .env → compose pull → compose up -d
                                                 (running services, updater last) → status in a shared volume
  • The app only forwards versions that are published releases newer than the running one.
  • The sidecar is opt-in (--profile updater, STOWAGE_UPDATER_TOKEN shared with the app) and only reachable on the compose network.
  • Relative volume paths such as ./pb_data resolve correctly because the helper mounts the project directory at the same path the host uses.
  • If the helper dies, the next status check reports the run as failed with the helper's last log lines.
  • The page polls the log, notices the app going down, then watches /api/health and reloads when a different version answers.

Release pipeline

  • build matrix: {app, pocketbase, caddy, updater} × {linux/amd64, linux/arm64}; amd64 on ${{ vars.CI_RUNNER || 'ubuntu-latest' }} (set to proxmox, runner gh-runner-stowage registered on Proxmox CT 105), arm64 on ubuntu-24.04-arm.
  • Pushes by digest, publish merges the digests per image into the semver/latest tags.
  • Layer caches at <image>:buildcache-linux-{amd64,arm64} in GHCR.
  • GIT_SHA build arg → STOWAGE_BUILD_SHA, shown next to the version.

Review fixes

Two independent reviews ran on the first commit; everything they found is fixed in the follow-up commit:

  • The helper now passes the operator's --env-files to compose (secrets interpolated the same way as on the host) and pins the version in each of them, after the pull succeeds. A failed pull changes nothing.
  • All services of the project are updated, not only the ones running at that moment, so a restarting PocketBase cannot be left on the old migrations.
  • A run that has not finished in 30 minutes is stopped and reported as failed; a failure while recreating the updater itself no longer marks the app update as failed.
  • Constant-time token compare, invalid JSON gets a 400, 500 bodies no longer echo internal paths, compose and env files outside the project directory are mounted too, mounts are declared as Mounts so paths with : work.
  • App side: 30 s timeout for starting an update and a refetch after any apply error, so a run that did start is followed; "Stop following"/Reload controls in every phase and a "taking longer than expected" message after 10 minutes; "Check now" says when the list is stale or when it was already checked in the last minute instead of claiming "up to date"; updater status is validated with zod; release links must point at github.com; the sidecar's 401 shows the token hint; Markdown links and bold in notes render as text.
  • Docs: STOWAGE_RELEASE_REPO documented, the hourly-check wording corrected, a note that a shell-exported STOWAGE_VERSION wins over .env.
  • Tests: route tests for both endpoints, view/apply tests, stale-list and reload/succeeded/stop-following component tests.

Test plan

  • pnpm typecheck, pnpm lint clean
  • pnpm test: 92 files, 428 tests; vitest --config vitest.pb.config.ts src/server/updates src/app/api/updates: 27 tests
  • Sidecar end to end against a scratch compose project with a local registry: 0.1.0 → 0.2.0 → 0.1.0, plus a run with PocketBase stopped beforehand and a run against a version that does not exist; .env pinned only on success, every service recreated, relative ./data mount kept its host path, --env-file passed through, updater recreated itself last, new instance reports the result, helper cleaned up
  • Dev app with STOWAGE_UPDATER_URL pointed at that sidecar: GET /api/updates returns the sidecar status, POST apply with a non-newer version is refused, anonymous gets 401
  • First tagged release through the new pipeline (needs a tag; the next release will exercise it)

🤖 Generated with Claude Code

Settings > Updates (admins): installed version and build sha, newer
releases from GitHub with their notes, and either an "Update to x.y.z"
button or the compose commands to run by hand.

- src/server/updates: tiny semver, GitHub Releases client (1h cache,
  stale on failure, refresh at most once a minute), updater client
- GET /api/updates, POST /api/updates/apply (admin), version in /api/health
- deploy/updater: sidecar with the Docker socket. The app asks it to move
  the compose project to a version; a helper container started from the
  same image pins STOWAGE_VERSION in .env, pulls, recreates the running
  services and finally the updater itself. State lives in a shared volume
  so the recreated updater reports the run; a vanished helper is reported
  as failed. Opt-in via `--profile updater` in docker-compose.release.yml.
- Release workflow: per-platform native builds (amd64 on the runner named
  by the CI_RUNNER variable, arm64 on ubuntu-24.04-arm), digests merged
  into multi-arch manifests, registry layer caches, GIT_SHA build arg,
  stowage-updater image.
- Docs: README "Updating", RELEASING "Where builds run", env vars,
  changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2cdb85d9-6559-4764-82f8-fa11267dffd7
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Sidecar: pass the operator's env files to compose and pin the version in
them only after the pull succeeds; update every service of the project,
not just running ones; stop and fail a run after 30 minutes; a failure
while recreating the updater itself no longer fails the app update;
constant-time token compare; 400 on bad JSON; generic 500 body; mount
compose/env file directories outside the project; Mounts instead of bind
strings; shorter error messages.

App: 30 s timeout to start an update and a refetch after apply errors so
a started run is followed; Stop following/Reload in every phase and slow
copy after 10 minutes; Check now reports stale lists and recent checks
honestly; zod-validated updater status; github.com-only release links;
token hint on 401; links and bold rendered as text in notes.

Docs and tests: STOWAGE_RELEASE_REPO, check cadence and exported
STOWAGE_VERSION notes; route, view and component tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kroqdotdev
kroqdotdev merged commit 86d3724 into main Nov 9, 2026
2 checks passed
@kroqdotdev
kroqdotdev deleted the feat/in-app-updates branch November 9, 2026 20:53
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