feat: add a one-command self-host installer at warmbly.com/install.sh with an interactive data-control wizard, give docker-compose.yml image keys and per-store volume variables, add an image-mode updater, move engagement/form/audit retention into instance settings, and add warmblyctl backup/restore

This commit is contained in:
Matthew Meszaros
2026-09-04 05:49:54 -07:00
parent 068b34751b
commit d68bbcd2ab
49 changed files with 6023 additions and 103 deletions
+10 -2
View File
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
service:
description: "Service to build (all, backend, consumer, worker, forms, tracking, realtime)"
description: "Service to build (all, backend, consumer, worker, forms, updater, tracking, realtime)"
required: false
default: "all"
@@ -61,6 +61,12 @@ jobs:
- 'cmd/forms/**'
- 'forms/**'
- 'deploy/docker/forms.Dockerfile'
updater:
- 'go.mod'
- 'go.sum'
- 'internal/updater/**'
- 'cmd/updater/**'
- 'deploy/docker/updater.Dockerfile'
tracking:
- 'tracking/**'
realtime:
@@ -75,6 +81,7 @@ jobs:
CONSUMER: ${{ steps.filter.outputs.consumer }}
WORKER: ${{ steps.filter.outputs.worker }}
FORMS: ${{ steps.filter.outputs.forms }}
UPDATER: ${{ steps.filter.outputs.updater }}
TRACKING: ${{ steps.filter.outputs.tracking }}
REALTIME: ${{ steps.filter.outputs.realtime }}
run: |
@@ -82,7 +89,7 @@ jobs:
native=""
if [ "$EVENT" = "workflow_dispatch" ]; then
sel="${SELECTED:-all}"
for s in backend consumer worker forms; do
for s in backend consumer worker forms updater; do
if [ "$sel" = "all" ] || [ "$sel" = "$s" ]; then go="$go\"$s\","; fi
done
for s in tracking realtime; do
@@ -93,6 +100,7 @@ jobs:
if [ "$CONSUMER" = "true" ]; then go="$go\"consumer\","; fi
if [ "$WORKER" = "true" ]; then go="$go\"worker\","; fi
if [ "$FORMS" = "true" ]; then go="$go\"forms\","; fi
if [ "$UPDATER" = "true" ]; then go="$go\"updater\","; fi
if [ "$TRACKING" = "true" ]; then native="$native\"tracking\","; fi
if [ "$REALTIME" = "true" ]; then native="$native\"realtime\","; fi
fi
+21 -1
View File
@@ -34,6 +34,7 @@ jobs:
forms: ${{ steps.filter.outputs.forms }}
make: ${{ steps.filter.outputs.make }}
ios: ${{ steps.filter.outputs.ios }}
installer: ${{ steps.filter.outputs.installer }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
@@ -70,6 +71,10 @@ jobs:
- 'integrations/make/**'
ios:
- 'ios/**'
installer:
- 'site/public/install.sh'
- 'site/public/install.sh.sha256'
- 'scripts/check-installer.sh'
migrations-ci:
name: Migrations
@@ -229,6 +234,21 @@ jobs:
- name: Build
run: pnpm build
# The installer is served verbatim from site/public, so what CI checks is
# exactly what a `curl | sh` executes. The checksum is published next to it
# for anyone who would rather download, verify and read before running, and
# it is only useful if it cannot drift.
installer-ci:
name: Installer CI
needs: changes
if: needs.changes.outputs.installer == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check the installer
run: ./scripts/check-installer.sh
make-ci:
name: Make App CI
needs: changes
@@ -398,7 +418,7 @@ jobs:
ci-status:
name: CI Status
runs-on: ubuntu-latest
needs: [changes, migrations-ci, go-ci, web-ci, admin-ci, site-ci, forms-ci, make-ci, rust-ci, elixir-ci, ios-ci, frontend-images]
needs: [changes, migrations-ci, go-ci, web-ci, admin-ci, site-ci, forms-ci, installer-ci, make-ci, rust-ci, elixir-ci, ios-ci, frontend-images]
if: always()
steps:
- name: Check CI status
+53 -1
View File
@@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
service: [backend, consumer, worker]
service: [backend, consumer, worker, forms, updater]
runs-on: ubuntu-latest
permissions:
contents: read
@@ -225,6 +225,38 @@ jobs:
with:
fetch-depth: 0
# The installer verifies what it pulled against this file, so it is what
# makes "curl | sh" checkable after the fact rather than only before it.
# One line per service, because the thing that reads it is a POSIX shell.
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish the image manifest
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
{
printf '{\n'
printf ' "tag": "%s",\n' "$TAG"
printf ' "registry": "%s",\n' "$IMAGE_PREFIX"
printf ' "images": {\n'
first=1
for s in backend consumer worker forms updater web admin tracking realtime; do
digest=$(docker buildx imagetools inspect "$IMAGE_PREFIX/$s:$TAG" \
--format '{{json .Manifest.Digest}}' | tr -d '"')
[ "$first" = 1 ] || printf ',\n'
first=0
printf ' "%s": "%s"' "$s" "$digest"
done
printf '\n }\n}\n'
} > /tmp/images.json
cat /tmp/images.json
- name: Generate release notes
env:
GITHUB_SERVER_URL: ${{ github.server_url }}
@@ -240,6 +272,23 @@ jobs:
cat /tmp/changelog.md
cat <<'EOF'
## Install
```
curl -fsSL https://warmbly.com/install.sh | sh -s -- --version ${{ github.ref_name }}
```
Add `--wizard` to be asked where each store lives, what is kept and
for how long, and how it is backed up.
`images.json` below lists the manifest digest of every image in this
release. The installer checks what it pulled against it, and you can
too:
```
docker image inspect ${{ env.IMAGE_PREFIX }}/backend:${{ github.ref_name }} --format '{{index .RepoDigests 0}}'
```
## Docker Images
All images are available at `ghcr.io/${{ github.repository_owner }}/warmbly`:
@@ -253,6 +302,8 @@ jobs:
| Realtime | `${{ env.IMAGE_PREFIX }}/realtime:${{ github.ref_name }}` |
| Dashboard (web) | `${{ env.IMAGE_PREFIX }}/web:${{ github.ref_name }}` |
| Admin | `${{ env.IMAGE_PREFIX }}/admin:${{ github.ref_name }}` |
| Forms | `${{ env.IMAGE_PREFIX }}/forms:${{ github.ref_name }}` |
| Updater | `${{ env.IMAGE_PREFIX }}/updater:${{ github.ref_name }}` |
## Deployment
@@ -266,5 +317,6 @@ jobs:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: /tmp/release-body.md
files: /tmp/images.json
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}