[OCaml] High Intensity Training Online
style Adopt Google shell style and jq for JSON
Rewrite the hook and setup script to the Google Shell Style Guide: readonly constants, braced expansions, [[ ]] tests, local function vars, and a main entry point. Generate deploy-info.json with jq instead of hand-rolled sed escaping.
Changed files
deploy/post-receive
@@ -1,133 +1,170 @@
1
1
#!/usr/bin/env bash
2
Removed:
# hito post-receive hook. Runs on the VPS after every push.
3
2
#
3
Added:
# hito post-receive hook. Runs on the VPS after every push to the bare repo.
4
Added:
#
4
5
# For a push to master: check out the pushed revision into a fresh timestamped
5
6
# release, build and test it against the shared opam switch, flip
6
7
# /srv/hito/current, restart the service, and confirm it answers HTTP. Any
7
Removed:
# build/test failure leaves the live release untouched and fails the push. A
8
Removed:
# failed health check rolls current back to the previous release and restarts.
8
Added:
# build or test failure leaves the live release untouched and fails the push.
9
Added:
# A failed health check rolls current back to the previous release and restarts.
10
Added:
#
11
Added:
# Style: Google Shell Style Guide.
12
Added:
9
13
set -euo pipefail
10
14
11
Removed:
DEPLOY_BRANCH=master
12
Removed:
SRV=/srv/hito
13
Removed:
BARE=/srv/git/hito.git
14
Removed:
SHARED="$SRV/shared"
15
Removed:
RELEASES="$SRV/releases"
16
Removed:
KEEP=5
17
Removed:
HEALTH_URL="http://127.0.0.1:8080/login"
18
Removed:
HEALTH_TRIES=10
19
Removed:
HEALTH_DELAY=1
20
Removed:
export OPAMROOT=/home/mpeter/.opam
21
Removed:
export OPAMSWITCH="$SHARED"
15
Added:
readonly DEPLOY_BRANCH="master"
16
Added:
readonly SRV="/srv/hito"
17
Added:
readonly BARE="/srv/git/hito.git"
18
Added:
readonly SHARED="${SRV}/shared"
19
Added:
readonly RELEASES="${SRV}/releases"
20
Added:
readonly KEEP=5
21
Added:
readonly HEALTH_URL="http://127.0.0.1:8080/login"
22
Added:
readonly HEALTH_TRIES=10
23
Added:
readonly HEALTH_DELAY=1
22
24
23
Removed:
log() { printf ' hito-deploy: %s\n' "$*" >&2; }
24
Removed:
fail() { log "FAILED: $*"; exit 1; }
25
Added:
export OPAMROOT="/home/mpeter/.opam"
26
Added:
export OPAMSWITCH="${SHARED}"
25
27
26
Removed:
# Poll the app until it answers, or report failure after HEALTH_TRIES.
28
Added:
err() {
29
Added:
printf ' hito-deploy: %s\n' "$*" >&2
30
Added:
}
31
Added:
32
Added:
fail() {
33
Added:
err "FAILED: $*"
34
Added:
exit 1
35
Added:
}
36
Added:
37
Added:
# Poll the app until it answers, or fail after HEALTH_TRIES attempts.
27
38
health_ok() {
28
39
local i
29
Removed:
for i in $(seq 1 "$HEALTH_TRIES"); do
30
Removed:
if curl -sf -o /dev/null --max-time 3 "$HEALTH_URL"; then
40
Added:
for ((i = 0; i < HEALTH_TRIES; i++)); do
41
Added:
if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then
31
42
return 0
32
43
fi
33
Removed:
sleep "$HEALTH_DELAY"
44
Added:
sleep "${HEALTH_DELAY}"
34
45
done
35
46
return 1
36
47
}
37
48
38
Removed:
while read -r _old new ref; do
39
Removed:
if [ "$ref" != "refs/heads/$DEPLOY_BRANCH" ]; then
40
Removed:
log "skipping $ref (only $DEPLOY_BRANCH deploys)"
41
Removed:
continue
42
Removed:
fi
49
Added:
# Materialize the pushed revision from the bare repo into ${rel}. git archive
50
Added:
# yields exactly the tracked tree, never a stray shared/ or releases/ directory;
51
Added:
# those are strict siblings of a release and must never appear inside one.
52
Added:
checkout_release() {
53
Added:
local sha="$1" rel="$2"
54
Added:
git --git-dir="${BARE}" archive "${sha}" | tar -x -C "${rel}"
55
Added:
}
43
56
44
Removed:
start=$SECONDS
45
Removed:
ts=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
46
Removed:
rel="$RELEASES/$ts"
47
Removed:
buildlog="$rel/deploy.log"
48
Removed:
prev=$(readlink -f "$SRV/current" 2>/dev/null || true)
49
Removed:
log "deploying ${new:0:12} to $rel"
50
Removed:
mkdir -p "$rel"
57
Added:
# Build and test against the shared switch. The full log stays inside the
58
Added:
# release so a successful deploy leaves a record, not only a failed one.
59
Added:
build_release() {
60
Added:
local rel="$1"
61
Added:
local buildlog="${rel}/deploy.log"
62
Added:
eval "$(opam env --switch="${SHARED}" --set-switch)"
63
Added:
cd "${rel}"
51
64
52
Removed:
# Materialize the pushed revision from the bare repo. Use git archive so the
53
Removed:
# release contains exactly the tracked tree and never a stray shared/ or
54
Removed:
# releases/ directory. shared/ and releases/ are strict siblings of a release
55
Removed:
# and must never appear inside one.
56
Removed:
if ! git --git-dir="$BARE" archive "$new" | tar -x -C "$rel"; then
57
Removed:
rm -rf "$rel"; fail "checkout"
58
Removed:
fi
65
Added:
err "syncing dependencies"
66
Added:
opam install . --deps-only --with-test -y >"${buildlog}" 2>&1 \
67
Added:
|| { tail -30 "${buildlog}" >&2; return 1; }
59
68
60
Removed:
# Build and test against the shared switch. The full build log is kept inside
61
Removed:
# the release ($buildlog) so a successful deploy leaves a record, not only a
62
Removed:
# failed one. Install missing deps only when hito.opam changed.
63
Removed:
eval "$(opam env --switch="$SHARED" --set-switch)"
64
Removed:
cd "$rel"
69
Added:
err "dune build @check"
70
Added:
dune build @check >>"${buildlog}" 2>&1 \
71
Added:
|| { tail -40 "${buildlog}" >&2; return 1; }
65
72
66
Removed:
log "syncing dependencies"
67
Removed:
opam install . --deps-only --with-test -y >"$buildlog" 2>&1 \
68
Removed:
|| { tail -30 "$buildlog" >&2; rm -rf "$rel"; fail "opam deps"; }
73
Added:
err "dune runtest"
74
Added:
dune runtest >>"${buildlog}" 2>&1 \
75
Added:
|| { tail -40 "${buildlog}" >&2; return 1; }
69
76
70
Removed:
log "dune build @check"
71
Removed:
dune build @check >>"$buildlog" 2>&1 \
72
Removed:
|| { tail -40 "$buildlog" >&2; rm -rf "$rel"; fail "@check"; }
77
Added:
err "dune build (release)"
78
Added:
dune build >>"${buildlog}" 2>&1 \
79
Added:
|| { tail -40 "${buildlog}" >&2; return 1; }
80
Added:
}
73
81
74
Removed:
log "dune runtest"
75
Removed:
dune runtest >>"$buildlog" 2>&1 \
76
Removed:
|| { tail -40 "$buildlog" >&2; rm -rf "$rel"; fail "tests"; }
82
Added:
# Write per-release provenance and append one durable history line.
83
Added:
record_metadata() {
84
Added:
local sha="$1" rel="$2" ts="$3" build_secs="$4"
85
Added:
local subject
86
Added:
subject="$(git --git-dir="${BARE}" log -1 --format=%s "${sha}")"
77
87
78
Removed:
log "dune build (release)"
79
Removed:
dune build >>"$buildlog" 2>&1 \
80
Removed:
|| { tail -40 "$buildlog" >&2; rm -rf "$rel"; fail "build"; }
88
Added:
jq -n \
89
Added:
--arg commit "${sha}" \
90
Added:
--arg subject "${subject}" \
91
Added:
--arg branch "${DEPLOY_BRANCH}" \
92
Added:
--arg deployed_at "${ts}" \
93
Added:
--argjson build_seconds "${build_secs}" \
94
Added:
--arg release "${rel}" \
95
Added:
'{commit: $commit, subject: $subject, branch: $branch,
96
Added:
deployed_at: $deployed_at, build_seconds: $build_seconds,
97
Added:
release: $release}' \
98
Added:
>"${rel}/deploy-info.json"
81
99
82
Removed:
build_secs=$((SECONDS - start))
100
Added:
printf '%s\t%s\t%ss\t%s\t%s\n' \
101
Added:
"${ts}" "${sha:0:12}" "${build_secs}" "${DEPLOY_BRANCH}" "${subject}" \
102
Added:
>>"${SHARED}/deploy-history.tsv"
103
Added:
}
83
104
84
Removed:
# Wire the shared database into this release.
85
Removed:
ln -sfn "$SHARED/hito.sqlite" "$rel/hito.sqlite"
86
Removed:
87
Removed:
# Record deploy metadata: in the release for provenance, and appended to a
88
Removed:
# durable log under shared/ so the deploy history outlives pruned releases.
89
Removed:
subject=$(git --git-dir="$BARE" log -1 --format=%s "$new")
90
Removed:
cat >"$rel/deploy-info.json" <<JSON
91
Removed:
{
92
Removed:
"commit": "$new",
93
Removed:
"subject": $(printf '%s' "$subject" | sed 's/\\/\\\\/g; s/"/\\"/g; s/.*/"&"/'),
94
Removed:
"branch": "$DEPLOY_BRANCH",
95
Removed:
"deployed_at": "$ts",
96
Removed:
"build_seconds": $build_secs,
97
Removed:
"release": "$rel"
105
Added:
# Point current at ${rel} in a single rename so no request sees a half state.
106
Added:
flip_current() {
107
Added:
local rel="$1"
108
Added:
ln -sfn "${rel}" "${SRV}/current.tmp"
109
Added:
mv -T "${SRV}/current.tmp" "${SRV}/current"
98
110
}
99
Removed:
JSON
100
Removed:
printf '%s\t%s\t%ss\t%s\t%s\n' \
101
Removed:
"$ts" "${new:0:12}" "$build_secs" "$DEPLOY_BRANCH" "$subject" \
102
Removed:
>>"$SHARED/deploy-history.tsv"
103
111
104
Removed:
# Atomic flip: swap the symlink in one rename so no request sees a half state.
105
Removed:
ln -sfn "$rel" "$SRV/current.tmp"
106
Removed:
mv -T "$SRV/current.tmp" "$SRV/current"
107
Removed:
log "current -> $rel"
108
Removed:
109
Removed:
# Restart via the single scoped sudoers rule.
112
Added:
# Restart, health-check, and roll back to ${prev} if the new release is unwell.
113
Added:
restart_and_verify() {
114
Added:
local prev="$1"
110
115
sudo -n systemctl restart hito || fail "restart"
111
Removed:
112
Removed:
# Health check. systemd reports active as soon as the process starts, so poll
113
Removed:
# the app itself. On failure, roll current back to the previous release and
114
Removed:
# restart, so a broken build never stays live.
115
116
if health_ok; then
116
Removed:
log "healthy: $HEALTH_URL"
117
Removed:
elif [ -n "$prev" ] && [ -d "$prev" ]; then
118
Removed:
log "health check failed; rolling back to $prev"
119
Removed:
ln -sfn "$prev" "$SRV/current.tmp"
120
Removed:
mv -T "$SRV/current.tmp" "$SRV/current"
117
Added:
err "healthy: ${HEALTH_URL}"
118
Added:
return 0
119
Added:
fi
120
Added:
if [[ -n "${prev}" && -d "${prev}" ]]; then
121
Added:
err "health check failed; rolling back to ${prev}"
122
Added:
flip_current "${prev}"
121
123
sudo -n systemctl restart hito || true
122
124
fail "health check (rolled back to previous release)"
123
Removed:
else
124
Removed:
fail "health check (no previous release to roll back to)"
125
125
fi
126
Added:
fail "health check (no previous release to roll back to)"
127
Added:
}
126
128
127
Removed:
log "deployed ${new:0:12} in ${build_secs}s"
128
Removed:
129
Removed:
# Prune all but the most recent $KEEP releases.
130
Removed:
ls -1dt "$RELEASES"/*/ 2>/dev/null | tail -n +$((KEEP + 1)) \
129
Added:
# Remove all but the most recent KEEP releases.
130
Added:
prune_releases() {
131
Added:
ls -1dt "${RELEASES}"/*/ 2>/dev/null | tail -n +$((KEEP + 1)) \
131
132
| xargs -r rm -rf
132
Removed:
log "done"
133
Removed:
done
133
Added:
}
134
Added:
135
Added:
deploy_ref() {
136
Added:
local sha="$1"
137
Added:
local start="${SECONDS}"
138
Added:
local ts rel prev build_secs
139
Added:
ts="$(date -u +"%Y-%m-%dT%H-%M-%SZ")"
140
Added:
rel="${RELEASES}/${ts}"
141
Added:
prev="$(readlink -f "${SRV}/current" 2>/dev/null || true)"
142
Added:
143
Added:
err "deploying ${sha:0:12} to ${rel}"
144
Added:
mkdir -p "${rel}"
145
Added:
checkout_release "${sha}" "${rel}" || { rm -rf "${rel}"; fail "checkout"; }
146
Added:
build_release "${rel}" || { rm -rf "${rel}"; fail "build"; }
147
Added:
build_secs=$((SECONDS - start))
148
Added:
149
Added:
ln -sfn "${SHARED}/hito.sqlite" "${rel}/hito.sqlite"
150
Added:
record_metadata "${sha}" "${rel}" "${ts}" "${build_secs}"
151
Added:
flip_current "${rel}"
152
Added:
err "current -> ${rel}"
153
Added:
restart_and_verify "${prev}"
154
Added:
err "deployed ${sha:0:12} in ${build_secs}s"
155
Added:
prune_releases
156
Added:
err "done"
157
Added:
}
158
Added:
159
Added:
main() {
160
Added:
local _old new ref
161
Added:
while read -r _old new ref; do
162
Added:
if [[ "${ref}" != "refs/heads/${DEPLOY_BRANCH}" ]]; then
163
Added:
err "skipping ${ref} (only ${DEPLOY_BRANCH} deploys)"
164
Added:
continue
165
Added:
fi
166
Added:
deploy_ref "${new}"
167
Added:
done
168
Added:
}
169
Added:
170
Added:
main "$@"
deploy/setup.sh
@@ -1,100 +1,118 @@
1
1
#!/usr/bin/env bash
2
Removed:
# hito deployment setup — one root script, three subcommands.
3
2
#
4
Removed:
# sudo bash setup.sh install One-time host setup: shared switch, hook,
5
Removed:
# scoped sudoers, and systemd unit pointed at
6
Removed:
# current/. Idempotent. Does NOT create a
7
Removed:
# release; the first push (or `deploy`) does.
3
Added:
# hito deployment setup. One root script, three subcommands.
8
4
#
9
Removed:
# sudo bash setup.sh deploy Build master as a fresh release on the shared
10
Removed:
# switch and flip current. Health-checks and
11
Removed:
# rolls back on failure. Use to go live without
12
Removed:
# a push, or after `install`.
5
Added:
# sudo bash setup.sh install One-time host setup: shared switch, hook,
6
Added:
# scoped sudoers, and the systemd unit pointed at
7
Added:
# current/. Idempotent. Creates no release; the
8
Added:
# first push (or 'deploy') does.
9
Added:
# sudo bash setup.sh deploy Build master as a fresh release on the shared
10
Added:
# switch and flip current. Health-checks and
11
Added:
# rolls back on failure. Use to go live without a
12
Added:
# push, or right after 'install'.
13
Added:
# sudo bash setup.sh reset Wipe /srv/hito to the clean layout, preserving
14
Added:
# the database and the shared switch, then
15
Added:
# 'deploy'. Destructive: all else is removed.
13
16
#
14
Removed:
# sudo bash setup.sh reset Wipe /srv/hito to the clean layout, preserving
15
Removed:
# the database and the shared switch, then run
16
Removed:
# `deploy`. Destructive: everything else under
17
Removed:
# /srv/hito is removed.
17
Added:
# The post-receive hook beside this script is the per-push equivalent of
18
Added:
# 'deploy'; 'install' copies it into the bare repo.
18
19
#
19
Removed:
# The hook (post-receive) beside this script is the per-push equivalent of
20
Removed:
# `deploy`; `install` copies it into the bare repo.
20
Added:
# Style: Google Shell Style Guide.
21
Added:
21
22
set -euo pipefail
22
23
23
Removed:
DEPLOY_USER=mpeter
24
Removed:
DEPLOY_GROUP=git
25
Removed:
SRV=/srv/hito
26
Removed:
BARE=/srv/git/hito.git
27
Removed:
UNIT=/etc/systemd/system/hito.service
28
Removed:
SUDOERS=/etc/sudoers.d/hito-deploy
29
Removed:
SHARED="$SRV/shared"
30
Removed:
RELEASES="$SRV/releases"
31
Removed:
DB="$SHARED/hito.sqlite"
32
Removed:
SWITCH="$SHARED/_opam"
33
Removed:
OCAML_COMPILER=ocaml-base-compiler.5.5.0
34
Removed:
HEALTH_URL="http://127.0.0.1:8080/login"
35
Removed:
HERE=$(cd "$(dirname "$0")" && pwd)
24
Added:
readonly DEPLOY_USER="mpeter"
25
Added:
readonly DEPLOY_GROUP="git"
26
Added:
readonly SRV="/srv/hito"
27
Added:
readonly BARE="/srv/git/hito.git"
28
Added:
readonly UNIT="/etc/systemd/system/hito.service"
29
Added:
readonly SUDOERS="/etc/sudoers.d/hito-deploy"
30
Added:
readonly SHARED="${SRV}/shared"
31
Added:
readonly RELEASES="${SRV}/releases"
32
Added:
readonly DB="${SHARED}/hito.sqlite"
33
Added:
readonly SWITCH="${SHARED}/_opam"
34
Added:
readonly OCAML_COMPILER="ocaml-base-compiler.5.5.0"
35
Added:
readonly HEALTH_URL="http://127.0.0.1:8080/login"
36
Added:
readonly OPAMROOT_DIR="/home/${DEPLOY_USER}/.opam"
37
Added:
HERE="$(cd "$(dirname "$0")" && pwd)"
38
Added:
readonly HERE
36
39
37
Removed:
require_root() { [ "$(id -u)" -eq 0 ] || { echo "run as root"; exit 1; }; }
40
Added:
require_root() {
41
Added:
if [[ "$(id -u)" -ne 0 ]]; then
42
Added:
echo "run as root" >&2
43
Added:
exit 1
44
Added:
fi
45
Added:
}
38
46
39
Removed:
# --- shared building blocks -------------------------------------------------
47
Added:
# Run a command as the deploy user with the opam root exported.
48
Added:
as_deploy() {
49
Added:
sudo -u "${DEPLOY_USER}" -H env "OPAMROOT=${OPAMROOT_DIR}" "$@"
50
Added:
}
40
51
41
52
ensure_switch() {
42
Removed:
if [ ! -d "$SWITCH" ]; then
43
Removed:
echo " creating shared switch ($OCAML_COMPILER)"
44
Removed:
sudo -u "$DEPLOY_USER" -H bash -lc "
45
Removed:
export OPAMROOT=/home/$DEPLOY_USER/.opam
46
Removed:
opam switch create '$SHARED' '$OCAML_COMPILER' --no-install -y
47
Removed:
"
48
Removed:
else
53
Added:
if [[ -d "${SWITCH}" ]]; then
49
54
echo " shared switch present"
55
Added:
return 0
50
56
fi
57
Added:
echo " creating shared switch (${OCAML_COMPILER})"
58
Added:
as_deploy opam switch create "${SHARED}" "${OCAML_COMPILER}" --no-install -y
51
59
}
52
60
53
61
install_hook() {
54
Removed:
install -o "$DEPLOY_USER" -g "$DEPLOY_GROUP" -m 0755 \
55
Removed:
"$HERE/post-receive" "$BARE/hooks/post-receive"
56
Removed:
echo " installed $BARE/hooks/post-receive"
62
Added:
install -o "${DEPLOY_USER}" -g "${DEPLOY_GROUP}" -m 0755 \
63
Added:
"${HERE}/post-receive" "${BARE}/hooks/post-receive"
64
Added:
echo " installed ${BARE}/hooks/post-receive"
57
65
}
58
66
59
67
install_sudoers() {
60
Removed:
cat >"$SUDOERS" <<SUDO
61
Removed:
$DEPLOY_USER ALL=(root) NOPASSWD: /usr/bin/systemctl restart hito
68
Added:
cat >"${SUDOERS}" <<SUDO
69
Added:
${DEPLOY_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart hito
62
70
SUDO
63
Removed:
chmod 0440 "$SUDOERS"
64
Removed:
visudo -c -f "$SUDOERS" >/dev/null && echo " sudoers valid"
71
Added:
chmod 0440 "${SUDOERS}"
72
Added:
visudo -c -f "${SUDOERS}" >/dev/null && echo " sudoers valid"
65
73
}
66
74
67
75
point_unit_at_current() {
68
Removed:
cp -a "$UNIT" "$UNIT.bak.$(date +%s)"
76
Added:
cp -a "${UNIT}" "${UNIT}.bak.$(date +%s)"
69
77
sed -i \
70
78
-e 's#^ExecStart=.*#ExecStart=/srv/hito/current/_build/default/bin/main.exe#' \
71
79
-e 's#^WorkingDirectory=.*#WorkingDirectory=/srv/hito/current#' \
72
80
-e 's#^Environment=HITO_DB=.*#Environment=HITO_DB=sqlite3:/srv/hito/shared/hito.sqlite#' \
73
Removed:
"$UNIT"
81
Added:
"${UNIT}"
74
82
systemctl daemon-reload
75
83
echo " unit points at current/ (backup kept alongside)"
76
84
}
77
85
78
Removed:
# Build master in a staging dir OUTSIDE /srv/hito (so dune's project-root search
79
Removed:
# never sees current or a sibling release), publish it, flip current, restart,
80
Removed:
# health-check, and roll back on failure. Records deploy metadata.
86
Added:
health_ok() {
87
Added:
local i
88
Added:
for ((i = 0; i < 10; i++)); do
89
Added:
if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then
90
Added:
return 0
91
Added:
fi
92
Added:
sleep 1
93
Added:
done
94
Added:
return 1
95
Added:
}
96
Added:
97
Added:
# Build master in a staging dir OUTSIDE /srv/hito, so dune's project-root search
98
Added:
# never sees current or a sibling release, then publish it, flip current,
99
Added:
# restart, health-check, and roll back on failure. Records deploy metadata.
81
100
build_and_flip() {
82
Removed:
local start ts rel stage sha subject build_secs prev
83
Removed:
start=$SECONDS
84
Removed:
ts=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
85
Removed:
rel="$RELEASES/$ts"
86
Removed:
prev=$(readlink -f "$SRV/current" 2>/dev/null || true)
87
Removed:
sha=$(git --git-dir="$BARE" rev-parse refs/heads/master)
88
Removed:
stage=$(sudo -u "$DEPLOY_USER" mktemp -d /tmp/hito-stage.XXXXXX)
101
Added:
local start="${SECONDS}"
102
Added:
local ts rel stage sha subject build_secs prev
103
Added:
ts="$(date -u +"%Y-%m-%dT%H-%M-%SZ")"
104
Added:
rel="${RELEASES}/${ts}"
105
Added:
prev="$(readlink -f "${SRV}/current" 2>/dev/null || true)"
106
Added:
sha="$(git --git-dir="${BARE}" rev-parse "refs/heads/master")"
107
Added:
stage="$(as_deploy mktemp -d /tmp/hito-stage.XXXXXX)"
89
108
90
109
echo " building master ${sha:0:12}"
91
Removed:
git --git-dir="$BARE" archive "$sha" | tar -x -C "$stage"
92
Removed:
chown -R "$DEPLOY_USER:$DEPLOY_GROUP" "$stage"
93
Removed:
sudo -u "$DEPLOY_USER" -H bash -lc "
110
Added:
git --git-dir="${BARE}" archive "${sha}" | tar -x -C "${stage}"
111
Added:
chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${stage}"
112
Added:
as_deploy bash -lc "
94
113
set -euo pipefail
95
Removed:
export OPAMROOT=/home/$DEPLOY_USER/.opam
96
Removed:
cd '$stage'
97
Removed:
eval \"\$(opam env --switch='$SHARED' --set-switch)\"
114
Added:
cd '${stage}'
115
Added:
eval \"\$(opam env --switch='${SHARED}' --set-switch)\"
98
116
opam install . --deps-only --with-test -y 2>&1 | tee deploy.log
99
117
dune build @check 2>&1 | tee -a deploy.log
100
118
dune runtest 2>&1 | tee -a deploy.log
@@ -102,107 +120,119 @@
102
120
"
103
121
build_secs=$((SECONDS - start))
104
122
105
Removed:
mkdir -p "$RELEASES"
106
Removed:
mv "$stage" "$rel"
107
Removed:
ln -sfn "$DB" "$rel/hito.sqlite"
123
Added:
mkdir -p "${RELEASES}"
124
Added:
mv "${stage}" "${rel}"
125
Added:
ln -sfn "${DB}" "${rel}/hito.sqlite"
108
126
109
Removed:
subject=$(git --git-dir="$BARE" log -1 --format=%s "$sha")
110
Removed:
cat >"$rel/deploy-info.json" <<JSON
111
Removed:
{
112
Removed:
"commit": "$sha",
113
Removed:
"subject": $(printf '%s' "$subject" | sed 's/\\/\\\\/g; s/"/\\"/g; s/.*/"&"/'),
114
Removed:
"branch": "master",
115
Removed:
"deployed_at": "$ts",
116
Removed:
"build_seconds": $build_secs,
117
Removed:
"release": "$rel"
118
Removed:
}
119
Removed:
JSON
127
Added:
subject="$(git --git-dir="${BARE}" log -1 --format=%s "${sha}")"
128
Added:
jq -n \
129
Added:
--arg commit "${sha}" \
130
Added:
--arg subject "${subject}" \
131
Added:
--arg branch "master" \
132
Added:
--arg deployed_at "${ts}" \
133
Added:
--argjson build_seconds "${build_secs}" \
134
Added:
--arg release "${rel}" \
135
Added:
'{commit: $commit, subject: $subject, branch: $branch,
136
Added:
deployed_at: $deployed_at, build_seconds: $build_seconds,
137
Added:
release: $release}' \
138
Added:
>"${rel}/deploy-info.json"
120
139
printf '%s\t%s\t%ss\t%s\t%s\n' \
121
Removed:
"$ts" "${sha:0:12}" "$build_secs" "master" "$subject" \
122
Removed:
>>"$SHARED/deploy-history.tsv"
140
Added:
"${ts}" "${sha:0:12}" "${build_secs}" "master" "${subject}" \
141
Added:
>>"${SHARED}/deploy-history.tsv"
123
142
124
Removed:
chown -R "$DEPLOY_USER:$DEPLOY_GROUP" "$rel"
125
Removed:
chown "$DEPLOY_USER:$DEPLOY_GROUP" "$SHARED/deploy-history.tsv"
126
Removed:
ln -sfn "$rel" "$SRV/current.tmp"
127
Removed:
mv -T "$SRV/current.tmp" "$SRV/current"
128
Removed:
chown -h "$DEPLOY_USER:$DEPLOY_GROUP" "$SRV/current"
129
Removed:
echo " current -> $rel (built in ${build_secs}s)"
143
Added:
chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${rel}"
144
Added:
chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "${SHARED}/deploy-history.tsv"
145
Added:
ln -sfn "${rel}" "${SRV}/current.tmp"
146
Added:
mv -T "${SRV}/current.tmp" "${SRV}/current"
147
Added:
chown -h "${DEPLOY_USER}:${DEPLOY_GROUP}" "${SRV}/current"
148
Added:
echo " current -> ${rel} (built in ${build_secs}s)"
130
149
131
150
systemctl restart hito
132
151
if health_ok; then
133
Removed:
echo " healthy at $HEALTH_URL"
134
Removed:
elif [ -n "$prev" ] && [ -d "$prev" ]; then
135
Removed:
echo " health check failed; rolling back to $prev"
136
Removed:
ln -sfn "$prev" "$SRV/current.tmp"
137
Removed:
mv -T "$SRV/current.tmp" "$SRV/current"
152
Added:
echo " healthy at ${HEALTH_URL}"
153
Added:
return 0
154
Added:
fi
155
Added:
if [[ -n "${prev}" && -d "${prev}" ]]; then
156
Added:
echo " health check failed; rolling back to ${prev}"
157
Added:
ln -sfn "${prev}" "${SRV}/current.tmp"
158
Added:
mv -T "${SRV}/current.tmp" "${SRV}/current"
138
159
systemctl restart hito || true
139
Removed:
echo " rolled back; recent journal:"; journalctl -u hito -n 15 --no-pager
160
Added:
echo " rolled back; recent journal:" >&2
161
Added:
journalctl -u hito -n 15 --no-pager >&2
140
162
exit 1
141
Removed:
else
142
Removed:
echo " health check failed and no previous release; journal:"
143
Removed:
journalctl -u hito -n 15 --no-pager; exit 1
144
163
fi
164
Added:
echo " health check failed and no previous release; journal:" >&2
165
Added:
journalctl -u hito -n 15 --no-pager >&2
166
Added:
exit 1
145
167
}
146
168
147
Removed:
health_ok() {
148
Removed:
local i
149
Removed:
for i in $(seq 1 10); do
150
Removed:
curl -sf -o /dev/null --max-time 3 "$HEALTH_URL" && return 0
151
Removed:
sleep 1
152
Removed:
done
153
Removed:
return 1
154
Removed:
}
155
Removed:
156
Removed:
# --- subcommands ------------------------------------------------------------
157
Removed:
158
169
cmd_install() {
159
170
echo "== shared and releases directories =="
160
Removed:
mkdir -p "$RELEASES" "$SHARED"
171
Added:
mkdir -p "${RELEASES}" "${SHARED}"
161
172
# Adopt a pre-existing in-place DB exactly once.
162
Removed:
if [ -f "$SRV/hito.sqlite" ] && [ ! -e "$DB" ]; then
163
Removed:
mv "$SRV/hito.sqlite" "$DB"; echo " adopted existing hito.sqlite -> $DB"
173
Added:
if [[ -f "${SRV}/hito.sqlite" && ! -e "${DB}" ]]; then
174
Added:
mv "${SRV}/hito.sqlite" "${DB}"
175
Added:
echo " adopted existing hito.sqlite -> ${DB}"
164
176
fi
165
Removed:
chown -R "$DEPLOY_USER:$DEPLOY_GROUP" "$RELEASES" "$SHARED"
166
Removed:
echo "== shared opam switch =="; ensure_switch
167
Removed:
echo "== post-receive hook =="; install_hook
168
Removed:
echo "== scoped sudoers =="; install_sudoers
169
Removed:
echo "== systemd unit =="; point_unit_at_current
177
Added:
chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${RELEASES}" "${SHARED}"
178
Added:
echo "== shared opam switch =="; ensure_switch
179
Added:
echo "== post-receive hook =="; install_hook
180
Added:
echo "== scoped sudoers =="; install_sudoers
181
Added:
echo "== systemd unit =="; point_unit_at_current
170
182
echo
171
183
echo "Done. Run 'sudo bash setup.sh deploy' to build the first release,"
172
184
echo "or push master to trigger the hook."
173
185
}
174
186
175
187
cmd_deploy() {
176
Removed:
[ -d "$SWITCH" ] || { echo "FATAL: no shared switch; run 'install' first"; exit 1; }
188
Added:
if [[ ! -d "${SWITCH}" ]]; then
189
Added:
echo "FATAL: no shared switch; run 'install' first" >&2
190
Added:
exit 1
191
Added:
fi
177
192
echo "== deploy master =="; build_and_flip
178
193
}
179
194
180
195
cmd_reset() {
181
Removed:
[ -f "$DB" ] || { echo "FATAL: no database at $DB; aborting before wipe"; exit 1; }
196
Added:
if [[ ! -f "${DB}" ]]; then
197
Added:
echo "FATAL: no database at ${DB}; aborting before wipe" >&2
198
Added:
exit 1
199
Added:
fi
200
Added:
local stash
182
201
echo "== stop service =="; systemctl stop hito || true
183
202
echo "== stash DB and switch =="
184
Removed:
local stash; stash=$(mktemp -d /root/hito-reset-stash.XXXXXX)
185
Removed:
cp -a "$DB" "$stash/hito.sqlite" # safety copy, never removed by us
186
Removed:
mv "$DB" "$stash/hito.sqlite.live"
187
Removed:
[ -d "$SWITCH" ] && mv "$SWITCH" "$stash/_opam"
188
Removed:
echo " DB backup at $stash/hito.sqlite"
203
Added:
stash="$(mktemp -d /root/hito-reset-stash.XXXXXX)"
204
Added:
cp -a "${DB}" "${stash}/hito.sqlite" # safety copy, never removed by us
205
Added:
mv "${DB}" "${stash}/hito.sqlite.live"
206
Added:
if [[ -d "${SWITCH}" ]]; then
207
Added:
mv "${SWITCH}" "${stash}/_opam"
208
Added:
fi
209
Added:
echo " DB backup at ${stash}/hito.sqlite"
189
210
echo "== wipe /srv/hito contents =="
190
Removed:
find "$SRV" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
211
Added:
find "${SRV}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
191
212
echo "== rebuild skeleton =="
192
Removed:
mkdir -p "$SHARED" "$RELEASES"
193
Removed:
mv "$stash/hito.sqlite.live" "$DB"
194
Removed:
[ -d "$stash/_opam" ] && mv "$stash/_opam" "$SWITCH"
195
Removed:
chown -R "$DEPLOY_USER:$DEPLOY_GROUP" "$SHARED" "$RELEASES"
213
Added:
mkdir -p "${SHARED}" "${RELEASES}"
214
Added:
mv "${stash}/hito.sqlite.live" "${DB}"
215
Added:
if [[ -d "${stash}/_opam" ]]; then
216
Added:
mv "${stash}/_opam" "${SWITCH}"
217
Added:
fi
218
Added:
chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${SHARED}" "${RELEASES}"
196
219
ensure_switch
197
220
echo "== deploy master =="; build_and_flip
198
221
echo
199
Removed:
echo "Done. Safety DB backup remains at $stash/hito.sqlite (remove when confident)."
222
Added:
echo "Done. Safety DB backup remains at ${stash}/hito.sqlite (remove when confident)."
200
223
}
201
224
202
Removed:
require_root
203
Removed:
case "${1:-}" in
204
Removed:
install) cmd_install ;;
205
Removed:
deploy) cmd_deploy ;;
206
Removed:
reset) cmd_reset ;;
207
Removed:
*) echo "usage: sudo bash setup.sh {install|deploy|reset}"; exit 2 ;;
208
Removed:
esac
225
Added:
main() {
226
Added:
require_root
227
Added:
case "${1:-}" in
228
Added:
install) cmd_install ;;
229
Added:
deploy) cmd_deploy ;;
230
Added:
reset) cmd_reset ;;
231
Added:
*)
232
Added:
echo "usage: sudo bash setup.sh {install|deploy|reset}" >&2
233
Added:
exit 2
234
Added:
;;
235
Added:
esac
236
Added:
}
237
Added:
238
Added:
main "$@"