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.

Commit
6bf7446e1e8ae56f4ad1a4a422bc210922c9cb5f
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
deploy/post-receive
index ab781dc8..b5830724 100755..100755
@@ -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
index 0e7b4e60..3719d06a 100755..100755
@@ -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 "$@"