Updater signing key, a rising dev version, and a production compose
Three things, all needed before the update loop can be tested. The public signing key is committed. Verified before trusting it: algorithm `Ed`, key ID 90E96FEA2F6D9B6A matching its own comment, 32-byte Ed25519 key. Dev builds now carry a version that RISES. Every build took its version from Cargo.toml, so each one was 0.1.0 — an installed 0.1.0 would read a manifest advertising 0.1.0, conclude it was current, and never update. The rolling channel would have looked broken while working exactly as written. Dev builds are now 0.1.<ci-run-number>, from one helper shared by both bundle jobs and the manifest writer, because three separate derivations of "what version is this" is three chances for the binary and the manifest to disagree. Plain semver, not a `-dev.N` prerelease: prerelease versions sort BELOW the release they qualify, so a tagged build would never update to a newer dev one, and Windows installer metadata wants a numeric X.Y.Z regardless. Bumping the minor still beats any dev build on the old line — 0.2.0 > 0.1.2932. The Windows job also gets the signing environment it was missing, so its NSIS installer is signed too. Without that the manifest would have had a Linux entry and nothing for the platform actually being tested. docker-compose.yml is now the production stack, per request: it pulls the published image instead of building, keeps Postgres OFF the host network, sets restart policies, health checks and log rotation, and refuses to start without a POSTGRES_PASSWORD rather than shipping a known one. Volume names are deliberately unchanged so an existing deployment upgrades in place instead of silently coming up against an empty database. Development keeps its own clearly-named file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
# ThoughtSync production settings. Copy to `.env` and edit:
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# Only POSTGRES_PASSWORD has no default — compose refuses to start without it.
|
||||||
|
# Everything else here is optional. Anything NOT in this file (site name, signups,
|
||||||
|
# attachment limits, trash retention, link previews) is configured in the admin
|
||||||
|
# Settings UI and stored in the database, not here.
|
||||||
|
|
||||||
|
# --- required ---------------------------------------------------------------
|
||||||
|
|
||||||
|
# Generate one and keep it: changing it later means also changing it inside the
|
||||||
|
# database, or Postgres will reject the app's connection.
|
||||||
|
#
|
||||||
|
# openssl rand -base64 24 | tr -d '/+=' | head -c 32
|
||||||
|
#
|
||||||
|
# Stick to letters and digits. This value goes into a connection URL, so a `@`,
|
||||||
|
# `/`, `:` or `#` in it will be misparsed as URL structure rather than password.
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# --- optional ---------------------------------------------------------------
|
||||||
|
|
||||||
|
# Which build to run.
|
||||||
|
#
|
||||||
|
# latest tracks the `main` branch — the production line (default)
|
||||||
|
# dev tracks the `dev` branch — newer, less settled
|
||||||
|
# <commit sha> pins one exact build; every push publishes one, and this is
|
||||||
|
# the rollback lever when an upgrade misbehaves
|
||||||
|
#
|
||||||
|
# NOTE: `main` can sit well behind `dev`. If a feature you expect is missing,
|
||||||
|
# check which branch it actually landed on before assuming a bug.
|
||||||
|
#THOUGHTSYNC_TAG=latest
|
||||||
|
|
||||||
|
# The host port the app is published on.
|
||||||
|
#THOUGHTSYNC_PORT=5000
|
||||||
|
|
||||||
|
# Which interface to bind. The default (all interfaces) is what lets desktop
|
||||||
|
# clients on your network reach the server. Behind a reverse proxy, set this to
|
||||||
|
# 127.0.0.1 so only the proxy can talk to it.
|
||||||
|
#THOUGHTSYNC_BIND=0.0.0.0
|
||||||
|
|
||||||
|
# Database identity. Changing these AFTER the first start does not rename anything
|
||||||
|
# that already exists — the volume keeps whatever the first run created.
|
||||||
|
#POSTGRES_USER=thoughtsync
|
||||||
|
#POSTGRES_DB=thoughtsync
|
||||||
|
|
||||||
|
# --- a note on HTTPS --------------------------------------------------------
|
||||||
|
#
|
||||||
|
# The app marks its session cookie Secure automatically when a request arrives over
|
||||||
|
# HTTPS, directly or via a proxy setting X-Forwarded-Proto — no setting needed.
|
||||||
|
#
|
||||||
|
# Worth knowing if you use the desktop app: typing a bare hostname there defaults to
|
||||||
|
# https://, deliberately, so a device token never crosses the wire in cleartext by
|
||||||
|
# accident. Serving over plain HTTP means typing the `http://` yourself.
|
||||||
@@ -91,8 +91,11 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No TAURI_SIGNING_PRIVATE_KEY — building unsigned, no updater artifacts."
|
echo "No TAURI_SIGNING_PRIVATE_KEY — building unsigned, no updater artifacts."
|
||||||
fi
|
fi
|
||||||
|
version="$(sh ../packaging/build-version.sh)"
|
||||||
|
echo "Building version $version"
|
||||||
cargo tauri build \
|
cargo tauri build \
|
||||||
--config '{"build":{"beforeBuildCommand":""}}' \
|
--config '{"build":{"beforeBuildCommand":""}}' \
|
||||||
|
--config "{\"version\":\"$version\"}" \
|
||||||
--config "$updater"
|
--config "$updater"
|
||||||
working-directory: desktop/src-tauri
|
working-directory: desktop/src-tauri
|
||||||
|
|
||||||
@@ -217,12 +220,23 @@ jobs:
|
|||||||
# the MSVC CRT/SDK, pre-warmed into the image, and links with lld-link).
|
# the MSVC CRT/SDK, pre-warmed into the image, and links with lld-link).
|
||||||
# Frontend already built above; skip the beforeBuildCommand rebuild.
|
# Frontend already built above; skip the beforeBuildCommand rebuild.
|
||||||
- name: Tauri build (NSIS installer)
|
- name: Tauri build (NSIS installer)
|
||||||
|
env:
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
|
version="$(sh ../packaging/build-version.sh)"
|
||||||
|
echo "Building version $version"
|
||||||
|
updater='{}'
|
||||||
|
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||||
|
updater='{"bundle":{"createUpdaterArtifacts":true}}'
|
||||||
|
fi
|
||||||
cargo tauri build \
|
cargo tauri build \
|
||||||
--runner cargo-xwin \
|
--runner cargo-xwin \
|
||||||
--target x86_64-pc-windows-msvc \
|
--target x86_64-pc-windows-msvc \
|
||||||
--bundles nsis \
|
--bundles nsis \
|
||||||
--config '{"build":{"beforeBuildCommand":""}}'
|
--config '{"build":{"beforeBuildCommand":""}}' \
|
||||||
|
--config "{\"version\":\"$version\"}" \
|
||||||
|
--config "$updater"
|
||||||
working-directory: desktop/src-tauri
|
working-directory: desktop/src-tauri
|
||||||
|
|
||||||
- name: Upload installer
|
- name: Upload installer
|
||||||
@@ -292,8 +306,10 @@ jobs:
|
|||||||
echo "manifest to write. Add the secret to enable in-app updates."
|
echo "manifest to write. Add the secret to enable in-app updates."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
# The version the bundles carry, read from the crate rather than guessed.
|
# The SAME helper the bundles were built with — a second derivation here
|
||||||
version="$(grep -m1 '^version' desktop/src-tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
|
# could drift, and a manifest whose version doesn't match the binary it
|
||||||
|
# points at is an updater that never settles.
|
||||||
|
version="$(sh desktop/packaging/build-version.sh)"
|
||||||
if [ "${GITHUB_REF_NAME}" = "dev" ]; then
|
if [ "${GITHUB_REF_NAME}" = "dev" ]; then
|
||||||
export RELEASE_TAG=dev
|
export RELEASE_TAG=dev
|
||||||
export RELEASE_NOTES="Development build from ${GITHUB_SHA}"
|
export RELEASE_NOTES="Development build from ${GITHUB_SHA}"
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
#
|
||||||
|
# Echo the version this build should carry. One definition, used in three places
|
||||||
|
# (both bundle jobs and the manifest writer) — if they ever disagreed, the app would
|
||||||
|
# compare its own version against a manifest describing a different build, and the
|
||||||
|
# updater would either offer nothing or loop forever offering the same thing.
|
||||||
|
#
|
||||||
|
# WHY DEV BUILDS NEED THEIR OWN VERSION AT ALL:
|
||||||
|
# an updater decides by comparing semver. Every dev build carries the version in
|
||||||
|
# Cargo.toml, so without this they'd all be `0.1.0` — an installed build would see a
|
||||||
|
# manifest advertising the version it already has, conclude it was current, and never
|
||||||
|
# update. The rolling channel needs a number that actually rises.
|
||||||
|
#
|
||||||
|
# The CI run number is that number: monotonic, already unique per build, and it needs
|
||||||
|
# no state carried between runs. `0.1.0` + run 2932 becomes `0.1.2932`.
|
||||||
|
#
|
||||||
|
# Plain semver on purpose, NOT a `-dev.N` prerelease tag: prerelease versions sort
|
||||||
|
# BELOW the release they qualify (`0.1.0-dev.5` < `0.1.0`), so a tagged build would
|
||||||
|
# never update to a newer dev one, and Windows installer metadata wants a numeric
|
||||||
|
# X.Y.Z anyway. Bumping the minor in Cargo.toml still wins over any dev build on the
|
||||||
|
# old line, which is the ordering you want: 0.2.0 > 0.1.2932.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CARGO_TOML="$(dirname "$0")/../src-tauri/Cargo.toml"
|
||||||
|
base="$(grep -m1 '^version' "$CARGO_TOML" | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||||
|
|
||||||
|
# Dev builds only. Anything else (a v* tag, main) ships the version as written.
|
||||||
|
if [ "${GITHUB_REF_NAME:-}" = "dev" ] && [ -n "${GITHUB_RUN_NUMBER:-}" ]; then
|
||||||
|
printf '%s.%s\n' "${base%.*}" "$GITHUB_RUN_NUMBER"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$base"
|
||||||
|
fi
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://git.fabledsword.com/bvandeusen/thoughtsync/releases/download/stable/latest.json"
|
"https://git.fabledsword.com/bvandeusen/thoughtsync/releases/download/stable/latest.json"
|
||||||
],
|
],
|
||||||
"pubkey": "",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkwRTk2RkVBMkY2RDlCNkEKUldScW0yMHY2bS9wa0VBdWFpM3c1d2trQnlNVUJXUUtwZXBzQjduM3FRVzdGa3dXNGxObkZFV28K",
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "passive"
|
"installMode": "passive"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
# docker compose -f docker-compose.dev.yml up
|
# docker compose -f docker-compose.dev.yml up
|
||||||
#
|
#
|
||||||
# Then open http://localhost:5173 (the Vite dev server proxies /api to the backend).
|
# Then open http://localhost:5173 (the Vite dev server proxies /api to the backend).
|
||||||
# (docker-compose.yml, by contrast, builds + runs the production image on :5000.)
|
#
|
||||||
|
# docker-compose.yml — the DEFAULT file — is the production stack instead: it pulls
|
||||||
|
# the published image, keeps Postgres off the host network, and expects a .env. This
|
||||||
|
# one builds nothing and is deliberately insecure-by-convenience (weak password,
|
||||||
|
# Postgres published on 5432) because it is meant for a laptop, not a deployment.
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
+76
-12
@@ -1,34 +1,98 @@
|
|||||||
# Local two-service stack (app + Postgres). Provided for convenience — per family
|
# ThoughtSync — PRODUCTION stack (app + Postgres).
|
||||||
# rule 12 the agent does NOT start this; run it yourself with `docker compose up`.
|
#
|
||||||
|
# This is the default compose file: `docker compose up -d` runs a real deployment
|
||||||
|
# from the published image. Development lives in docker-compose.dev.yml (hot-reload,
|
||||||
|
# builds from source).
|
||||||
|
#
|
||||||
|
# cp .env.example .env # then set POSTGRES_PASSWORD
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Per family rule 12 the agent does NOT start this — run it yourself.
|
||||||
|
#
|
||||||
|
# Upgrades: docker compose pull && docker compose up -d
|
||||||
|
# Rollback: set THOUGHTSYNC_TAG to a commit sha in .env, then the same two commands.
|
||||||
|
# Every push publishes an immutable :<sha> image for exactly this.
|
||||||
|
#
|
||||||
|
# Schema migrations run automatically at container start (see the Dockerfile CMD),
|
||||||
|
# so an upgrade is just a pull and a restart. Take a backup first anyway:
|
||||||
|
#
|
||||||
|
# docker compose exec -T db pg_dump -U thoughtsync thoughtsync > backup.sql
|
||||||
|
#
|
||||||
|
# Attachments are files, not rows — they live in the `thoughtsync-data` volume and
|
||||||
|
# a pg_dump does NOT contain them. Back up both or you'll restore notes whose images
|
||||||
|
# are gone.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: thoughtsync
|
POSTGRES_USER: ${POSTGRES_USER:-thoughtsync}
|
||||||
POSTGRES_PASSWORD: thoughtsync
|
# No default on purpose. A production compose that ships a known password is
|
||||||
POSTGRES_DB: thoughtsync
|
# how self-hosted databases end up in search engines; compose fails fast here
|
||||||
|
# instead, with the message below.
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-thoughtsync}
|
||||||
volumes:
|
volumes:
|
||||||
|
# Volume names kept from the previous compose file so an existing deployment
|
||||||
|
# upgrades in place. Renaming them would silently start against an empty
|
||||||
|
# database while the old one sat there, orphaned and looking like data loss.
|
||||||
- thoughtsync-db:/var/lib/postgresql/data
|
- thoughtsync-db:/var/lib/postgresql/data
|
||||||
ports:
|
# Deliberately NOT published to the host. The app reaches Postgres over the
|
||||||
- "5432:5432"
|
# compose network; exposing 5432 only widens the attack surface. If you need
|
||||||
|
# psql, `docker compose exec db psql -U thoughtsync` gets you there without it.
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-thoughtsync} -d ${POSTGRES_DB:-thoughtsync}"]
|
||||||
interval: 5s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
logging: &logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
# Unbounded container logs are a slow-motion disk-full outage on a
|
||||||
|
# long-running self-hosted box.
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
# :latest tracks `main`. Set THOUGHTSYNC_TAG=dev in .env to follow the
|
||||||
|
# development line instead, or a commit sha to pin exactly.
|
||||||
|
image: git.fabledsword.com/bvandeusen/thoughtsync:${THOUGHTSYNC_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:thoughtsync@db:5432/thoughtsync
|
# The only required application setting. Everything else a person might want
|
||||||
|
# to tune lives in the admin Settings UI, backed by the database (rule 25).
|
||||||
|
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-thoughtsync}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-thoughtsync}
|
||||||
volumes:
|
volumes:
|
||||||
|
# Uploaded attachments. /var/thoughtsync is fixed in the app (Config.DATA_DIR),
|
||||||
|
# not configurable — mount it or lose every image on container recreation.
|
||||||
- thoughtsync-data:/var/thoughtsync
|
- thoughtsync-data:/var/thoughtsync
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
# Default binds every interface, which is what lets desktop clients on the LAN
|
||||||
|
# reach it. Behind a reverse proxy, set THOUGHTSYNC_BIND=127.0.0.1 so only the
|
||||||
|
# proxy can talk to it.
|
||||||
|
- "${THOUGHTSYNC_BIND:-0.0.0.0}:${THOUGHTSYNC_PORT:-5000}:5000"
|
||||||
|
healthcheck:
|
||||||
|
# python rather than curl: the runtime image is python:3.12-slim and carries no
|
||||||
|
# HTTP client binary. Hits the app's own /api/health.
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- python
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
import sys, urllib.request
|
||||||
|
sys.exit(0 if urllib.request.urlopen("http://127.0.0.1:5000/api/health", timeout=5).status == 200 else 1)
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
# Generous: the container waits for Postgres and runs migrations before it
|
||||||
|
# serves anything, and a first boot builds the whole schema.
|
||||||
|
start_period: 60s
|
||||||
|
logging: *logging
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
thoughtsync-db:
|
thoughtsync-db:
|
||||||
|
|||||||
Reference in New Issue
Block a user