Repository navigation
Expand file tree
/
Copy pathself-update.sh
More file actions
executable file
·181 lines (166 loc) · 8.93 KB
/
Copy pathself-update.sh
File metadata and controls
executable file
·181 lines (166 loc) · 8.93 KB
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
178
179
180
181
#!/usr/bin/env bash
# Scheduled self-update for the deployed MCP server.
#
# Polls GitHub for the latest release tag (via the releases/latest redirect —
# no API token, no rate-limit concern). When the tag differs from what's
# deployed, it:
# 1. pins the repo to that tag's commit (as the service user) and reinstalls
# the package — REQUIRED because the venv is a non-editable install, so a
# bare `git checkout` does NOT change the running code;
# 2. activates the new code, one of two ways:
# * DB release (ships *.db.zip assets): runs deploy/update-db.sh to
# download + verify + validate + swap the DBs and restart the service
# (with its own health-probe + auto-rollback of the DBs);
# * code-only release (NO DB assets — a patch that changes only server
# code, so no ~600 MB DB re-upload is needed): restarts the service to
# load the new code and health-probes it, rolling the CODE back to the
# previously-deployed tag and restarting if the probe fails;
# 3. records the new tag and emails the result.
# When already current it exits silently (no download, no restart, no email).
#
# Runs as ROOT so it can systemctl and write under /srv/ndcourts; git/uv steps
# and the corpus-DB path resolution drop to the service user (the unit's User=,
# e.g. ndcourts) via sudo, since corpus DBs resolve per-$HOME.
#
# Config via env (defaults match deploy/SETUP.md's /srv/ndcourts layout):
# APP_HOME, REPO, RUN_USER, SERVICE, SLUG, MARKER, MAIL_TO, MAIL_FROM, LOCK
# Set MAIL_TO (and configure a sendmail-compatible MTA) to get email; without
# it, results go to journald only (journalctl -u ndlaw-update).
set -euo pipefail
APP_HOME="${APP_HOME:-/srv/ndcourts}"
REPO="${REPO:-$APP_HOME/ndlaw-mcp}"
RUN_USER="${RUN_USER:-ndcourts}"
SERVICE="${SERVICE:-ndlaw-mcp}"
SLUG="${SLUG:-jet52/ndlaw}"
MARKER="${MARKER:-$APP_HOME/.deployed-release}"
MAIL_TO="${MAIL_TO:-}"
MAIL_FROM="${MAIL_FROM:-ndlaw-update@$(hostname -f 2>/dev/null || hostname)}"
LOCK="${LOCK:-/run/ndlaw-update.lock}"
PORT="${PORT:-8000}" # local health-probe port
REPO_BASE="${REPO_BASE:-https://github.com/$SLUG/releases/latest/download}"
log() { echo "[self-update] $*"; }
notify() { # subject, body
local subject="$1" body="$2" sm
log "$subject"
[ -n "$MAIL_TO" ] || { log "no MAIL_TO set — emailing skipped"; return 0; }
sm="$(command -v sendmail || echo /usr/sbin/sendmail)"
if [ -x "$sm" ]; then
printf 'To: %s\nFrom: %s\nSubject: %s\n\n%s\n' \
"$MAIL_TO" "$MAIL_FROM" "$subject" "$body" | "$sm" -t \
|| log "WARN: sendmail failed — see journald for the result"
else
log "WARN: no sendmail found — cannot email '$subject'"
fi
}
# Reinstall the package at a git ref as the service user (non-editable venv ⇒
# checkout alone does not change the running code). Used for both the forward
# install and the code-only rollback. Returns nonzero on any step's failure.
reinstall_code() {
sudo -u "$RUN_USER" git -C "$REPO" -c advice.detachedHead=false checkout --quiet "$1" \
&& sudo -u "$RUN_USER" bash -lc "cd '$REPO' && PATH=\"\$HOME/.local/bin:\$PATH\" uv pip install . >/dev/null 2>&1"
}
# True if the latest release ships DB assets (so update-db.sh has something to
# fetch). A one-byte ranged GET of opinions.db.zip: 200/206 ⇒ present, 404 ⇒
# code-only release. GitHub honors Range on release assets, so this does not
# pull the ~600 MB file. update-db.sh remains the authority on DB validation;
# this only routes between the DB path and the code-only path.
release_has_db_assets() {
curl -fsSL -r 0-0 -o /dev/null "$REPO_BASE/opinions.db.zip" 2>/dev/null
}
# Health-probe the local service (mirrors update-db.sh's probe). Returns 0 once
# an MCP initialize handshake succeeds within ~15s, else 1.
health_probe() {
local i
for i in $(seq 1 15); do
if curl -fsS -X POST "http://127.0.0.1:$PORT/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"self-update","version":"0"}}}' \
>/dev/null 2>&1; then return 0; fi
sleep 1
done
return 1
}
main() {
# Serialize: a slow run must not overlap the next timer tick.
exec 9>"$LOCK"
flock -n 9 || { log "another run holds the lock — exiting"; exit 0; }
command -v git >/dev/null || { log "ABORT: git not found"; exit 1; }
command -v curl >/dev/null || { log "ABORT: curl not found"; exit 1; }
# Full hostname (e.g. mcp.ndconst.org) for notification messages; falls
# back to the short name if the box has no FQDN configured.
local host
host="$(hostname -f 2>/dev/null || hostname)"
# Latest release tag from the releases/latest redirect target.
local latest_url tag deployed
latest_url="$(curl -fsS -o /dev/null -w '%{url_effective}' -L \
"https://github.com/$SLUG/releases/latest")" \
|| { log "ABORT: cannot reach GitHub"; exit 1; }
tag="${latest_url##*/}"
case "$tag" in
v[0-9]*) ;;
*) log "ABORT: unexpected tag '$tag' from '$latest_url'"; exit 1 ;;
esac
deployed="$(cat "$MARKER" 2>/dev/null || echo none)"
if [ "$tag" = "$deployed" ]; then
log "already at $tag — nothing to do"
exit 0
fi
log "new release detected: $deployed -> $tag"
# 1) Pin code to the tag and reinstall (non-editable venv ⇒ reinstall is mandatory).
sudo -u "$RUN_USER" git -C "$REPO" fetch --tags --prune --quiet origin \
|| { notify "ndlaw deploy FAILED ($tag): git fetch" \
"git fetch failed on $host deploying $tag. No change made."; exit 1; }
sudo -u "$RUN_USER" git -C "$REPO" -c advice.detachedHead=false checkout --quiet "$tag" \
|| { notify "ndlaw deploy FAILED ($tag): git checkout" \
"git checkout $tag failed on $host. No change made."; exit 1; }
sudo -u "$RUN_USER" bash -lc "cd '$REPO' && PATH=\"\$HOME/.local/bin:\$PATH\" uv pip install . >/dev/null 2>&1" \
|| { notify "ndlaw deploy FAILED ($tag): pip install" \
"uv pip install . failed on $host deploying $tag. Code is checked out at $tag but not installed; service still on prior code. Investigate."; exit 1; }
# 1b) Refresh the static landing page so the per-release stamped counts
# (scripts/stamp_counts.py, push-db stage 0) reach the live site. Non-fatal.
if [ -d /var/www/ndlaw.org ] && [ -d "$REPO/deploy/ndlaw-landing" ]; then
cp "$REPO"/deploy/ndlaw-landing/* /var/www/ndlaw.org/ \
|| log "WARN: landing-page refresh failed (site serves the previous copy)"
fi
# 2) Activate the new code. Two paths, chosen by whether the release ships DBs.
if release_has_db_assets; then
# DB release: update-db.sh verifies sha256, validates, swaps (keeping .bak),
# restarts the service (loading the new code), health-probes, and
# auto-rolls-back the DB on probe failure.
if "$REPO/deploy/update-db.sh"; then
echo "$tag" > "$MARKER"
local n
n="$(sudo -u "$RUN_USER" sqlite3 "$APP_HOME/opinions.db" 'SELECT COUNT(*) FROM opinions;' 2>/dev/null || echo '?')"
notify "ndlaw deployed $tag" \
"Server $host is now on $tag (opinions=$n). Code pinned to the tag and reinstalled; DB swapped and health-probed OK. Prior DB kept at $APP_HOME/opinions.db.bak."
else
notify "ndlaw deploy FAILED ($tag): update-db rolled back" \
"update-db.sh failed/rolled back on $host deploying $tag; DB restored from .bak. NOTE: code is already at $tag, so code and DB may be mismatched — investigate before the next run. Logs: journalctl -u $SERVICE -n 50."
exit 1
fi
else
# Code-only release (no DB assets): the DBs are unchanged, so just restart
# the service to load the reinstalled code and health-probe it. On failure,
# roll the CODE back to the previously-deployed tag (the DBs were untouched).
log "code-only release (no DB assets) — restart service + health-probe"
systemctl restart "$SERVICE"
if health_probe; then
echo "$tag" > "$MARKER"
notify "ndlaw deployed $tag (code-only)" \
"Server $host is now on $tag. Code reinstalled and the service restarted + health-probed OK; databases unchanged (no DB assets in this release)."
else
log "health probe FAILED after code-only deploy — rolling code back to $deployed"
if [ "$deployed" != "none" ] && reinstall_code "$deployed"; then
systemctl restart "$SERVICE"
notify "ndlaw deploy FAILED ($tag): code rolled back to $deployed" \
"Health probe failed after the code-only deploy of $tag on $host; rolled the code back to $deployed and restarted. Marker left at $deployed. Logs: journalctl -u $SERVICE -n 50."
else
notify "ndlaw deploy FAILED ($tag): ROLLBACK FAILED — service may be down" \
"Health probe failed deploying $tag and the code rollback to '$deployed' also failed on $host. The service may be DOWN — investigate now. Logs: journalctl -u $SERVICE -n 50."
fi
exit 1
fi
fi
}
main "$@"