mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
#373 restarts the Compose container by exiting the server, which is right for the shipped deployment: `restart: unless-stopped` relaunches it. The updater verified that policy through the Docker socket and, when it could not (no socket mounted), failed open and exited anyway. Failing open is the correct choice for the GATE, where refusing would block every install without a socket, but not for the kill: a container the daemon does not restart goes down for good, with no UI left to recover it from. That is exactly the case a plain `docker run` of this image without `--restart` produces, and the image sets CODEMAN_IN_CONTAINER=1 itself, so it takes the container path. The decision now happens server-side, where both the socket and the Compose env are reachable, and rides down to the script as `--restart-by-exit 0|1`. It is 1 when the Compose file declared `CODEMAN_RESTART_BY_EXIT=1` (added there and only there, since that file is what sets the restart policy; the image ENV deliberately does not) or when the daemon confirmed an auto-restart policy. Otherwise the build still lands, the status becomes `completed-needs-manual-restart` with the `docker restart` hint, and the server keeps running. The shipped deployment is unchanged in effect: with the socket it was already confirmed, and without it the declaration now covers it. Also: a root-run `Start-Codeman.sh` (common on Unraid) created the fingerprint baseline's `.codeman` directory before the container's first start and left it root-owned, which the unprivileged server could then never write its own state into. It is chowned to PUID:PGID when running as root. Verified with a real image build of the merged tree (classic builder; this box's BuildKit lacks buildx): runs as uid 1000, tsc/esbuild and the toolchain present, the four CLIs at their pins, docker/.env absent, and `docker inspect $HOSTNAME` returns the restart policy through the mounted socket as that user. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qg6bcATm1pNNY4kQWGwzgu
130 lines
5.0 KiB
Bash
130 lines
5.0 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
env_file="$script_dir/.env"
|
|
compose_file="$script_dir/docker-compose.yaml"
|
|
|
|
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 starting Codeman.\n' "$script_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
compose_command=(docker compose --env-file "$env_file" -f "$compose_file")
|
|
appdata_path=$(
|
|
"${compose_command[@]}" config --environment |
|
|
awk -F= '$1 == "CODEMAN_APPDATA_PATH" { sub(/^[^=]*=/, ""); print; exit }'
|
|
)
|
|
docker_socket=$(
|
|
"${compose_command[@]}" config --environment |
|
|
awk -F= '$1 == "DOCKER_SOCKET" { sub(/^[^=]*=/, ""); print; exit }'
|
|
)
|
|
|
|
if [[ -z "$appdata_path" ]]; then
|
|
printf 'Error: CODEMAN_APPDATA_PATH is not set in %s\n' "$env_file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "$appdata_path" ]]; then
|
|
if [[ "$EUID" == '0' ]]; then
|
|
printf 'Error: Refusing to create CODEMAN_APPDATA_PATH as root: %s\n' "$appdata_path" >&2
|
|
printf 'Create it as the unprivileged account that should run Codeman, then retry.\n' >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p -- "$appdata_path"
|
|
fi
|
|
|
|
if owner_ids=$(stat -c '%u:%g' -- "$appdata_path" 2>/dev/null); then
|
|
:
|
|
elif owner_ids=$(stat -f '%u:%g' "$appdata_path" 2>/dev/null); then
|
|
:
|
|
else
|
|
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
|
|
|
|
if [[ -z "$docker_socket" || ! -S "$docker_socket" ]]; then
|
|
printf 'Error: DOCKER_SOCKET is not a Unix socket: %s\n' "${docker_socket:-<unset>}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if socket_ids=$(stat -c '%u:%g' -- "$docker_socket" 2>/dev/null); then
|
|
:
|
|
elif socket_ids=$(stat -f '%u:%g' "$docker_socket" 2>/dev/null); then
|
|
:
|
|
else
|
|
printf 'Error: Cannot determine the owner of DOCKER_SOCKET: %s\n' "$docker_socket" >&2
|
|
exit 1
|
|
fi
|
|
|
|
export DOCKER_SOCKET_GID=${socket_ids##*:}
|
|
|
|
repo_path=${CODEMAN_REPO_PATH:-$(cd -- "$script_dir/.." && pwd)}
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
printf 'Error: CODEMAN_REPO_PATH is not a directory: %s\n' "$repo_path" >&2
|
|
exit 1
|
|
fi
|
|
export CODEMAN_REPO_PATH="$repo_path"
|
|
|
|
# The in-app updater runs `git checkout` and `npm install` against this checkout
|
|
# as PUID:PGID. If the directory belongs to someone else, git refuses outright
|
|
# ("detected dubious ownership") and the update fails at the first step — so warn
|
|
# here, where the fix is obvious, rather than in a failed update hours later.
|
|
if repo_owner=$(stat -c '%u' -- "$repo_path" 2>/dev/null || stat -f '%u' "$repo_path" 2>/dev/null); then
|
|
if [[ "$repo_owner" != "$PUID" ]]; then
|
|
printf 'Warning: %s is owned by UID %s but Codeman runs as UID %s.\n' "$repo_path" "$repo_owner" "$PUID" >&2
|
|
printf 'In-app updates will fail until the ownership matches. Codeman itself still starts.\n' >&2
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -d "$repo_path/.git" ]]; then
|
|
printf 'Note: %s is not a git checkout, so in-app updates are unavailable.\n' "$repo_path" >&2
|
|
fi
|
|
|
|
# Record what the container is about to be built and created FROM. The in-app
|
|
# updater compares these against the release it wants to apply: a release that
|
|
# changes either file cannot be applied by the container restarting itself (a
|
|
# restart reuses the existing image and config), so it is refused and the user
|
|
# is sent back here. Written on every start, so the baseline always describes
|
|
# the container that is actually running. See docs/docker-self-update.md.
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256_of() { sha256sum -- "$1" | cut -d' ' -f1; }
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
sha256_of() { shasum -a 256 -- "$1" | cut -d' ' -f1; }
|
|
else
|
|
sha256_of() { printf ''; }
|
|
fi
|
|
|
|
dockerfile_sha=$(sha256_of "$script_dir/server.Dockerfile")
|
|
compose_sha=$(sha256_of "$compose_file")
|
|
if [[ -n "$dockerfile_sha" && -n "$compose_sha" ]]; then
|
|
# $CODEMAN_APPDATA_PATH is mounted at the runtime account's home, so this is
|
|
# dataPath('docker-env-applied.json') as the server inside the container sees it.
|
|
state_dir="$appdata_path/.codeman"
|
|
mkdir -p -- "$state_dir"
|
|
printf '{\n "dockerfileSha256": "%s",\n "composeSha256": "%s"\n}\n' \
|
|
"$dockerfile_sha" "$compose_sha" >"$state_dir/docker-env-applied.json.tmp"
|
|
mv -- "$state_dir/docker-env-applied.json.tmp" "$state_dir/docker-env-applied.json"
|
|
# A root-run start (common on Unraid) would otherwise leave a root-owned
|
|
# `.codeman` on a FIRST start, before the container has created it as PUID,
|
|
# and the unprivileged server could then never write its own state there.
|
|
if [[ "$EUID" == '0' ]]; then
|
|
chown -- "$PUID:$PGID" "$state_dir" "$state_dir/docker-env-applied.json"
|
|
fi
|
|
else
|
|
printf 'Warning: no sha256 tool found; in-app updates will not detect environment changes.\n' >&2
|
|
fi
|
|
|
|
exec docker compose --env-file "$env_file" -f "$compose_file" up --build -d
|