From 30fad12db14d4561692e900543eac1f167095c0e Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 3 Sep 2026 21:32:56 +0000 Subject: [PATCH 1/2] Release 0.4.0 Boards now keep themselves current from GitHub releases (#11). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PY5Y9gpnQ9kevUFwXJakyn --- apps/cli/package.json | 2 +- apps/mcp/package.json | 2 +- apps/server/package.json | 2 +- apps/tui/package.json | 2 +- apps/worker/package.json | 2 +- package.json | 2 +- packages/client/package.json | 2 +- packages/core/package.json | 2 +- packages/db/package.json | 2 +- packages/design-tokens/package.json | 2 +- packages/mail/package.json | 2 +- packages/markup/package.json | 2 +- packages/mcp/package.json | 2 +- packages/plugin-api/package.json | 2 +- packages/plugin-host/package.json | 2 +- packages/ui/package.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 31f4c74..b5079b9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tsbb", - "version": "0.3.0", + "version": "0.4.0", "description": "A TypeScript bulletin board. Forums, plugins, avatars, signatures and a terminal client.", "type": "module", "license": "MIT", diff --git a/apps/mcp/package.json b/apps/mcp/package.json index f727e9a..c12612b 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tsbb-mcp", - "version": "0.3.0", + "version": "0.4.0", "description": "MCP server for a tsbb bulletin board. Lets an AI assistant read and post on any board, as the member whose token it holds.", "type": "module", "license": "MIT", diff --git a/apps/server/package.json b/apps/server/package.json index bc899a7..a18b680 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/server", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "dependencies": { diff --git a/apps/tui/package.json b/apps/tui/package.json index 8ba9603..d3704de 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tsbb-tui", - "version": "0.3.0", + "version": "0.4.0", "description": "Terminal client for a tsbb bulletin board. Reads and posts against any centralised install.", "type": "module", "license": "MIT", diff --git a/apps/worker/package.json b/apps/worker/package.json index 1dc80db..dd97e42 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/worker", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "dependencies": { diff --git a/package.json b/package.json index 0275ffd..bdcad02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tsbb", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "packageManager": "pnpm@11.18.0", diff --git a/packages/client/package.json b/packages/client/package.json index 8b28336..6aeeba9 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/client", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index ab46086..8441ac9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/core", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index 74cb1cc..1f72b24 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/db", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json index 463ea7b..995a728 100644 --- a/packages/design-tokens/package.json +++ b/packages/design-tokens/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/design-tokens", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { "./tokens.css": "./src/tokens.css" } diff --git a/packages/mail/package.json b/packages/mail/package.json index 2780cd9..11a6a24 100644 --- a/packages/mail/package.json +++ b/packages/mail/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/mail", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" }, diff --git a/packages/markup/package.json b/packages/markup/package.json index 7e07d7d..c541820 100644 --- a/packages/markup/package.json +++ b/packages/markup/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/markup", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 378f223..6e337df 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/mcp", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { diff --git a/packages/plugin-api/package.json b/packages/plugin-api/package.json index 8adaf03..5c5488c 100644 --- a/packages/plugin-api/package.json +++ b/packages/plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/plugin-api", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } diff --git a/packages/plugin-host/package.json b/packages/plugin-host/package.json index 443e135..9698fe2 100644 --- a/packages/plugin-host/package.json +++ b/packages/plugin-host/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/plugin-host", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" }, diff --git a/packages/ui/package.json b/packages/ui/package.json index d12dd1d..8a800bb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@tsbb/ui", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts", "./styles": "./src/styles/index.ts" }, From ece9450e6264acc24170b142b1118e01fe7cbc6f Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 3 Sep 2026 22:02:00 +0000 Subject: [PATCH 2/2] Let a container keep itself current by running from a checkout on its volume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self-updater shipped in 0.4.0 can move a git checkout but not an image, so a board on Railway — including bbs.hqtui.com — only ever saw the "new version available" notice. This gives the image a way to be updated from the inside. With TSBB_CHECKOUT_DIR pointing into a volume, the container clones the repository there on first boot, at the image's own commit, and installs its dependencies from the image's pnpm store, so the first start takes about three seconds and needs no download. It then runs the server from that checkout in a loop: the updater fetches each release into the volume and exits, and the loop starts the new code. A container restart comes back on the release it had rather than the image it was built from, because the checkout is on the volume. Without TSBB_CHECKOUT_DIR the entrypoint runs the image's own code exactly as before, so tsbb.dev is unchanged. Copying the image's node_modules into the checkout instead of installing does not work — pnpm sees a tree made for another path and stops to ask whether it may rebuild it, and there is no TTY to say yes. Linking from the store is what the install already does, and it is as fast. Tried for real in Docker: a container seeded at the commit before the 0.4.0 bump found v0.4.0 a minute after boot, fetched, checked out, installed in one second, restarted, and answered the healthcheck on the new commit. A restart of that container came back on 0.4.0. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PY5Y9gpnQ9kevUFwXJakyn --- Dockerfile | 13 ++++++++-- README.md | 18 +++++++++++--- bin/entrypoint.sh | 63 +++++++++++++++++++++++++++++++++++++++++++++++ railway.json | 4 +-- 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 bin/entrypoint.sh diff --git a/Dockerfile b/Dockerfile index afce1b9..d82300b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,13 @@ ENV NODE_ENV=production \ RUN corepack enable +# git is for a container that runs from a checkout on a volume and updates +# itself (TSBB_CHECKOUT_DIR — see bin/entrypoint.sh). An image without it can +# only ever be replaced by another image. +RUN apt-get update \ + && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + # The whole tree is copied before installing rather than listing each workspace # manifest by hand. Listing them is faster to rebuild and silently wrong: a new # package is invisible to pnpm until it happens to gain an external dependency. @@ -23,5 +30,7 @@ RUN pnpm install --frozen-lockfile --ignore-scripts EXPOSE 3000 # Migrations run at boot inside the server, so a deploy can never leave new code -# running against an old schema. -CMD ["node", "apps/server/src/index.ts"] +# running against an old schema. The entrypoint runs the image's own code +# unless TSBB_CHECKOUT_DIR says to run — and keep updating — a checkout on a +# volume instead. +CMD ["sh", "bin/entrypoint.sh"] diff --git a/README.md b/README.md index 9013d8e..f6de60c 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ one with a fresh session secret and whatever you already had set. | `TSBB_UPDATES` | on | Set to `off` and the board never checks for a release | | `TSBB_RESTART` | `respawn` | `exit` under a supervisor that restarts the process itself | | `TSBB_UPDATE_REPO` | `profullstack/tsbb` | A fork that publishes its own releases | +| `TSBB_CHECKOUT_DIR` | — | A directory on a volume; the container runs, and updates, a checkout there | The worker runs inside the server by default, so email works from one command. @@ -284,10 +285,19 @@ out, and a **Check now** button; with automatic updates off it also offers **Update now**, and `tsbb update` does the same from a shell. Two things it will not do. It never touches a checkout with local changes — -that is somebody's work, and it stops and says so. And it never updates a -container: the Docker image has no `.git` to move, so a board on Railway, Fly -or a plain `docker run` sees the notice and is updated by redeploying the image, -which is how those platforms expect it anyway. +that is somebody's work, and it stops and says so. And it never updates an +image: the Docker image has no `.git` to move, so a board running the image's +own code sees the notice and is updated by redeploying, which is how those +platforms expect it anyway. + +A container *can* keep itself current if it has a volume. Set +`TSBB_CHECKOUT_DIR` to a directory on it (`/app/data/app`, say) and the +container clones the repository there on first boot — at the image's own +commit, with dependencies linked from the image's package store, so the first +start is as fast as any other — and runs from that checkout instead. Each release is then +fetched into the volume and the server restarted from it, and a container +restart comes back on the release it had, not the image it was built from. +`bbs.hqtui.com` runs this way. On restart the board spawns a fresh copy of itself with the same command line and exits, so `pnpm start` in a terminal or under nohup carries on with the new diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh new file mode 100644 index 0000000..6719222 --- /dev/null +++ b/bin/entrypoint.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# How the container starts. +# +# With no TSBB_CHECKOUT_DIR it runs the code baked into the image, which is +# what an image is for: it is updated by building and deploying a new one. +# +# With TSBB_CHECKOUT_DIR pointing into a volume, the container instead keeps a +# git checkout THERE and runs from it. On first boot it clones the repository +# at the image's own commit and installs from the image's own package store, +# so the first start needs no download and passes the healthcheck like any +# other. From then on the board updates itself: the server fetches each new +# release into the checkout, exits, and this script starts it again from the +# new code — so a board on a platform that only redeploys images still follows +# releases within minutes, and survives a container restart because the +# checkout is on the volume, not in the container. +set -eu + +IMAGE_ROOT="${TSBB_IMAGE_ROOT:-/app}" +DIR="${TSBB_CHECKOUT_DIR:-}" +REPO="${TSBB_UPDATE_REPO:-profullstack/tsbb}" + +if [ -z "$DIR" ]; then + cd "$IMAGE_ROOT" + exec node apps/server/src/index.ts +fi + +if [ ! -d "$DIR/.git" ]; then + echo "[tsbb] first boot: cloning $REPO into $DIR" + rm -rf "$DIR" + git clone --quiet "https://github.com/$REPO" "$DIR" + # Start from exactly the commit this image was built from, so the checkout + # and the image agree, so the install below finds everything in the store. + if [ -n "${RAILWAY_GIT_COMMIT_SHA:-}" ]; then + git -C "$DIR" -c advice.detachedHead=false checkout --quiet "$RAILWAY_GIT_COMMIT_SHA" \ + || echo "[tsbb] commit ${RAILWAY_GIT_COMMIT_SHA} is not in the clone; staying on the default branch" + fi + # The image's install left every package in pnpm's store, so linking them + # into the new checkout is seconds and needs no network. Copying the image's + # node_modules instead does not work: pnpm sees a tree made for another path + # and stops to ask whether it may rebuild it. CI=true is the "yes" it wants + # if the lockfile has drifted and it must. + echo "[tsbb] installing dependencies from the image's store" + ( cd "$DIR" && CI=true pnpm install --frozen-lockfile --ignore-scripts --offline ) \ + || ( cd "$DIR" && CI=true pnpm install --frozen-lockfile --ignore-scripts ) +fi + +cd "$DIR" +echo "[tsbb] running from $DIR at $(git rev-parse --short HEAD) ($(git describe --tags --always 2>/dev/null))" + +# The updater exits after installing a release; this loop is the supervisor +# that brings the new code up. A crash waits a few seconds so a broken build +# cannot spin the container. +export TSBB_RESTART=exit +while :; do + if node apps/server/src/index.ts; then + echo "[tsbb] server exited; starting again from $(git rev-parse --short HEAD)" + else + code=$? + echo "[tsbb] server exited with status $code; starting again in 5s" + sleep 5 + fi +done diff --git a/railway.json b/railway.json index 76713fe..c783f2c 100644 --- a/railway.json +++ b/railway.json @@ -5,9 +5,9 @@ "dockerfilePath": "Dockerfile" }, "deploy": { - "startCommand": "node apps/server/src/index.ts", + "startCommand": "sh bin/entrypoint.sh", "healthcheckPath": "/healthz", - "healthcheckTimeout": 120, + "healthcheckTimeout": 300, "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 10, "numReplicas": 1