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
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ STORAGE_LIMIT_GB=
# Leave blank if you do not want a label URL base configured.
SITE_URL=http://localhost:3000

# In-app updates (Settings > Updates)
#
# The app always shows newer releases from GitHub. To update from the browser,
# start the release stack with the `updater` profile and give both services the
# same long random token (for example from `openssl rand -hex 32`). The updater
# needs the Docker socket, so only enable it on a host you control.
STOWAGE_UPDATER_TOKEN=

# Repo whose GitHub Releases the Updates page offers. Only change this for a
# fork that publishes its own images.
# STOWAGE_RELEASE_REPO=kroqdotdev/stowage

# Profiles Compose treats as active for every command. Listing them here keeps
# `docker compose up -d` and the in-app updater in agreement about which
# services run. Examples: `pocketbase,updater` or `full,updater`.
COMPOSE_PROFILES=

# E2E tests
#
# Optional credentials for a real user/admin on the target PocketBase instance.
Expand Down
169 changes: 117 additions & 52 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,49 @@ jobs:
- name: Vitest (PocketBase domain)
run: pnpm test:pb

docker:
name: Publish Docker images
runs-on: ubuntu-latest
# Each image is built natively per platform: amd64 on the self-hosted
# runner named by the CI_RUNNER repo variable (GitHub's when it is unset),
# arm64 on GitHub's free arm runner. The platform builds push by digest and
# the merge job joins them into one multi-arch tag, so no QEMU emulation is
# needed anywhere. Layer caches live in GHCR next to the images.
build:
name: Build ${{ matrix.image }} (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
needs: verify
timeout-minutes: 40
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
image: [app, pocketbase, caddy, updater]
platform: [linux/amd64, linux/arm64]
include:
- image: app
dockerfile: ./Dockerfile
- image: pocketbase
dockerfile: ./Dockerfile.pocketbase
- image: caddy
dockerfile: ./Dockerfile.caddy
- image: updater
dockerfile: ./Dockerfile.updater
- platform: linux/amd64
runner: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
- platform: linux/arm64
runner: ubuntu-24.04-arm
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/stowage-${{ matrix.image }}

steps:
- uses: actions/checkout@v6

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Platform slug
id: platform
shell: bash
run: |
platform="${{ matrix.platform }}"
echo "slug=${platform//\//-}" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand All @@ -100,72 +130,106 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata (app)
id: app_meta
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/stowage-app
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
images: ${{ env.IMAGE }}

- name: Docker metadata (pocketbase)
id: pb_meta
uses: docker/metadata-action@v5
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/stowage-pocketbase
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ env.IMAGE }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
build-args: |
GIT_SHA=${{ github.sha }}
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache-${{ steps.platform.outputs.slug }}
cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache-${{ steps.platform.outputs.slug }},mode=max

- name: Export digest
shell: bash
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"

- name: Docker metadata (caddy)
id: caddy_meta
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.image }}-${{ steps.platform.outputs.slug }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

publish:
name: Publish ${{ matrix.image }} manifest
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
needs:
- verify
- build
timeout-minutes: 15
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
image: [app, pocketbase, caddy, updater]
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/stowage-${{ matrix.image }}

steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-${{ matrix.image }}-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/stowage-caddy
images: ${{ env.IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}

- name: Build and publish app image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.app_meta.outputs.tags }}
labels: ${{ steps.app_meta.outputs.labels }}

- name: Build and publish PocketBase image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.pocketbase
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.pb_meta.outputs.tags }}
labels: ${{ steps.pb_meta.outputs.labels }}
- name: Create multi-arch manifest
shell: bash
working-directory: ${{ runner.temp }}/digests
run: |
set -euo pipefail
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $tags \
$(printf "$IMAGE@sha256:%s " *)

- name: Build and publish Caddy image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.caddy
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.caddy_meta.outputs.tags }}
labels: ${{ steps.caddy_meta.outputs.labels }}
- name: Inspect
run: docker buildx imagetools inspect "$IMAGE:${{ steps.meta.outputs.version }}"

release:
name: Create GitHub release
runs-on: ubuntu-latest
needs:
- verify
- docker
- publish
permissions:
contents: write

Expand Down Expand Up @@ -198,6 +262,7 @@ jobs:
echo "- \`ghcr.io/$OWNER/stowage-app:$VERSION\`"
echo "- \`ghcr.io/$OWNER/stowage-pocketbase:$VERSION\`"
echo "- \`ghcr.io/$OWNER/stowage-caddy:$VERSION\`"
echo "- \`ghcr.io/$OWNER/stowage-updater:$VERSION\`"
echo
echo "## Deployment bundle"
echo
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ Until then, version numbers still communicate upgrade risk:

## [Unreleased]

### Added

- Settings > Updates for admins: the installed version and build, newer
releases with their notes, and a one-click update when the optional
`updater` sidecar runs (`--profile updater`), with the compose commands shown
otherwise.
- Published `stowage-updater` release image.
- `/api/health` now reports the running version.

### Changed

- Rebuilt the UI in the Workshop design: concrete background, charcoal
sidebar, one amber primary, cards for objects and sheets for lists, a
phone layout with bottom navigation, and an asset scan flow.
- Release images are built natively per platform (amd64 on the self-hosted
runner, arm64 on GitHub's arm runner) and merged into multi-arch manifests,
with layer caches kept in GHCR.

## [0.2.0] - 2026-04-21

### Added
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ RUN pnpm build
# Stage 3 — runtime
FROM node:22-alpine AS runner
ARG APP_PORT=3000
# Short git sha of the build; shown next to the version in Settings > Updates.
ARG GIT_SHA=
WORKDIR /app
ENV NODE_ENV=production \
HOSTNAME=0.0.0.0 \
PORT=${APP_PORT}
PORT=${APP_PORT} \
STOWAGE_BUILD_SHA=${GIT_SHA}
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder /app/.next/standalone ./
Expand Down
16 changes: 16 additions & 0 deletions Dockerfile.updater
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# syntax=docker/dockerfile:1.6

# The updater sidecar: a small Node HTTP server plus the Docker CLI and the
# compose plugin. The app calls it to pull a published release and recreate
# the compose services. It needs the Docker socket mounted (see
# docker-compose.release.yml, profile "updater").
FROM node:22-alpine
RUN apk add --no-cache docker-cli docker-cli-compose
WORKDIR /updater
COPY deploy/updater/server.mjs ./server.mjs
ENV NODE_ENV=production
VOLUME ["/state"]
EXPOSE 9876
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -q --spider http://127.0.0.1:9876/health || exit 1
CMD ["node", "/updater/server.mjs", "serve"]
Loading
Loading