aboutsummaryrefslogtreecommitdiff
path: root/scripts/deploy.sh
blob: 25c86dc9b69e385de3764bf1d0130dba130259f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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 --build-arg "VERSION=$TAG" -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 --build-arg 'VERSION=$TAG' -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