summaryrefslogtreecommitdiff
path: root/scripts/deploy.sh
blob: c345a218704c2bf4031b3c34db9156f58e1d7213 (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
#!/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, cross-building here"
            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: cross-compiling $host_arch binaries on this machine"
    rm -rf .prebuilt
    mkdir -p .prebuilt
    docker build -q -f deploy/Dockerfile.cross -t bokf-cross deploy/ >/dev/null
    docker run --rm -e "VERSION=$TAG" -v "$PWD":/src:ro \
        -v "$PWD/.prebuilt":/out bokf-cross
    if command -v file >/dev/null 2>&1 &&
        ! file .prebuilt/bokfd | grep -q aarch64; then
        echo "deploy: cross-build produced no arm64 binary" >&2
        rm -rf .prebuilt
        exit 1
    fi
    echo "deploy: assembling 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' -"
    rm -rf .prebuilt
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