summaryrefslogtreecommitdiff
path: root/scripts/deploy.sh
blob: ad5350c7bee799dfc0064a6d142d4bc17ba9493e (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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env bash
# Deploy a locally built bokf image to BOKF_HOST over SSH. See docs/DEPLOY.md.
#
#   scripts/deploy.sh [--dev] [tag]
#
# Normal mode builds an image and recreates the container, building natively on
# the host when the target architecture differs. --dev (same architecture only)
# skips the image: it copies the fresh binaries into the running container and
# reloads the daemon with SIGHUP, which re-execs them in place.
#
# 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")

DEV=0
if [ "${1:-}" = "--dev" ]; then
    DEV=1
    shift
fi

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"; }

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
}

echo "deploy: gate: build + tests"
make -j"$(nproc)"
make test

BOKF_BUILD="${BOKF_BUILD:-auto}"
host_arch="$(uname -m)"
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 the image on the host"
            BOKF_BUILD=remote
        fi
        ;;
    local |     remote)
        host_arch="$(remote 'uname -m' 2>/dev/null || echo unknown)"
        ;;
    *)
        echo "deploy: BOKF_BUILD must be auto, local or remote" >&2
        exit 1
        ;;
esac

if [ "$DEV" = 1 ] && [ "$BOKF_BUILD" = local ]; then
    if ! remote "test -f '$BOKF_REMOTE_DIR/var/db/bokfd.db'"; then
        echo "deploy: no database on $BOKF_HOST; run a normal deploy first" >&2
        exit 1
    fi
    echo "deploy: dev: copying the locally built binaries into the running container"
    remote "mkdir -p /tmp/bokf-dev"
    scp -q build/bokfd build/bokfctl "$BOKF_HOST:/tmp/bokf-dev/"
    remote "cd '$BOKF_REMOTE_DIR'
            cid=\$(docker compose ps -q bokfd)
            test -n \"\$cid\" || { echo 'deploy: bokfd is not running' >&2; exit 1; }
            docker cp /tmp/bokf-dev/bokfd \"\$cid:/usr/local/bin/bokfd\"
            docker cp /tmp/bokf-dev/bokfctl \"\$cid:/usr/local/bin/bokfctl\"
            docker kill --signal=HUP \"\$cid\""
    ver=""
    for _ in $(seq 1 15); do
        ver="$(remote "cd '$BOKF_REMOTE_DIR' && docker exec \$(docker compose ps -q bokfd) bokfctl meta 2>/dev/null" |
            sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' || true)"
        [ "$ver" = "$TAG" ] && break
        sleep 1
    done
    if [ "$ver" = "$TAG" ]; then
        echo "deploy: dev reloaded, daemon reports $ver"
        exit 0
    fi
    echo "deploy: dev reload did not come back with $TAG (got '${ver:-nothing}')" >&2
    remote "cd '$BOKF_REMOTE_DIR' && docker compose logs --tail=30 bokfd" >&2 || true
    exit 1
fi

if [ "$DEV" = 1 ]; then
    echo "deploy: dev: host is $host_arch, hot reload needs a matching" \
         "architecture; doing a full deploy" >&2
fi

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: assembling image bokf:$TAG natively 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"

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