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:

Updating and rolling back

- 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. -

+ Day-two operator flow: pull or build a new app image, restart only the{" "} + 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. +

+ + + + + + + +

Preflight checklist

+
    +
  1. + Read release notes for migration or env changes — migrations are forward-only (see + Rollback below). +
  2. +
  3. + Take a fresh backup when the release may change schema — see{" "} + Backup and scaling. +
  4. +
  5. + Source path only: git pull and confirm git status is clean (no + uncommitted local changes the build would silently pick up). +
  6. +
  7. + Image path only: note the current tag or digest from docker inspect on the + running gittensory container so rollback has a known-good target. +
  8. +
  9. + Confirm routine health is green before you start —{" "} + curl http://localhost:8787/ready and a quick docker compose ps. +
  10. +

Path 1: pull a published image

@@ -667,7 +723,42 @@ GITTENSORY_IMAGE=ghcr.io/jsonbored/gittensory-selfhost@sha256:... ./scripts/depl even when those three are present).

-

Rollback: no dedicated command today

+

Post-update checklist

+
    +
  1. + Wait for the deploy script's health wait to finish (or run the helper below if you + updated manually with plain docker compose). +
  2. +
  3. + curl http://localhost:8787/ready returns HTTP 200. +
  4. +
  5. + docker compose ps gittensory shows healthy. +
  6. +
  7. + Tail logs for selfhost_listening and, on first boot after a schema bump,{" "} + selfhost_migrations_applied — not selfhost_job_dead. +
  8. +
  9. + Confirm the release id — neither /health nor /ready exposes a + version string; check .env and the running container image instead. +
  10. +
+ +

+ If any check fails, see Troubleshooting + . +

+ +

Rollback: no dedicated command

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 and after any update

-

Before updating:

- -

- 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. -

-

Uninstalling and decommissioning

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"); + }); +});