diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/deploy.sh | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..2abd1e1 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Deploy a locally built bokf image to BOKF_HOST over SSH. See docs/DEPLOY.md. +# +# scripts/deploy.sh [tag] +# +# BOKF_HOST (user@host) and BOKF_REMOTE_DIR are read from the environment or +# from .env in the repository root. The host needs only Docker and Compose. +set -euo pipefail + +cd "$(dirname "$0")/.." + +env_val() { + sed -n "s/^$1=//p" .env 2>/dev/null | tail -n 1 +} + +if [ -f .env ]; then + BOKF_HOST="${BOKF_HOST:-$(env_val BOKF_HOST)}" + BOKF_REMOTE_DIR="${BOKF_REMOTE_DIR:-$(env_val BOKF_REMOTE_DIR)}" + BOKF_BUILD="${BOKF_BUILD:-$(env_val BOKF_BUILD)}" +fi + +: "${BOKF_HOST:?set BOKF_HOST (user@host) in .env or the environment}" +BOKF_REMOTE_DIR="${BOKF_REMOTE_DIR:-/srv/bokf}" +SSH=(ssh "$BOKF_HOST") + +TAG="${1:-}" +if [ -z "$TAG" ]; then + TAG="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" +fi +case "$TAG" in + *[!A-Za-z0-9._-]* | "") + echo "deploy: invalid tag '$TAG'" >&2 + exit 1 + ;; +esac + +remote() { "${SSH[@]}" "$1"; } + +echo "deploy: gate: build + tests" +make -j"$(nproc)" +make test + +BOKF_BUILD="${BOKF_BUILD:-auto}" +case "$BOKF_BUILD" in + auto) + host_arch="$(remote 'uname -m' 2>/dev/null || echo unknown)" + if [ "$(uname -m)" = "$host_arch" ]; then + BOKF_BUILD=local + else + echo "deploy: host is $host_arch, building there" + BOKF_BUILD=remote + fi + ;; + local | remote) ;; + *) + echo "deploy: BOKF_BUILD must be auto, local or remote" >&2 + exit 1 + ;; +esac + +if [ "$BOKF_BUILD" = local ]; then + echo "deploy: building image bokf:$TAG locally" + docker build -t "bokf:$TAG" . + echo "deploy: shipping image to $BOKF_HOST" + docker save "bokf:$TAG" | gzip | "${SSH[@]}" 'gunzip | docker load' +else + echo "deploy: building image bokf:$TAG on $BOKF_HOST" + tar -cf - \ + --exclude=./.git --exclude=./build --exclude=./var --exclude=./.env \ + --exclude='./*.se' --exclude='./*.db' --exclude='./*.db-wal' \ + --exclude='./*.db-shm' --exclude=./docs . | + "${SSH[@]}" "docker build -t 'bokf:$TAG' -" +fi + +remote "mkdir -p '$BOKF_REMOTE_DIR'" +scp -q compose.yaml "$BOKF_HOST:$BOKF_REMOTE_DIR/compose.yaml" + +prev_tag="$(remote "grep -E '^BOKF_TAG=' '$BOKF_REMOTE_DIR/.env' 2>/dev/null | cut -d= -f2" || true)" + +remote "touch '$BOKF_REMOTE_DIR/.env' + sed -i '/^BOKF_IMAGE=/d;/^BOKF_TAG=/d' '$BOKF_REMOTE_DIR/.env' + printf 'BOKF_IMAGE=bokf\nBOKF_TAG=%s\n' '$TAG' >> '$BOKF_REMOTE_DIR/.env'" + +if ! remote "test -f '$BOKF_REMOTE_DIR/var/db/bokfd.db'"; then + echo "deploy: fresh host: no database yet." + echo "deploy: run once on $BOKF_HOST, then deploy again:" + echo " cd $BOKF_REMOTE_DIR" + echo " docker compose run --rm -e BOKFD_PASSWORD='<admin-password>' bokfd init" + echo " docker compose up -d" + exit 0 +fi + +echo "deploy: starting containers" +remote "cd '$BOKF_REMOTE_DIR' && docker compose up -d --no-build" + +wait_healthy() { + for _ in $(seq 1 30); do + cid="$(remote "cd '$BOKF_REMOTE_DIR' && docker compose ps -q bokfd" 2>/dev/null || true)" + if [ -n "$cid" ]; then + st="$(remote "docker inspect --format '{{.State.Health.Status}}' '$cid'" 2>/dev/null || true)" + if [ "$st" = "healthy" ]; then + return 0 + fi + fi + sleep 2 + done + return 1 +} + +if wait_healthy; then + echo "deploy: bokf:$TAG is healthy on $BOKF_HOST" + exit 0 +fi + +echo "deploy: health check failed for bokf:$TAG" >&2 +remote "cd '$BOKF_REMOTE_DIR' && docker compose logs --tail=50 bokfd" >&2 || true + +if [ -n "$prev_tag" ] && [ "$prev_tag" != "$TAG" ]; then + echo "deploy: rolling back to bokf:$prev_tag" >&2 + remote "sed -i 's/^BOKF_TAG=.*/BOKF_TAG=$prev_tag/' '$BOKF_REMOTE_DIR/.env'" + if remote "cd '$BOKF_REMOTE_DIR' && docker compose up -d --no-build" && + wait_healthy; then + echo "deploy: rollback to bokf:$prev_tag is healthy" >&2 + else + echo "deploy: rollback FAILED, manual attention needed" >&2 + fi +fi +exit 1 |
