diff --git a/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx b/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx index d58a7f0eab..702dc653f9 100644 --- a/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx +++ b/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx @@ -69,7 +69,7 @@ const SECTION_LINKS = [ { title: "Operations", description: - "Health checks, logs, metrics, dashboards, jobs, queues, and daily operator routines.", + "Health checks, logs, metrics, safe update/rollback checklists, deploy scripts, and daily operator routines.", to: "/docs/self-hosting-operations", }, { diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index d4ed554bac..248796f64e 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -607,18 +607,74 @@ volumes:
- Both update paths below only ever restart the gittensory app service (
- --no-deps) — they never touch other compose-profile services or their state
- (Postgres, Redis, Qdrant, and Grafana's own grafana-data volume), and they
- never touch .env keys other than the one they persist for next time. That means{" "}
- .env, the gittensory-config/ mount, every data volume — including
- the app's own /data volume where Codex/Claude Code auth material lives —
- and any docker-compose.override.yml are preserved automatically across an
- update. You don't need to back those up or re-supply them just to run either script,
- and you only need to recreate a profile service yourself if you're deliberately
- upgrading that service (its own image tag in docker-compose.yml, or a
- Postgres/Redis/Qdrant major-version bump) rather than the app.
-
gittensory service, verify /ready, and confirm the release id. Use{" "}
+ Releases and images to pick a tag; use the
+ checklists below so updates never overwrite operator-owned secrets, config, or data.
+
+
+ .env and any *_FILE secret mounts — deploy scripts only write
+ back GITTENSORY_IMAGE (image path) or SENTRY_RELEASE /{" "}
+ GITTENSORY_VERSION (source path).
+ ./gittensory-config/ bind mount — private per-repo{" "}
+ .gittensory.yml policy.
+ gittensory-data (SQLite DB, Codex/Claude
+ auth under /data), gittensory-pg, qdrant-data,{" "}
+ gittensory-backups, and Grafana's grafana-data.
+ docker-compose.override.yml — still loaded via{" "}
+ SELFHOST_COMPOSE_FILES when set, or automatically when present beside{" "}
+ docker-compose.yml.
+ git pull and confirm git status is clean (no
+ uncommitted local changes the build would silently pick up).
+ docker inspect on the
+ running gittensory container so rollback has a known-good target.
+ curl http://localhost:8787/ready and a quick docker compose ps.
+ @@ -667,7 +723,42 @@ GITTENSORY_IMAGE=ghcr.io/jsonbored/gittensory-selfhost@sha256:... ./scripts/depl even when those three are present).
-docker compose).
+ curl http://localhost:8787/ready returns HTTP 200.
+ docker compose ps gittensory shows healthy.
+ selfhost_listening and, on first boot after a schema bump,{" "}
+ selfhost_migrations_applied — not selfhost_job_dead.
+ /health nor /ready exposes a
+ version string; check .env and the running container image instead.
+ + If any check fails, see Troubleshooting + . +
+ +
There is no rollback script. Rolling back means re-running one of the two
scripts above pointed at an older target:
@@ -699,42 +790,6 @@ GITTENSORY_IMAGE=ghcr.io/jsonbored/gittensory-selfhost@sha256:... ./scripts/depl
the same on the live instance.
-
Before updating:
-git status is clean (no uncommitted local changes the
- build would silently pick up or drop).
-
- After updating, work through the same checks as any other health pass — see{" "}
- Health endpoints and Useful commands above: confirm{" "}
- /ready returns 200, docker compose ps shows the service{" "}
- healthy, and tail recent logs for startup errors or an unexpected absence of{" "}
- selfhost_listening / selfhost_migrations_applied.
-
- Neither /health nor /ready reports a version, so confirm the
- deployed release directly — GITTENSORY_IMAGE or SENTRY_RELEASE in{" "}
- .env records what the deploy script just resolved, and{" "}
- docker inspect confirms what the running container actually has:
-
- If an operating check fails, go to{" "} - Self-host troubleshooting. -
-Tearing an instance down cleanly touches four independent things: the GitHub App diff --git a/scripts/deploy-selfhost-image.sh b/scripts/deploy-selfhost-image.sh index d9e5cc5ca2..5a17f57990 100755 --- a/scripts/deploy-selfhost-image.sh +++ b/scripts/deploy-selfhost-image.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Pull and deploy a published self-host image without rebuilding on the host. # +# After a successful run, verify with: ./scripts/selfhost-post-update-check.sh +# # Defaults to the latest official image: # ./scripts/deploy-selfhost-image.sh # diff --git a/scripts/deploy-selfhost-prebuilt.sh b/scripts/deploy-selfhost-prebuilt.sh index 88984ca5a9..484c29aadb 100755 --- a/scripts/deploy-selfhost-prebuilt.sh +++ b/scripts/deploy-selfhost-prebuilt.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Build and deploy the self-host runtime from a prebuilt bundle without relying on host Node/npm. # +# After a successful run, verify with: ./scripts/selfhost-post-update-check.sh +# # Defaults are intentionally operator-friendly: # ./scripts/deploy-selfhost-prebuilt.sh # diff --git a/scripts/selfhost-post-update-check.sh b/scripts/selfhost-post-update-check.sh new file mode 100755 index 0000000000..42759d8d35 --- /dev/null +++ b/scripts/selfhost-post-update-check.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Post-update verification for a self-host instance (#1823). +# +# Run after deploy-selfhost-image.sh, deploy-selfhost-prebuilt.sh, or any manual +# `docker compose up -d --no-deps gittensory` that ships a new app image. +# +# Checks /ready, compose health, .env release metadata, and the running container image. +# Does not modify .env, volumes, gittensory-config/, or any profile service. +set -euo pipefail + +ENV_FILE="${SELFHOST_ENV_FILE:-.env}" +SERVICE="${SELFHOST_SERVICE:-gittensory}" +PORT="${PORT:-8787}" +READY_URL="${SELFHOST_READY_URL:-http://127.0.0.1:${PORT}/ready}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/selfhost-deploy-common.sh +. "$SCRIPT_DIR/lib/selfhost-deploy-common.sh" + +require_cmd docker +require_cmd curl +docker compose version >/dev/null + +mapfile -t compose_args < <(compose_file_args) + +container_id="$(docker compose "${compose_args[@]}" ps -q "$SERVICE" 2>/dev/null || true)" +if [ -z "$container_id" ]; then + echo "error: $SERVICE is not running" >&2 + docker compose "${compose_args[@]}" ps "$SERVICE" >&2 || true + exit 1 +fi + +echo "selfhost post-update check: probing $READY_URL" +if ! curl -sf "$READY_URL" >/dev/null; then + echo "error: $READY_URL did not return HTTP 2xx" >&2 + exit 1 +fi + +status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || true)" +echo "selfhost post-update check: $SERVICE container status=$status" +if [ "$status" != "healthy" ] && [ "$status" != "running" ]; then + echo "error: expected healthy or running, got $status" >&2 + docker compose "${compose_args[@]}" ps "$SERVICE" >&2 || true + exit 1 +fi + +if [ -f "$ENV_FILE" ]; then + echo "selfhost post-update check: release metadata from $ENV_FILE" + grep -E '^(GITTENSORY_IMAGE|GITTENSORY_VERSION|SENTRY_RELEASE)=' "$ENV_FILE" || true +else + echo "selfhost post-update check: warning — $ENV_FILE not found (skipping release metadata grep)" >&2 +fi + +running_image="$(docker inspect --format '{{.Config.Image}}' "$container_id")" +echo "selfhost post-update check: running image=$running_image" +echo "selfhost post-update check: ok" diff --git a/test/unit/docs-selfhost-update-rollback.test.ts b/test/unit/docs-selfhost-update-rollback.test.ts new file mode 100644 index 0000000000..00a9fed397 --- /dev/null +++ b/test/unit/docs-selfhost-update-rollback.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +// Drift guard (#1823): the self-host update/rollback docs must stay aligned with the shipped deploy +// scripts and the post-update verification helper. + +const OPERATIONS = "apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx"; +const IMAGE_SCRIPT = "scripts/deploy-selfhost-image.sh"; +const PREBUILT_SCRIPT = "scripts/deploy-selfhost-prebuilt.sh"; +const POST_UPDATE_SCRIPT = "scripts/selfhost-post-update-check.sh"; + +const operations = readFileSync(OPERATIONS, "utf8"); +const imageScript = readFileSync(IMAGE_SCRIPT, "utf8"); +const prebuiltScript = readFileSync(PREBUILT_SCRIPT, "utf8"); +const postUpdateScript = readFileSync(POST_UPDATE_SCRIPT, "utf8"); + +describe("self-host update + rollback docs (#1823)", () => { + it("documents both deploy paths, operator-owned paths, and the post-update helper", () => { + expect(operations).toContain("deploy-selfhost-image.sh"); + expect(operations).toContain("deploy-selfhost-prebuilt.sh"); + expect(operations).toContain("selfhost-post-update-check.sh"); + expect(operations).toContain("Preflight checklist"); + expect(operations).toContain("Post-update checklist"); + expect(operations).toContain("Operator-owned"); + expect(operations).toContain("gittensory-config/"); + expect(operations).toContain("gittensory-data"); + expect(operations).toContain("Migrations are forward-only"); + }); + + it("deploy scripts restart only gittensory with --no-deps", () => { + expect(imageScript).toContain('up -d --no-build --no-deps "$SERVICE"'); + expect(prebuiltScript).toContain('up -d --no-deps "$SERVICE"'); + }); + + it("post-update script probes /ready without mutating operator-owned state", () => { + expect(postUpdateScript).toContain("/ready"); + expect(postUpdateScript).toContain("GITTENSORY_IMAGE"); + expect(postUpdateScript).toContain("GITTENSORY_VERSION"); + expect(postUpdateScript).toContain("SENTRY_RELEASE"); + expect(postUpdateScript).not.toContain("env_put"); + expect(postUpdateScript).not.toContain("docker compose down"); + }); +});