zuka
zuka/deploy/zuka-deploy

worklyn / zukapublic

Agent-first git hosting. One Rust binary: git over HTTP and SSH, a REST API, MCP, CI, and multi-tenant isolation.

Get a copy: git clone https://zuka.worklyn.com/worklyn/zuka.git
zuka/deploy/zuka-deploy
·zuka-deploy4.2 KBDownload
1#!/bin/bash
2# Install a freshly built binary as the running service, and undo it if the service
3# does not come back healthy.
4#
5# Run by CI through a single narrow sudoers rule. It takes no arguments, because an
6# argument is a thing the caller controls and this runs as root.
7#
8# zuka-build ALL=(root) NOPASSWD: /usr/local/bin/zuka-deploy
9#
10# This script must be root-owned and not writable by the CI user. It is deliberately
11# NOT deployed by CI: a script that CI can rewrite and then invoke under sudo is a
12# root escalation with extra steps. Update it by hand.
13#
14# Understand what this grants: whoever can push to the source repository decides what
15# binary runs as this host's service. That is what continuous deployment is. It is
16# acceptable here because push access to that repository is the same trust as root on
17# this host — if that ever stops being true, this script has to go.
18
19set -euo pipefail
20
21# Hardcoded, not an argument: the sudoers rule above permits this command with no
22# arguments, so an argument could never arrive, and pretending otherwise would leave
23# a parameter that looks configurable and is not.
24readonly SERVICE="zuka"
25readonly JOBS="zuka-jobs"
26readonly STAGED="/var/lib/zuka-build/staging/zuka"
27readonly PRIVATE="/var/lib/zuka-deploy"
28readonly INSTALLED="/usr/local/bin/zuka"
29readonly HEALTH="http://127.0.0.1:8790/healthz"
30readonly HEALTH_TIMEOUT=60
31
32log() { printf '[deploy] %s\n' "$*" >&2; }
33die() { log "FAILED: $*"; exit 1; }
34
35[[ $EUID -eq 0 ]] || die "must run as root"
36
37# The staged path is writable by the CI user, so anything checked about the file
38# there can be made untrue before it is installed. Copy it into a root-only directory
39# first and never look at the original again.
40mkdir -p "$PRIVATE"
41chmod 700 "$PRIVATE"
42[[ -f $STAGED ]] || die "no staged binary at $STAGED"
43
44readonly CANDIDATE="$PRIVATE/candidate"
45cp "$STAGED" "$CANDIDATE"
46chmod 755 "$CANDIDATE"
47
48# A binary that cannot report its own version will not serve traffic either. Cheaper
49# to find out now than after the restart.
50candidate_build="$("$CANDIDATE" version 2>/dev/null)" || die "staged binary does not run"
51log "candidate: $candidate_build"
52
53if [[ -x $INSTALLED ]]; then
54 current_build="$("$INSTALLED" version 2>/dev/null || echo unknown)"
55 log "current: $current_build"
56 if [[ $candidate_build == "$current_build" ]]; then
57 log "already running this build; nothing to do"
58 exit 0
59 fi
60 cp "$INSTALLED" "$PRIVATE/previous"
61else
62 rm -f "$PRIVATE/previous"
63fi
64
65healthy() {
66 local body
67 body="$(curl -fsS --max-time 3 "$HEALTH" 2>/dev/null)" || return 1
68 # Health alone is not enough: systemd could have restarted the *old* binary and
69 # reported perfectly good health. The running build has to be the new one.
70 # `version` prints "<name> <build>"; the build is the last field.
71 [[ $body == *"${candidate_build##* }"* ]]
72}
73
74install_and_wait() {
75 # Same filesystem, so the rename is atomic: no window where the path is a
76 # half-written file, which matters because systemd may restart at any moment.
77 cp "$1" "$INSTALLED.new"
78 chmod 755 "$INSTALLED.new"
79 mv "$INSTALLED.new" "$INSTALLED"
80 # Both units run this binary. Restarting only the web unit would leave
81 # maintenance running last week's code against this week's data directory.
82 systemctl restart "$SERVICE" "$JOBS"
83
84 local waited=0
85 while ((waited < HEALTH_TIMEOUT)); do
86 if healthy; then return 0; fi
87 sleep 2
88 waited=$((waited + 2))
89 done
90 return 1
91}
92
93log "installing and restarting $SERVICE"
94if install_and_wait "$CANDIDATE"; then
95 log "healthy on $candidate_build"
96 exit 0
97fi
98
99log "did not become healthy in ${HEALTH_TIMEOUT}s — rolling back"
100if [[ ! -f $PRIVATE/previous ]]; then
101 die "no previous binary to roll back to; service is down"
102fi
103
104cp "$PRIVATE/previous" "$INSTALLED.new"
105chmod 755 "$INSTALLED.new"
106mv "$INSTALLED.new" "$INSTALLED"
107systemctl restart "$SERVICE" "$JOBS"
108
109waited=0
110while ((waited < HEALTH_TIMEOUT)); do
111 if curl -fsS --max-time 3 "$HEALTH" >/dev/null 2>&1; then
112 log "rolled back; the bad build was not deployed"
113 # Non-zero: the deploy failed, even though the host is fine. CI must go red.
114 exit 1
115 fi
116 sleep 2
117 waited=$((waited + 2))
118done
119
120# Rollback did not restore health. The host needs a human.
121die "rollback did not restore health — $SERVICE is DOWN"