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.7 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# Started by root, not called by CI. CI drops a binary and a request file in a
6# staging directory; a systemd.path unit notices the request and runs this. Nothing
7# the CI user writes becomes an argument, and CI never gains a privilege — it can
8# only ask.
9#
10# The alternative was a sudoers rule, which does not work here anyway: the build
11# instance runs with NoNewPrivileges=yes, and sudo is setuid. Unhardening the one
12# unit that executes arbitrary pushed code to let it call sudo is the wrong trade.
13#
14# Understand what this still grants: whoever can push to the source repository
15# decides which binary runs as this host's service. That is what continuous
16# deployment is, and no amount of care in this script changes it. It is acceptable
17# because push access to that repository is the same trust as root on this host. If
18# that ever stops being true, this arrangement has to go.
19#
20# Install:
21# sudo install -o root -g root -m 0755 zuka-deploy /usr/local/bin/zuka-deploy
22# sudo install -o root -g root -m 0644 zuka-deploy.path zuka-deploy.service \
23# /etc/systemd/system/
24# sudo systemctl enable --now zuka-deploy.path
25
26set -euo pipefail
27
28readonly SERVICE="zuka"
29readonly JOBS="zuka-jobs"
30readonly STAGING="/var/lib/zuka-build/staging"
31readonly PRIVATE="/var/lib/zuka-deploy"
32readonly INSTALLED="/usr/local/bin/zuka"
33readonly HEALTH="http://127.0.0.1:8790/healthz"
34readonly HEALTH_TIMEOUT=90
35
36log() { printf '[deploy] %s\n' "$*" >&2; }
37
38# The result file is how CI learns whether this worked, so it must be written on
39# every exit path — including the ones nobody thought about. Without the trap, a
40# script that dies early leaves CI polling until it times out, reporting "deploy
41# timed out" for what was actually a crash on line three.
42result=""
43finish() {
44 local code=$?
45 [[ -n $result ]] || result="FAIL unknown deploy failure (exit $code)"
46 printf '%s\n' "$result" >"$STAGING/deploy.result.tmp"
47 chmod 644 "$STAGING/deploy.result.tmp"
48 mv "$STAGING/deploy.result.tmp" "$STAGING/deploy.result"
49 log "$result"
50}
51trap finish EXIT
52
53die() {
54 result="FAIL $*"
55 exit 1
56}
57
58[[ $EUID -eq 0 ]] || die "must run as root"
59
60# Everything in the staging directory is writable by the CI user, so anything checked
61# about a file there can be made untrue before it is used. Copy into a root-only
62# directory and never look at the original again.
63mkdir -p "$PRIVATE"
64chmod 700 "$PRIVATE"
65[[ -f $STAGING/zuka ]] || die "no staged binary"
66
67readonly CANDIDATE="$PRIVATE/candidate"
68cp "$STAGING/zuka" "$CANDIDATE"
69chmod 755 "$CANDIDATE"
70
71# A binary that cannot report its own version will not serve traffic either, and
72# finding out here costs nothing while finding out after the restart costs an outage.
73version_output="$("$CANDIDATE" version 2>/dev/null)" || die "staged binary does not run"
74# `version` prints "<name> <build>".
75readonly BUILD="${version_output##* }"
76log "candidate $BUILD"
77
78if [[ -x $INSTALLED ]]; then
79 current="$("$INSTALLED" version 2>/dev/null || echo 'unknown unknown')"
80 if [[ ${current##* } == "$BUILD" ]]; then
81 result="OK $BUILD (already running)"
82 exit 0
83 fi
84 cp "$INSTALLED" "$PRIVATE/previous"
85else
86 rm -f "$PRIVATE/previous"
87fi
88
89# Health alone is not proof: systemd could have restarted the old binary and reported
90# perfectly good health. The build it reports has to be the one just installed.
91healthy() {
92 local body
93 body="$(curl -fsS --max-time 3 "$HEALTH" 2>/dev/null)" || return 1
94 [[ $body == *"$BUILD"* ]]
95}
96
97install_binary() {
98 # Same filesystem, so the rename is atomic. systemd may restart at any moment and
99 # must never find a half-written file at this path.
100 cp "$1" "$INSTALLED.new"
101 chmod 755 "$INSTALLED.new"
102 mv "$INSTALLED.new" "$INSTALLED"
103 # Both units run this binary. Restarting only the web unit would leave
104 # maintenance running the old code against the new data directory.
105 systemctl restart "$SERVICE" "$JOBS"
106}
107
108wait_for() {
109 local waited=0
110 while ((waited < HEALTH_TIMEOUT)); do
111 "$1" && return 0
112 sleep 2
113 waited=$((waited + 2))
114 done
115 return 1
116}
117
118log "installing $BUILD and restarting"
119install_binary "$CANDIDATE"
120if wait_for healthy; then
121 result="OK $BUILD"
122 exit 0
123fi
124
125log "not healthy on $BUILD after ${HEALTH_TIMEOUT}s — rolling back"
126[[ -f $PRIVATE/previous ]] || die "$BUILD is unhealthy and there is no previous binary; $SERVICE is DOWN"
127
128install_binary "$PRIVATE/previous"
129responds() { curl -fsS --max-time 3 "$HEALTH" >/dev/null 2>&1; }
130if wait_for responds; then
131 die "$BUILD did not become healthy; rolled back, the host is serving the previous build"
132fi
133
134die "$BUILD did not become healthy AND the rollback did not restore it; $SERVICE is DOWN"