diff --git a/.claude/skills/update-account-server/SKILL.md b/.claude/skills/update-account-server/SKILL.md index 2db5aa6c..cccf13e2 100644 --- a/.claude/skills/update-account-server/SKILL.md +++ b/.claude/skills/update-account-server/SKILL.md @@ -56,12 +56,15 @@ ssh root@69.63.206.178 "cat /root/Container/gamma-account/Caddyfile" | diff - Show the user any difference and copy a file over (`git show :cloud/deploy/ | ssh root@69.63.206.178 "cat > /root/Container/gamma-account/"`) only once they agree. `.env` is never copied — new variables from `.env.example` (`git diff .. -- cloud/deploy/.env.example`) are -named to the user to add by hand. The same `compose.yml` also pins the -`demo` service's image tag (demo.gammapdf.com, the `update-demo-server` -skill): a diff on that one line is the demo's own pin, not drift — keep the -host's tag when copying the file over (never let this skill move the demo -to an older image), and `up -d` restarts only the services whose image or -config changed. +named to the user to add by hand. + +The public demo (demo.gammapdf.com) is NOT in this project: it is its own +compose project in `/root/Container/gamma-demo/` (the `update-demo-server` +skill; never touch it from here). This project holds only its way in: the +Caddyfile's `@demo` handle and Caddy on the external network `gamma-edge`, +where the demo answers as `gamma-demo`. `compose.yml` refuses to start while +that network is missing; on a new host create it first +(`docker network inspect gamma-edge >/dev/null 2>&1 || docker network create gamma-edge`). ## Update diff --git a/.claude/skills/update-demo-server/SKILL.md b/.claude/skills/update-demo-server/SKILL.md index 02783e04..51f904b4 100644 --- a/.claude/skills/update-demo-server/SKILL.md +++ b/.claude/skills/update-demo-server/SKILL.md @@ -1,34 +1,40 @@ --- name: update-demo-server -description: Build the Gamma server image from a branch by dispatching docker.yml (only :sha-, never :latest), then pin that tag for the `demo` service on the VPS behind demo.gammapdf.com and restart it. No merge to main is needed. +description: Build the Gamma server image from a branch by dispatching docker.yml (only :sha-, never :latest), then pin that tag in the demo's own compose project on the VPS (/root/Container/gamma-demo, demo.gammapdf.com) and restart it. No merge to main is needed. --- # Updating the public demo on the VPS `demo.gammapdf.com` is a Gamma in demo mode ([docs/dev/guests.md](../../../docs/dev/guests.md) -"Demo mode"): the `demo` service of the compose project on the VPS -`root@69.63.206.178`, folder `/root/Container/gamma-account/`, next to -`account`, `share` and `caddy` (Cloudflare in front, Caddy's `*.gammapdf.com` -site proxying the name to `demo:9001`). It runs `ghcr.io/tim4431/gamma` -pinned by a `sha-` tag. `.github/workflows/docker.yml` dispatched on a -branch without a version pushes exactly that tag and never moves `:latest`, -which the NAS pulls. This skill builds through it and pins the result in the -HOST's `compose.yml`. The host's `demo` image line is the one that counts: it -is usually ahead of the one in the repository. Setup and first deployment: -[cloud/deploy/README.md](../../../cloud/deploy/README.md) "The demo server". +"Demo mode"). It is its OWN compose project on the VPS `root@69.63.206.178`, +folder `/root/Container/gamma-demo/`: `compose.yml`, `.env` (one line, +`GAMMA_TAG=sha-`, the image to run), `demo.env` (its settings) and +`data/` (its whole state). It runs `ghcr.io/tim4431/gamma:${GAMMA_TAG}` and +answers as `gamma-demo` on the external Docker network `gamma-edge`, where the +account project's Caddy (`/root/Container/gamma-account/`) routes the name to +it. The repository's copies are in `cloud/deploy/demo/`; setup and the layout: +[cloud/deploy/demo/README.md](../../../cloud/deploy/demo/README.md). + +`.github/workflows/docker.yml` dispatched on a branch without a version pushes +exactly the `sha-` tag and never moves `:latest`, which the NAS pulls. +This skill builds through it and pins the result by writing the demo's `.env`. Workflows: [docs/dev/github_actions.md](../../../docs/dev/github_actions.md#dockeryml). -Never read out, copy off the host or overwrite `.env`, `share.env`, -`demo.env`, `data/` or `share-data/`. Restart only `demo` (plus a Caddy -reload when the Caddyfile changed). `account` and `share` belong to the -`update-account-server` skill. Never commit. Never pass `-f version` to -`docker.yml`: a version adds the release tags, and with them `:latest`. +Rules: -In every command below, `` is the full `headSha` and `` is -`sha-` + its first 7 characters (`echo sha-${sha:0:7}`). The metadata -action's `type=sha` truncates to 7; `git rev-parse --short` may print more -and must not be used for the tag. The pinned `share` tag (`sha-a0d31c6`) shows -the shape. +- Work only in `/root/Container/gamma-demo/`. Never run a compose command in + `gamma-account/` from this skill, never restart `account`, `share` or + `caddy`: they belong to the `update-account-server` skill. The demo's route + (the Caddyfile's `@demo` handle, Caddy on `gamma-edge`) lives there too; if + it is missing, say so and point to that skill. +- Never read out, copy off the host or overwrite `demo.env` or `data/`. +- Never commit. Never pass `-f version` to `docker.yml`: a version adds the + release tags, and with them `:latest`. + +In every command below, `` is the full `headSha` and `` is `sha-` +plus its first 7 characters (`echo sha-${sha:0:7}`). The metadata action's +`type=sha` truncates to 7; `git rev-parse --short` may print more and must not +be used for the tag. ## 1. What will ship @@ -48,18 +54,17 @@ git log --oneline origin/.. ## 2. Build (or reuse a build) -A commit that already has a green `docker.yml` run has its tag in GHCR -(every run, including a push to `main`, pushes `sha-`). Look before -building: +A commit that already has a green `docker.yml` run has its tag in GHCR (every +run, including a push to `main`, pushes `sha-`). Look before building: ```bash git rev-parse origin/ gh run list --workflow docker.yml --commit --status success --limit 1 --json databaseId,headSha,event,url ``` -If a run is listed, skip the build and use its `headSha`. To redeploy an -older published commit without building (the user names it, or pick one -from `gh run list --workflow docker.yml --status success --limit 10 --json headSha,headBranch,event,createdAt`), +If a run is listed, skip the build and use its `headSha`. To redeploy an older +published commit without building (the user names it, or pick one from +`gh run list --workflow docker.yml --status success --limit 10 --json headSha,headBranch,event,createdAt`), use that `headSha` and go to step 3. Otherwise dispatch and find the run: @@ -69,180 +74,137 @@ gh workflow run docker.yml --ref gh run list --workflow docker.yml --branch --event workflow_dispatch --limit 1 --json databaseId,headSha,status,url ``` -The run may take a few seconds to appear. List it again rather than guess. -Its `headSha` must equal `git rev-parse origin/`. Then wait: +The run may take a few seconds to appear; list it again rather than guess. Its +`headSha` must equal `git rev-parse origin/`. Then wait (run it in the +background; a multi-arch build under QEMU takes about 10 minutes, longer +without a warm cache): ```bash gh run watch --exit-status ``` -It is a multi-arch build (amd64 and arm64 under QEMU), about 10 minutes, -longer without a warm cache. If it is red, report -`gh run view --log-failed` and stop: no tag was pushed and nothing -changes on the host. `:latest` is untouched either way (the tag rules are -explained in docker.yml's header). - -Note `` and ``. +Red → report `gh run view --log-failed` and stop: no tag was pushed +and nothing changes on the host. ## 3. What runs now ```bash -ssh root@69.63.206.178 "cd /root/Container/gamma-account && grep -A3 '^ demo:' compose.yml | grep 'image:' ; docker inspect --format '{{index .Config.Labels \"org.opencontainers.image.revision\"}}' \$(docker compose ps -q demo)" +ssh root@69.63.206.178 "cd /root/Container/gamma-demo && cat .env && docker inspect --format '{{index .Config.Labels \"org.opencontainers.image.revision\"}}' \$(docker compose ps -q demo)" ``` -This prints the pinned image line (keep the old tag for rollback) and the -running commit. If the revision equals ``, the image needs no deploy: -say so, run step 4 only if `cloud/deploy/` changed since that commit, and -otherwise stop. - -- No `demo:` service in the host's `compose.yml`, or no `demo.env` - (`ssh root@69.63.206.178 "ls /root/Container/gamma-account"`), means this - is the FIRST deployment. Follow cloud/deploy/README.md "The demo server" - with the user. `demo.env` must exist before the new `compose.yml` is - copied over: while a file named by `env_file` is missing, every - `docker compose` command in that folder fails, the account server's - included. Create it from the example only with the user's agreement and - only if it is absent: - `git show :cloud/deploy/demo.env.example | ssh root@69.63.206.178 "cd /root/Container/gamma-account && test ! -e demo.env && cat > demo.env && chmod 600 demo.env"`. - The user sets `GAMMA_ADMIN_PASSWORD` or reads the one-time random password - from the log themselves (step 6). Do not paste it into the chat. +This prints the pinned tag (keep it for rollback) and the running commit. If +the revision equals ``, the image needs no deploy: say so, do step 4 only +if `cloud/deploy/demo/` changed since that commit, and otherwise stop. + +No `/root/Container/gamma-demo/` at all means a first deployment: follow +[cloud/deploy/demo/README.md](../../../cloud/deploy/demo/README.md) "First +deployment" with the user. ## 4. Deploy files changed? -The host keeps its own `compose.yml` and `Caddyfile`. Compare them with the -commit being deployed: +Compare the host's `compose.yml` with the commit being deployed: + +```bash +ssh root@69.63.206.178 "cat /root/Container/gamma-demo/compose.yml" | diff --strip-trailing-cr - <(git show :cloud/deploy/demo/compose.yml) +``` + +The file carries no pin, so any difference is a real change. Show it to the +user and copy it only once they agree (a backup stays next to it): + +```bash +git show :cloud/deploy/demo/compose.yml | ssh root@69.63.206.178 "cd /root/Container/gamma-demo && cp compose.yml compose.yml.bak && cat > compose.yml && docker compose config -q" +``` + +`demo.env` is never copied or edited. List the variables the example gained or +lost since the running commit, and the names (names only, never values) the +host's file sets: ```bash -ssh root@69.63.206.178 "cat /root/Container/gamma-account/compose.yml" | diff - <(git show :cloud/deploy/compose.yml) -ssh root@69.63.206.178 "cat /root/Container/gamma-account/Caddyfile" | diff - <(git show :cloud/deploy/Caddyfile) +git diff .. -- cloud/deploy/demo/demo.env.example +ssh root@69.63.206.178 "grep -o '^[A-Z_]*=' /root/Container/gamma-demo/demo.env" ``` -- The `demo` image line normally differs, because the host is ahead. That - difference alone is no reason to copy anything. -- For any other difference, show it to the user and copy the file only once - they agree. A copied `compose.yml` resets the `demo` line to the - repository's tag, and step 5 then re-pins it. Changes it makes to - `account` or `share` (e.g. a new `share` tag) take effect only at their - next `docker compose up -d`. Tell the user; this skill does not restart - them. - - ```bash - git show :cloud/deploy/compose.yml | ssh root@69.63.206.178 "cd /root/Container/gamma-account && cp compose.yml compose.yml.bak && cat > compose.yml" - ``` - -- The Caddyfile is bind-mounted as a single file, so write it IN PLACE - (`cat >`, which keeps the inode). Never use `sed -i`, `mv` or `scp`, which - replace the file and leave the container reading the old one. Then reload, - and restore the old copy if the reload refuses the new one (the running - config stays the old one when a reload fails): - - ```bash - git show :cloud/deploy/Caddyfile | ssh root@69.63.206.178 "cd /root/Container/gamma-account && cp Caddyfile Caddyfile.bak && cat > Caddyfile && (docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || { cat Caddyfile.bak > Caddyfile; echo 'reload refused: Caddyfile restored'; exit 1; })" - ``` - -- `demo.env` is never copied or edited. List the variables the example - gained or lost since the running commit, and the names (names only, - never values) the host's file sets: - - ```bash - git diff .. -- cloud/deploy/demo.env.example - ssh root@69.63.206.178 "grep -o '^[A-Z_]*=' /root/Container/gamma-account/demo.env" - ``` - - Name any new variable to the user to add by hand (then `up -d demo` - applies it). +Name any new variable to the user to add by hand; step 5's `up -d` applies it. ## 5. Pin the tag and restart -One script on the host. It checks the tag exists before touching the file, -rewrites ONLY the image line inside the `demo:` service (the range runs from -` demo:` to the next line indented two spaces or less; `share` uses the same -image name with another tag and stays as it is), refuses and restores if -anything else changed, then pulls and recreates `demo` alone: +One script on the host. It checks the tag format and that the image exists +before touching anything, writes `.env` (the old one kept as `.env.bak`), +checks the resolved image, then pulls and recreates the demo: ```bash ssh root@69.63.206.178 bash -s -- <<'EOF' set -eu TAG="$1" -IMG=ghcr.io/tim4431/gamma echo "$TAG" | grep -Eq '^sha-[0-9a-f]{7}$' || { echo "not a sha-<7 hex> tag: $TAG"; exit 1; } -cd /root/Container/gamma-account -test -f demo.env || { echo "demo.env missing: first deployment, see cloud/deploy/README.md"; exit 1; } -grep -q '^ demo:' compose.yml || { echo "no demo service in compose.yml: first deployment, see cloud/deploy/README.md"; exit 1; } -docker pull -q "$IMG:$TAG" -cp compose.yml compose.yml.pre-demo-pin -sed -i "/^ demo:[[:space:]]*\$/,/^ \{0,2\}[a-z]/ s#^\( image: ghcr\.io/tim4431/gamma:\)sha-[0-9a-f]*[[:space:]]*\$#\1$TAG#" compose.yml -diff compose.yml.pre-demo-pin compose.yml || true -if ! docker compose config demo | grep -q "image: $IMG:$TAG\$"; then - cp compose.yml.pre-demo-pin compose.yml; echo "the demo image line was not rewritten; compose.yml restored"; exit 1 -fi -if [ "$(diff compose.yml.pre-demo-pin compose.yml | grep -c '^>')" -gt 1 ]; then - cp compose.yml.pre-demo-pin compose.yml; echo "more than one line changed; compose.yml restored"; exit 1 +cd /root/Container/gamma-demo +docker pull -q "ghcr.io/tim4431/gamma:$TAG" +cp .env .env.bak +printf 'GAMMA_TAG=%s\n' "$TAG" > .env +if ! docker compose config demo | grep -q "image: ghcr.io/tim4431/gamma:$TAG\$"; then + cp .env.bak .env; echo "compose does not resolve the new tag; .env restored"; exit 1 fi -docker compose pull demo -docker compose up -d demo +docker compose up -d EOF ``` -The printed `diff` shows the one changed line (none if the tag was already -pinned). `up -d demo` recreates only `demo`: guests lose their session for -the seconds of the restart and keep their workspaces. At start the image -upgrades the data directory itself (`manage.py migrate`, snapshot first into -`demo-data/backups/`). It refuses a directory written by a newer build, so a -`demo` that keeps restarting needs its log read before anything else. +`up -d` recreates the demo alone: guests lose their connection for the seconds +of the restart and keep their workspaces. At start the image upgrades the data +directory itself (`manage.py migrate`, snapshot first into `data/backups/`). It +refuses a directory written by a newer build, so a `demo` that keeps +restarting needs its log read before anything else. -Offer to set the same tag on the `demo` image line of the repository's -`cloud/deploy/compose.yml` (a working-tree edit left for the user to commit, -never committed here). Then a later copy of the file, by this skill or by -`update-account-server`, does not roll the demo back. +Offer to set the same tag in the repository's `cloud/deploy/demo/.env.example` +(a working-tree edit left for the user to commit, never committed here), so a +first deployment from the repository starts on a current build. ## 6. Verify ```bash -ssh root@69.63.206.178 "cd /root/Container/gamma-account && docker compose ps && docker compose logs --tail 20 demo" -ssh root@69.63.206.178 "cd /root/Container/gamma-account && docker inspect --format '{{index .Config.Labels \"org.opencontainers.image.revision\"}}' \$(docker compose ps -q demo)" +ssh root@69.63.206.178 "cd /root/Container/gamma-demo && docker compose ps && docker compose logs --tail 20 demo" +ssh root@69.63.206.178 "cd /root/Container/gamma-demo && docker inspect --format '{{index .Config.Labels \"org.opencontainers.image.revision\"}}' \$(docker compose ps -q demo)" curl -s https://demo.gammapdf.com/api/server-config | grep -o '"demo": *true' # "demo":true curl -s -o /dev/null -w "%{http_code}\n" https://demo.gammapdf.com/ # 200 ``` -- `demo` is `Up … (healthy)`. The image's healthcheck needs up to ~30 s +- `demo` is `Up … (healthy)`; the image's healthcheck needs up to about 30 s after start. Its revision label equals ``. - `server-config` reports `"demo":true`. If it is missing, `GAMMA_DEMO=1` is - not in `demo.env` (or the build predates demo mode). A Cloudflare 521/502 - means Caddy or the container is not reachable: check - `docker compose logs caddy`. A 404 from the demo name means the host's - Caddyfile lacks the `@demo` handle (step 4). -- On a first deployment the admin's one-time password is in the log. Give - the user the command (`docker compose logs demo | grep -A2 "created the admin account"`) - rather than its output. The shared AI key, "Guests may use it" and the - allowance are the admin's to set in the GUI (Settings → Server → Shared AI - provider). AI keys never go into `demo.env`. - -Report the old → new tag and commit (short sha + subject), the run link if -one was built, and anything unusual in the log (a migration step, errors). + not in `demo.env` (or the build predates demo mode). +- A 502 from the demo's name means Caddy cannot reach `gamma-demo`: check + `docker network inspect gamma-edge` lists both the demo and Caddy. A 404 + means the account project's Caddyfile lacks the `@demo` handle. Both are + the `update-account-server` skill's to fix; tell the user. +- On a first deployment the admin's one-time password is in the log. Give the + user the command (`docker compose logs demo | grep -A2 "created the admin account"`) + rather than its output. The shared AI connection, "Guests may use it" and + the allowance are the admin's to set in the GUI (Settings → Server → Shared + AI provider). AI keys never go into `demo.env`. + +Report the old → new tag and commit (short sha + subject), the run link if one +was built, and anything unusual in the log (a migration step, errors). ## Resetting the demo (only when the user asks) -`demo-data/` is the demo's whole state (guest accounts and workspaces, the -admin account, the shared AI key, the settings) and it is disposable, but +`data/` is the demo's whole state (guest accounts and workspaces, the admin +account, the shared AI connection, the settings) and it is disposable, but wipe it only on the user's explicit request: ```bash -ssh root@69.63.206.178 "cd /root/Container/gamma-account && docker compose stop demo && rm -rf demo-data && docker compose up -d demo" +ssh root@69.63.206.178 "cd /root/Container/gamma-demo && docker compose down && rm -rf data && docker compose up -d" ``` -The instance starts fresh. The admin is seeded again, with a new random -password in the log unless `demo.env` sets one, and the shared AI key, +The instance starts fresh: the admin is seeded again, with a new random +password in the log unless `demo.env` sets one, and the shared AI connection, "Guests may use it" and the allowance must be entered again. A -`GAMMA_GUEST_SEED` zip kept in `demo-data/` is gone too. Move it aside first -(`mv demo-data/guest-seed.zip .` before the `rm`, then -`mkdir -p demo-data && mv guest-seed.zip demo-data/` before the `up`) if one -is in use. +`GAMMA_GUEST_SEED` zip kept in `data/` goes too. Move it aside first +(`mv data/guest-seed.zip .` before the `rm`, then +`mkdir -p data && mv guest-seed.zip data/` before the `up`) if one is in use. ## Rollback -Run step 5 again with the previous tag from step 3. If the newer build -already upgraded the data directory, the older one refuses it and keeps -restarting. Then either restore the snapshot the upgrade took in -`demo-data/backups/