| 13 |
13 |
|
# root. The build uses a dedicated checkout, never the operator's dev tree.
|
| 14 |
14 |
|
#
|
| 15 |
15 |
|
# Config via environment (defaults shown), set in the unit or /etc/sando/sando.env:
|
| 16 |
|
- |
# SANDO_SELF_UPDATE_DIR /srv/sando/self-update build checkout parent (build-user-owned)
|
| 17 |
|
- |
# SANDO_UPSTREAM_URL git@ssh.makenot.work:max/makenotwork.git
|
|
16 |
+ |
# SANDO_SELF_UPDATE_DIR /srv/sando/self-update build checkout parent (build-user-owned)
|
|
17 |
+ |
# SANDO_UPSTREAM_URL /srv/sando/mnw.git source repo (see "Source" below)
|
| 18 |
18 |
|
# SANDO_BUILD_USER sando
|
| 19 |
|
- |
# SANDO_BIN /usr/local/bin/sandod install destination
|
| 20 |
|
- |
# SANDO_DEPLOY_BRANCH main the only branch a self-update sha may live on
|
|
19 |
+ |
# SANDO_BIN /usr/local/bin/sandod install destination
|
|
20 |
+ |
# SANDO_DEPLOY_BRANCH main the only branch a self-update sha may live on
|
|
21 |
+ |
# SANDO_DAEMON_CONFIG /etc/sando/sando-daemon.toml config the --check-config self-test loads
|
|
22 |
+ |
#
|
|
23 |
+ |
# Source: build from the LOCAL bare repo sandod already maintains
|
|
24 |
+ |
# (/srv/sando/mnw.git), not a remote fetch. The self-update sha is one that was
|
|
25 |
+ |
# already deployed as a server release, so `/rebuild` has force-updated
|
|
26 |
+ |
# refs/heads/main in the bare repo to include it — the sha and a current, trusted
|
|
27 |
+ |
# main are both present locally. Building from a remote (git@ssh.makenot.work)
|
|
28 |
+ |
# gave the `sando` user no git creds and broke the moment git hosting was down —
|
|
29 |
+ |
# it blocked the self-update during the 2026-07-09 deploy (postmortem #7). The
|
|
30 |
+ |
# local bare repo has no such external dependency.
|
| 21 |
31 |
|
#
|
| 22 |
32 |
|
# Provenance: sandod (bearer-gated) only *triggers* this unit with a hex sha; it
|
| 23 |
33 |
|
# does not prove the sha is a commit anyone intended to deploy. Without a check,
|
| 24 |
|
- |
# the deploy token would be root code-exec on this host — push any branch to the
|
| 25 |
|
- |
# remote, self-update to its tip. So after fetch we REFUSE any sha that is not an
|
| 26 |
|
- |
# ancestor of origin/$SANDO_DEPLOY_BRANCH: a feature-branch tip, a force-pushed
|
| 27 |
|
- |
# commit, or an unknown sha never reaches the build/install lines (exit 4). A
|
| 28 |
|
- |
# signed-tag check is the planned follow-up once release signing exists.
|
|
34 |
+ |
# the deploy token would be root code-exec on this host. So after fetch we REFUSE
|
|
35 |
+ |
# any sha that is not an ancestor of origin/$SANDO_DEPLOY_BRANCH: a feature-branch
|
|
36 |
+ |
# tip, an unknown sha, or a commit not on the deploy branch never reaches the
|
|
37 |
+ |
# build/install lines (exit 4). Only sandod writes the bare repo (fetching main
|
|
38 |
+ |
# from the authenticated upstream), so its main is a trustworthy provenance seal.
|
|
39 |
+ |
# A signed-tag check is the planned follow-up once release signing exists.
|
|
40 |
+ |
#
|
|
41 |
+ |
# Safety net: a clean build (no stale incremental cache) plus a --check-config
|
|
42 |
+ |
# self-test of the freshly built binary against the LIVE config gate the install.
|
|
43 |
+ |
# A stale incremental object once produced a sandod that could not parse its own
|
|
44 |
+ |
# node_health config and crash-looped (postmortem #6); either guard alone stops
|
|
45 |
+ |
# that binary from ever being installed.
|
| 29 |
46 |
|
set -euo pipefail
|
| 30 |
47 |
|
|
| 31 |
48 |
|
SHA="${1:-}"
|
| 35 |
52 |
|
fi
|
| 36 |
53 |
|
|
| 37 |
54 |
|
SELF_DIR="${SANDO_SELF_UPDATE_DIR:-/srv/sando/self-update}"
|
| 38 |
|
- |
UPSTREAM_URL="${SANDO_UPSTREAM_URL:-git@ssh.makenot.work:max/makenotwork.git}"
|
|
55 |
+ |
UPSTREAM_URL="${SANDO_UPSTREAM_URL:-/srv/sando/mnw.git}"
|
| 39 |
56 |
|
BUILD_USER="${SANDO_BUILD_USER:-sando}"
|
| 40 |
57 |
|
BIN="${SANDO_BIN:-/usr/local/bin/sandod}"
|
| 41 |
58 |
|
DEPLOY_BRANCH="${SANDO_DEPLOY_BRANCH:-main}"
|
|
59 |
+ |
DAEMON_CONFIG="${SANDO_DAEMON_CONFIG:-/etc/sando/sando-daemon.toml}"
|
| 42 |
60 |
|
REPO_DIR="$SELF_DIR/MNW"
|
| 43 |
61 |
|
BUILD_HOME="$(getent passwd "$BUILD_USER" | cut -d: -f6)"
|
| 44 |
62 |
|
|
| 56 |
74 |
|
git clone '$UPSTREAM_URL' '$REPO_DIR'
|
| 57 |
75 |
|
fi
|
| 58 |
76 |
|
cd '$REPO_DIR'
|
|
77 |
+ |
# Pin origin to the configured source every run, so switching
|
|
78 |
+ |
# SANDO_UPSTREAM_URL (e.g. remote -> local bare repo) takes effect on an
|
|
79 |
+ |
# already-cloned checkout instead of silently keeping the old remote.
|
|
80 |
+ |
git remote set-url origin '$UPSTREAM_URL'
|
| 59 |
81 |
|
git fetch --prune origin
|
| 60 |
|
- |
# Provenance seal: the sha must be reachable from the deploy branch on the
|
| 61 |
|
- |
# canonical remote. --is-ancestor exits 1 for a non-ancestor and >1 for a
|
|
82 |
+ |
# Provenance seal: the sha must be reachable from the deploy branch in the
|
|
83 |
+ |
# source repo. --is-ancestor exits 1 for a non-ancestor and >1 for a
|
| 62 |
84 |
|
# bad/unresolvable ref, so any non-deploy-branch sha is refused fail-closed
|
| 63 |
85 |
|
# before a single line is built or installed.
|
| 64 |
86 |
|
if ! git merge-base --is-ancestor '$SHA' 'origin/$DEPLOY_BRANCH'; then
|
| 67 |
89 |
|
fi
|
| 68 |
90 |
|
git checkout --detach '$SHA'
|
| 69 |
91 |
|
cd sando
|
|
92 |
+ |
# Clean build: wipe the workspace target so no stale incremental object
|
|
93 |
+ |
# survives across shas. A reused pre-node_health Gate enum object once
|
|
94 |
+ |
# produced a sandod that crash-looped on the current config (postmortem
|
|
95 |
+ |
# #6). Self-updates are rare, so a full recompile is a cheap insurance.
|
|
96 |
+ |
cargo clean
|
| 70 |
97 |
|
cargo build --release --locked -p sando-daemon
|
| 71 |
98 |
|
"
|
| 72 |
99 |
|
|
| 75 |
102 |
|
NEW_BIN="$REPO_DIR/sando/target/release/sandod"
|
| 76 |
103 |
|
[[ -x "$NEW_BIN" ]] || { echo "sando-self-update: build produced no binary at $NEW_BIN" >&2; exit 3; }
|
| 77 |
104 |
|
|
|
105 |
+ |
# Self-test the fresh binary against the LIVE config BEFORE the swap: prove it can
|
|
106 |
+ |
# load + parse the exact daemon config + topology sandod will boot against. Run as
|
|
107 |
+ |
# the build user (not root) so the readability check matches the running daemon's
|
|
108 |
+ |
# identity. A binary that can't parse the current config (the postmortem #6
|
|
109 |
+ |
# brick) fails here and is never installed — sandod keeps running on the old one.
|
|
110 |
+ |
echo "sando-self-update: self-testing $NEW_BIN against $DAEMON_CONFIG"
|
|
111 |
+ |
if ! runuser -u "$BUILD_USER" -- env SANDO_CONFIG="$DAEMON_CONFIG" "$NEW_BIN" --check-config; then
|
|
112 |
+ |
echo "sando-self-update: new binary FAILED --check-config against $DAEMON_CONFIG; refusing to install (sandod left running on the current binary)" >&2
|
|
113 |
+ |
exit 5
|
|
114 |
+ |
fi
|
|
115 |
+ |
|
| 78 |
116 |
|
# Install + restart as root. install is atomic (writes a temp then renames), so
|
| 79 |
117 |
|
# a concurrent exec of $BIN never sees a half-written file.
|
| 80 |
118 |
|
echo "sando-self-update: installing $NEW_BIN -> $BIN and restarting sandod"
|