mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Same guard as Start-Codeman.sh's own (docs/docker-self-update.md-adjacent incident, 2026-09-21): docker-compose.yaml hard-codes `name: codeman`, so a second checkout run without COMPOSE_PROJECT_NAME resolves to the SAME Compose project as any other checkout on the host. It has to live here too, not just in Start-Codeman.sh: this script's own --no-cache build and its `down`/`down --volumes` both run BEFORE the handoff at the bottom of the file, so Start-Codeman.sh's copy of the guard would only fire after this script's own destructive calls already ran — and its default `down --volumes` is more destructive than Start-Codeman.sh's own targeted refresh, clearing every named volume the resolved project has. Also fixes a real bug the same guard shipped with: under `set -o pipefail`, `grep -v` legitimately exits 1 when nothing survives the filter (the ordinary, no-collision case), and without `|| true` on the pipeline that non-zero status propagates through the command substitution and `set -e` aborts the WHOLE script at the guard — every time, collision or not. Caught only by actually executing the guard end-to-end against a stub `docker` (the existing smoke-test harness), never by a static text/regex check on the source; the stub's `config --format json` response was also fixed to pretty-print like real Compose does, since a compact one-liner silently resolved project_name to empty and exercised neither script's guard the way production output does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuHtuPiHXdykq9T6rKQJ9n
213 lines
10 KiB
Bash
213 lines
10 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# The scripted major-update path for the Docker Compose deployment.
|
|
#
|
|
# docker/README.md and docs/docker-self-update.md both point operators here for
|
|
# anything the in-app updater itself refuses to apply: a changed
|
|
# `server.Dockerfile`, a changed `docker-compose.yaml`, or a new required
|
|
# `.env.example` key. None of those can be applied by a container restarting
|
|
# itself — a restart reuses the existing image and configuration (see "The
|
|
# environment gate" in docs/docker-self-update.md) — so this script does the
|
|
# three things an in-place update cannot: force a real image rebuild with no
|
|
# layer cache, stop the stack, then hand off to Start-Codeman.sh for the same
|
|
# careful PUID/PGID, override-file and fingerprint handling every other start
|
|
# goes through.
|
|
#
|
|
# ⚠️ Build BEFORE stopping the stack, deliberately, same reasoning as
|
|
# Start-Codeman.sh's own build-then-down ordering: the build needs nothing
|
|
# stopped, so a slow --no-cache rebuild costs no downtime, and a build failure
|
|
# (a bad Dockerfile edit, a network blip pulling a base image) leaves the
|
|
# ALREADY-RUNNING stack untouched instead of stopped with nothing to bring it
|
|
# back.
|
|
#
|
|
# ⚠️ Clears the codeman-node-modules/codeman-dist named volumes by DEFAULT.
|
|
# Docker seeds a named volume from the image only while that volume is EMPTY,
|
|
# so a rebuilt image's fresh node_modules/dist otherwise sit unused behind a
|
|
# volume's old content and the container comes back up looking unchanged —
|
|
# exactly wrong for a script whose whole point is "be certain of what ships".
|
|
# Start-Codeman.sh only clears them when it detects the checkout's HEAD or
|
|
# `package-lock.json` moved, which is right for its own ordinary-start case but
|
|
# too narrow here: nothing about a Dockerfile-only change (the case that sends
|
|
# people to this script in the first place) touches either of those. Pass
|
|
# --keep-volumes to opt out and reuse whatever is already in them.
|
|
#
|
|
# Usage: docker/Update-Codeman.sh [--keep-volumes]
|
|
# --keep-volumes Do not clear codeman-node-modules/codeman-dist. Safe to
|
|
# combine with a source change Start-Codeman.sh's own
|
|
# detection would have cleared anyway; unsafe if the reason
|
|
# you are here is a change to server.Dockerfile alone.
|
|
|
|
set -euo pipefail
|
|
|
|
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
env_file="$script_dir/.env"
|
|
compose_file="$script_dir/docker-compose.yaml"
|
|
|
|
keep_volumes=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--keep-volumes)
|
|
keep_volumes=1
|
|
;;
|
|
--help | -h)
|
|
printf 'Usage: bash %s [--keep-volumes]\n' "$0"
|
|
exit 0
|
|
;;
|
|
*)
|
|
printf 'Error: unrecognised argument: %s\n' "$arg" >&2
|
|
printf 'Usage: bash %s [--keep-volumes]\n' "$0" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -f "$env_file" ]]; then
|
|
printf 'Error: Docker environment file is missing: %s\n' "$env_file" >&2
|
|
printf 'Create it from %s/.env.example before running this script.\n' "$script_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Same override-file discovery as Start-Codeman.sh, and deliberately kept in
|
|
# step with it: a stack built here and started there must resolve to the exact
|
|
# same Compose files, or this script's build could target a configuration the
|
|
# handoff's own `up` never actually uses. Compose's own precedence (measured on
|
|
# v5.5.0 with both present: it uses .yml and ignores .yaml).
|
|
override_yml="$script_dir/docker-compose.override.yml"
|
|
override_yaml="$script_dir/docker-compose.override.yaml"
|
|
if [[ -f "$override_yml" && -f "$override_yaml" ]]; then
|
|
printf 'Warning: both %s and %s exist; Compose uses .yml and ignores .yaml.\n' \
|
|
"$override_yml" "$override_yaml" >&2
|
|
fi
|
|
compose_files=(-f "$compose_file")
|
|
for override_file in "$override_yml" "$override_yaml"; do
|
|
if [[ -f "$override_file" ]]; then
|
|
compose_files+=(-f "$override_file")
|
|
printf 'Using Compose override file: %s\n' "$override_file"
|
|
break
|
|
fi
|
|
done
|
|
compose_command=(docker compose --env-file "$env_file" "${compose_files[@]}")
|
|
|
|
# Same collision guard as Start-Codeman.sh, and load-bearing HERE rather than
|
|
# left to that script's own copy: this script's --no-cache build and its
|
|
# `down`/`down --volumes` (below) both run BEFORE the handoff at the bottom of
|
|
# this file, so Start-Codeman.sh's guard would only fire after the damage this
|
|
# one exists to prevent has already happened. docker-compose.yaml hard-codes
|
|
# `name: codeman`, so a second checkout run without COMPOSE_PROJECT_NAME
|
|
# resolves to the SAME Compose project as any other checkout on the host and
|
|
# operates on ITS containers and volumes — this script's default `down
|
|
# --volumes` makes that worse than Start-Codeman.sh's own targeted refresh,
|
|
# since it clears every named volume the resolved project has, not just the
|
|
# two this script means to. See Start-Codeman.sh's own guard for the full
|
|
# incident this is written against (2026-09-21).
|
|
project_name=$(
|
|
"${compose_command[@]}" config --format json 2>/dev/null |
|
|
sed -n 's/^[[:space:]]*"name":[[:space:]]*"\([^"]*\)".*$/\1/p' | head -n1
|
|
)
|
|
if [[ -n "$project_name" ]]; then
|
|
# `|| true` on the pipeline's LAST command: under `set -o pipefail`, `grep -v`
|
|
# exits 1 when nothing survives the filter — the ordinary, no-collision case,
|
|
# since `docker ps` finds nothing at all on a first-ever deployment or a
|
|
# single matching (own) working_dir gets filtered out. Without it, that exit
|
|
# status propagates through the command substitution and `set -e` aborts the
|
|
# WHOLE script right here, every time, regardless of whether a collision
|
|
# actually exists — caught only by actually running this end-to-end (a
|
|
# static text/regex check on the source cannot see it).
|
|
other_working_dir=$(
|
|
docker ps -a --filter "label=com.docker.compose.project=$project_name" \
|
|
--format '{{.Label "com.docker.compose.project.working_dir"}}' 2>/dev/null |
|
|
grep -v -F -x -- "$script_dir" | head -n1 || true
|
|
)
|
|
if [[ -n "$other_working_dir" ]]; then
|
|
printf 'Error: Compose project "%s" is already in use by a DIFFERENT checkout:\n' "$project_name" >&2
|
|
printf ' %s\n' "$other_working_dir" >&2
|
|
printf 'This checkout is:\n' >&2
|
|
printf ' %s\n' "$script_dir" >&2
|
|
printf '\n' >&2
|
|
printf 'docker-compose.yaml hard-codes `name: %s`, so two checkouts on the same host\n' "$project_name" >&2
|
|
printf 'collide unless each one sets a distinct COMPOSE_PROJECT_NAME. Continuing would\n' >&2
|
|
printf 'rebuild and stop the OTHER checkout'"'"'s running container and, by default,\n' >&2
|
|
printf 'delete ALL of its named volumes.\n' >&2
|
|
printf '\n' >&2
|
|
printf 'Fix: export COMPOSE_PROJECT_NAME=<something-unique-to-this-checkout> before\n' >&2
|
|
printf 'running this script, then retry.\n' >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Same owner-detection Start-Codeman.sh uses to derive PUID/PGID for its own
|
|
# build — without it, the --no-cache build below gets Compose's untouched
|
|
# default of 1000:1000, and on any host whose appdata owner differs (99:100 on
|
|
# Unraid, per docker/README.md's chown example), Start-Codeman.sh's own
|
|
# correctly-PUID'd build during the handoff then rebuilds those layers with the
|
|
# right values anyway — so the "no cache, certain of what ships" image this
|
|
# script produces is not the one that actually ends up running.
|
|
#
|
|
# Deliberately NOT the same as Start-Codeman.sh's own handling of a MISSING
|
|
# appdata directory (which creates it): this script updates an EXISTING
|
|
# deployment, so a missing appdata path means there is nothing here yet to
|
|
# update, and creating one would just be this script quietly doing
|
|
# Start-Codeman.sh's first-run job worse.
|
|
appdata_path=$(
|
|
"${compose_command[@]}" config --environment |
|
|
awk -F= '$1 == "CODEMAN_APPDATA_PATH" { sub(/^[^=]*=/, ""); print; exit }'
|
|
)
|
|
if [[ -z "$appdata_path" || ! -d "$appdata_path" ]]; then
|
|
printf 'Error: CODEMAN_APPDATA_PATH is not set or does not exist: %s\n' "${appdata_path:-<unset>}" >&2
|
|
printf 'Run docker/Start-Codeman.sh first to set up a new deployment.\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# `stat -c` is GNU, `stat -f` is BSD/macOS; the bind source lives on the Docker
|
|
# host, so both need to work. Identical to Start-Codeman.sh's own helper.
|
|
owner_of() {
|
|
stat -c '%u:%g' -- "$1" 2>/dev/null || stat -f '%u:%g' "$1" 2>/dev/null
|
|
}
|
|
|
|
if ! owner_ids=$(owner_of "$appdata_path"); then
|
|
printf 'Error: Cannot determine the owner of CODEMAN_APPDATA_PATH: %s\n' "$appdata_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
export PUID=${owner_ids%%:*}
|
|
export PGID=${owner_ids##*:}
|
|
|
|
if [[ "$PUID" == '0' ]]; then
|
|
printf 'Error: CODEMAN_APPDATA_PATH is owned by root: %s\n' "$appdata_path" >&2
|
|
printf 'Change the directory ownership to the unprivileged account that should run Codeman.\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --no-cache, always: a plain `build` reuses cached layers (npm install, apt
|
|
# packages, the CLI installs baked into the image) and can silently keep them
|
|
# frozen at whatever they were the day the cache was populated — exactly wrong
|
|
# for a major update, whose whole point is being certain of what actually
|
|
# ships. `scripts/build-agent-image.mjs` makes the same call for the same
|
|
# reason (see its entry in CLAUDE.md's Additional Commands table). Runs BEFORE
|
|
# the stack is stopped — see the header comment for why.
|
|
printf 'Building a fresh image (--no-cache)...\n'
|
|
"${compose_command[@]}" build --no-cache
|
|
|
|
printf 'Stopping the stack...\n'
|
|
if [[ "$keep_volumes" == '1' ]]; then
|
|
"${compose_command[@]}" down
|
|
else
|
|
printf 'Also clearing the codeman-node-modules/codeman-dist volumes (pass --keep-volumes to skip).\n'
|
|
"${compose_command[@]}" down --volumes
|
|
fi
|
|
|
|
# Start-Codeman.sh does everything a plain `up -d` does not: re-derives
|
|
# PUID/PGID, pre-creates CODEMAN_CASES_PATH with the right ownership, resolves
|
|
# DOCKER_SOCKET_GID, records the server.Dockerfile/docker-compose.yaml
|
|
# fingerprint the in-app updater's gate reads on every future update, and
|
|
# starts the (already freshly built) image. Reimplementing any of that here
|
|
# would only risk drifting out of step with it — hand off instead, exactly as
|
|
# docs/docker-self-update.md's own reset procedure does.
|
|
#
|
|
# ⚠️ `bash`, not a bare exec of the path: Start-Codeman.sh is committed
|
|
# non-executable (100644), the same as this script, and is documented
|
|
# everywhere as `bash docker/Start-Codeman.sh` rather than
|
|
# `./docker/Start-Codeman.sh` — execing the bare path fails with EACCES.
|
|
printf 'Handing off to Start-Codeman.sh...\n'
|
|
exec bash "$script_dir/Start-Codeman.sh"
|