scripts/deploy.sh rolls a published image out to an installation set up with move-server.sh (DEPLOY_TARGET in .env): checks the image for the target's architecture, waits until no agent or terminal runs (--force skips), pulls and recreates, runs the smoke test and goes back to the previous version on failure. move-server.sh: --exclude leaves paths out of the workspace copy; values in .env may be quoted (WORKSPACE='/home/...'). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
5.4 KiB
Bash
Executable File
118 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Roll out a published CodeCraft image to a remote installation that was set up with
|
||
# scripts/move-server.sh (docker-compose.yml + .env in one folder on the target).
|
||
#
|
||
# scripts/release.sh # build and publish a new version (amd64 + arm64)
|
||
# scripts/deploy.sh # roll the newest tag out to the target
|
||
#
|
||
# Steps:
|
||
# 1. check that the image exists in the registry for the target's architecture,
|
||
# 2. wait until no agent or terminal runs on the target (unless --force),
|
||
# 3. set CODECRAFT_VERSION in the target's .env, docker compose pull && up -d
|
||
# (or start.sh on hosts without Compose),
|
||
# 4. run scripts/smoke-test.sh on the target; on failure the previous version is
|
||
# started again.
|
||
#
|
||
# Usage: scripts/deploy.sh [vYYYYMM-NNNNN] [--target user@host:/dir] [--force]
|
||
# [--wait MINUTES] [--dry-run]
|
||
# vYYYYMM-NNNNN version to deploy (default: the newest vYYYYMM-NNNNN git tag)
|
||
# --target installation folder on the target (default: DEPLOY_TARGET from .env)
|
||
# --force restart even if agents or terminals are running (they end)
|
||
# --wait N how long to wait for an idle server before giving up (default 15)
|
||
# --dry-run show the steps only
|
||
#
|
||
# Environment:
|
||
# REGISTRY image name without tag (default git.familie-zink.org/zink/codecraft)
|
||
# CONTAINER container name on the target (default codecraft)
|
||
# PORT port of CodeCraft on the target, for the smoke test (default: PORT
|
||
# from the target's .env, else 8787)
|
||
set -euo pipefail
|
||
|
||
REGISTRY="${REGISTRY:-git.familie-zink.org/zink/codecraft}"
|
||
CONTAINER="${CONTAINER:-codecraft}"
|
||
|
||
tag="" target="" force=0 wait_min=15 dry=0
|
||
while (($#)); do
|
||
case "$1" in
|
||
--target) target="$2"; shift ;;
|
||
--force) force=1 ;;
|
||
--wait) wait_min="$2"; shift ;;
|
||
--dry-run) dry=1 ;;
|
||
-h|--help) sed -n '2,29p' "$0"; exit 0 ;;
|
||
-*) echo "unknown option $1" >&2; exit 2 ;;
|
||
*) tag="$1" ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
cd "$(git rev-parse --show-toplevel)"
|
||
step() { printf '\n==> %s\n' "$*"; }
|
||
fail() { echo "error: $*" >&2; exit 1; }
|
||
q() { printf '%q' "$1"; }
|
||
env_get() { [[ -f .env ]] && sed -n "s/^$1=//p" .env | tail -1 | sed -E "s/^'(.*)'\$/\1/; s/^\"(.*)\"\$/\1/"; }
|
||
|
||
target="${target:-$(env_get DEPLOY_TARGET || true)}"
|
||
[[ "$target" == *:/* ]] || fail "no target – pass --target user@host:/dir or set DEPLOY_TARGET in .env"
|
||
host="${target%%:*}"
|
||
dir="${target#*:}"; dir="${dir%/}"
|
||
remote() { ssh -o BatchMode=yes "$host" "$1"; }
|
||
run_remote() { if (( dry )); then echo "[dry-run] on $host: $1"; else remote "$1"; fi; }
|
||
|
||
if [[ -z "$tag" ]]; then
|
||
git fetch --tags --quiet origin || true
|
||
tag=$(git tag -l 'v[0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9]' | sort | tail -1)
|
||
[[ -n "$tag" ]] || fail "no release tag found"
|
||
fi
|
||
[[ "$tag" =~ ^v[0-9]{6}-[0-9]{5}$ ]] || fail "version must look like vYYYYMM-NNNNN, got $tag"
|
||
|
||
step "Checking $host:$dir and the image $REGISTRY:$tag"
|
||
remote "test -f $(q "$dir/.env") && test -f $(q "$dir/docker-compose.yml")" \
|
||
|| fail "$dir on $host has no .env/docker-compose.yml – set it up with scripts/move-server.sh"
|
||
current=$(remote "sed -n 's/^CODECRAFT_VERSION=//p' $(q "$dir/.env") | tail -1")
|
||
case "$(remote 'uname -m')" in
|
||
x86_64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) arch="" ;;
|
||
esac
|
||
if [[ -n "$arch" ]]; then
|
||
docker buildx imagetools inspect "$REGISTRY:$tag" 2>/dev/null | grep -q "Platform:.*linux/$arch" \
|
||
|| fail "$REGISTRY:$tag has no linux/$arch image – run scripts/release.sh first"
|
||
fi
|
||
echo "deploying $tag (running: ${current:-unknown}, target linux/${arch:-?})"
|
||
[[ "$current" == "$tag" ]] && echo "note: $tag is already configured – the container is recreated anyway"
|
||
|
||
# Busy = the container runs more than the server process (agents' shells, terminals, ...).
|
||
busy() { (( $(remote "docker top $(q "$CONTAINER") -o pid 2>/dev/null | tail -n +2 | wc -l") > 1 )); }
|
||
if (( ! force )) && remote "docker inspect -f '{{.State.Running}}' $(q "$CONTAINER") 2>/dev/null" | grep -q true; then
|
||
step "Waiting until no agent or terminal runs (up to $wait_min min, --force skips this)"
|
||
deadline=$(( $(date +%s) + wait_min * 60 ))
|
||
while busy; do
|
||
(( $(date +%s) < deadline )) || fail "still busy after $wait_min min – try later or use --force"
|
||
sleep 15
|
||
done
|
||
echo "idle"
|
||
fi
|
||
|
||
step "Updating to $tag"
|
||
run_remote "cd $(q "$dir") && sed -i 's/^CODECRAFT_VERSION=.*/CODECRAFT_VERSION=$tag/' .env \
|
||
&& (grep -q '^CODECRAFT_VERSION=' .env || echo 'CODECRAFT_VERSION=$tag' >> .env) \
|
||
&& docker pull -q $(q "$REGISTRY:$tag") >/dev/null \
|
||
&& if docker compose version >/dev/null 2>&1; then docker compose up -d; else ./start.sh; fi"
|
||
|
||
step "Smoke test"
|
||
port="${PORT:-$(remote "sed -n 's/^PORT=//p' $(q "$dir/.env") | tail -1")}"; port="${port:-8787}"
|
||
if (( dry )); then
|
||
echo "[dry-run] smoke test on $host: http://127.0.0.1:$port ${tag#v}"
|
||
else
|
||
ssh -o BatchMode=yes "$host" "cat > $(q "$dir/smoke-test.sh")" <scripts/smoke-test.sh
|
||
if ! remote "bash $(q "$dir/smoke-test.sh") http://127.0.0.1:$port ${tag#v}"; then
|
||
if [[ -n "$current" && "$current" != "$tag" ]]; then
|
||
echo "smoke test failed – going back to $current" >&2
|
||
remote "cd $(q "$dir") && sed -i 's/^CODECRAFT_VERSION=.*/CODECRAFT_VERSION=$current/' .env \
|
||
&& if docker compose version >/dev/null 2>&1; then docker compose up -d; else ./start.sh; fi"
|
||
fi
|
||
fail "deploy of $tag failed"
|
||
fi
|
||
fi
|
||
|
||
step "Done"
|
||
echo "$host runs CodeCraft $tag."
|