2026-03-25 15:52:26 +00:00
#!/bin/bash
# Self-update: pull latest code from git.tx1138.com and apply
# Designed to run on installed Archipelago nodes (as archipelago user)
#
# Usage:
# ./self-update.sh # Check + apply if available
# ./self-update.sh --check # Check only, don't apply
# ./self-update.sh --force # Apply even if already up to date
#
# The script:
# 1. Pulls latest code from origin (git.tx1138.com)
# 2. Builds the Rust backend (release mode)
# 3. Builds the Vue frontend (production mode)
# 4. Installs the new binary and web UI
# 5. Restarts the archipelago service
# 6. Verifies health after restart
set -euo pipefail
REPO_DIR = " $HOME /archy "
BACKEND_DIR = " $REPO_DIR /core "
FRONTEND_DIR = " $REPO_DIR /neode-ui "
INSTALL_BIN = "/usr/local/bin/archipelago"
INSTALL_WEB = "/opt/archipelago/web-ui"
STATE_FILE = "/var/lib/archipelago/update_state.json"
LOG_FILE = "/var/lib/archipelago/update.log"
LOCK_FILE = "/tmp/archipelago-update.lock"
# Colors
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
log( ) { echo -e " ${ BLUE } [ $( date '+%H:%M:%S' ) ] ${ NC } $* " | tee -a " $LOG_FILE " ; }
ok( ) { echo -e " ${ GREEN } [ $( date '+%H:%M:%S' ) ] OK ${ NC } $* " | tee -a " $LOG_FILE " ; }
err( ) { echo -e " ${ RED } [ $( date '+%H:%M:%S' ) ] ERROR ${ NC } $* " | tee -a " $LOG_FILE " ; }
warn( ) { echo -e " ${ YELLOW } [ $( date '+%H:%M:%S' ) ] WARN ${ NC } $* " | tee -a " $LOG_FILE " ; }
cleanup( ) {
rm -f " $LOCK_FILE "
}
trap cleanup EXIT
# Prevent concurrent updates
if [ -f " $LOCK_FILE " ] ; then
pid = $( cat " $LOCK_FILE " 2>/dev/null)
if kill -0 " $pid " 2>/dev/null; then
err " Update already in progress (PID $pid ) "
exit 1
fi
warn "Stale lock file found, removing"
rm -f " $LOCK_FILE "
fi
echo $$ > " $LOCK_FILE "
# Parse args
CHECK_ONLY = false
FORCE = false
while [ [ $# -gt 0 ] ] ; do
case " $1 " in
--check) CHECK_ONLY = true; shift ; ;
--force) FORCE = true; shift ; ;
*) shift ; ;
esac
done
# Ensure repo exists
if [ ! -d " $REPO_DIR /.git " ] ; then
err " Repo not found at $REPO_DIR "
err "Clone it first: git clone https://git.tx1138.com/lfg2025/archy ~/archy"
exit 1
fi
cd " $REPO_DIR "
2026-05-19 14:29:20 -04:00
if ! command -v nano >/dev/null 2>& 1; then
log "Installing nano for Archipelago terminal..."
if sudo apt-get update -qq 2>>" $LOG_FILE " && sudo apt-get install -y -qq nano 2>>" $LOG_FILE " ; then
ok "nano installed"
else
warn "Unable to install nano automatically; continuing update"
fi
fi
2026-03-25 15:52:26 +00:00
# Fetch latest
log "Fetching from origin..."
git fetch origin main --quiet 2>>" $LOG_FILE "
# Check if there are updates
LOCAL = $( git rev-parse HEAD)
REMOTE = $( git rev-parse origin/main)
if [ " $LOCAL " = " $REMOTE " ] && [ " $FORCE " = "false" ] ; then
ok " Already up to date ( $LOCAL ) "
if [ " $CHECK_ONLY " = "true" ] ; then
echo '{"update_available": false, "current": "' " $LOCAL " '"}'
fi
exit 0
fi
# Calculate what changed
COMMITS_BEHIND = $( git rev-list HEAD..origin/main --count)
log " Update available: $COMMITS_BEHIND commits behind "
log " Local: $LOCAL "
log " Remote: $REMOTE "
if [ " $CHECK_ONLY " = "true" ] ; then
CHANGELOG = $( git log HEAD..origin/main --oneline --no-merges | head -20)
echo '{"update_available": true, "current": "' " $LOCAL " '", "latest": "' " $REMOTE " '", "commits_behind": ' " $COMMITS_BEHIND " '}'
echo ""
echo "Changes:"
echo " $CHANGELOG "
exit 0
fi
# Backup current binary
BACKUP_DIR = "/var/lib/archipelago/update-backup"
mkdir -p " $BACKUP_DIR "
if [ -f " $INSTALL_BIN " ] ; then
cp " $INSTALL_BIN " " $BACKUP_DIR /archipelago.bak "
log "Backed up current binary"
fi
# Pull latest code
log "Pulling latest code..."
git pull origin main --ff-only 2>>" $LOG_FILE " || {
err "Git pull failed — local changes? Run: git reset --hard origin/main"
exit 1
}
NEW_VERSION = $( git rev-parse --short HEAD)
log " Now at: $NEW_VERSION "
# Build backend
log "Building Rust backend (release)..."
cd " $BACKEND_DIR "
if cargo build --release --workspace 2>>" $LOG_FILE " ; then
ok "Backend built successfully"
else
err "Backend build failed — rolling back"
cd " $REPO_DIR "
git reset --hard " $LOCAL " 2>>" $LOG_FILE "
exit 1
fi
# Install binary
BUILT_BIN = " $BACKEND_DIR /target/release/archipelago "
if [ ! -f " $BUILT_BIN " ] ; then
err " Built binary not found at $BUILT_BIN "
exit 1
fi
sudo cp " $BUILT_BIN " " $INSTALL_BIN "
sudo chmod +x " $INSTALL_BIN "
ok "Backend installed"
# Build frontend
log "Building Vue frontend (production)..."
cd " $FRONTEND_DIR "
npm ci --silent 2>>" $LOG_FILE " || npm install --silent 2>>" $LOG_FILE "
if npm run build 2>>" $LOG_FILE " ; then
ok "Frontend built successfully"
else
err "Frontend build failed — backend already updated, service may need manual fix"
exit 1
fi
2026-04-23 13:21:49 -04:00
# Install frontend (always ship fresh AIUI from demo/aiui; preserve claude-login.html)
2026-03-25 15:52:26 +00:00
BUILT_WEB = " $REPO_DIR /web/dist/neode-ui "
if [ -d " $BUILT_WEB " ] ; then
2026-04-23 13:21:49 -04:00
# Bake AIUI into the built tree so rsync --delete does not wipe it.
# demo/aiui is the canonical AIUI bundle checked into the repo; copying
# it here means every self-update ships a matching AIUI version instead
# of preserving whatever stale copy happened to be on disk (which is
# empty on nodes where an earlier ad-hoc deploy blew it away).
if [ -d " $REPO_DIR /demo/aiui " ] && [ -f " $REPO_DIR /demo/aiui/index.html " ] ; then
log "Staging AIUI bundle from demo/aiui into frontend dist..."
rm -rf " $BUILT_WEB /aiui "
cp -r " $REPO_DIR /demo/aiui " " $BUILT_WEB /aiui "
else
warn "demo/aiui not found in repo; existing /opt/archipelago/web-ui/aiui will be wiped by rsync --delete"
fi
# Sync new files, preserving claude-login.html (per-node admin bookmark)
2026-03-25 15:52:26 +00:00
sudo rsync -a --delete \
--exclude 'claude-login.html' \
" $BUILT_WEB / " " $INSTALL_WEB / "
ok "Frontend installed"
else
warn " Frontend build output not found at $BUILT_WEB — skipping "
fi
2026-04-23 10:07:53 -04:00
# Update helper scripts in /opt/archipelago/scripts/
# These are canonical home; keep a copy at /opt/archipelago/image-versions.sh
# for backward compatibility with older binaries that still look there.
SCRIPTS_DEST = "/opt/archipelago/scripts"
sudo mkdir -p " $SCRIPTS_DEST "
2026-05-19 09:26:43 -04:00
for script in image-versions.sh reconcile-containers.sh container-specs.sh container-doctor.sh sync-npm-public-hosts.sh app-surface-smoke-test.sh bitcoin-stack-lifecycle-test.sh; do
2026-04-23 10:07:53 -04:00
src = " $REPO_DIR /scripts/ $script "
if [ -f " $src " ] ; then
sudo install -m 755 " $src " " $SCRIPTS_DEST / $script "
ok " Updated $script "
else
warn " Missing $src — skipping "
fi
done
# Legacy path for image-versions.sh (older binaries looked here first)
2026-03-25 15:52:26 +00:00
if [ -f " $REPO_DIR /scripts/image-versions.sh " ] ; then
sudo cp " $REPO_DIR /scripts/image-versions.sh " /opt/archipelago/image-versions.sh
fi
2026-05-01 03:14:07 -04:00
# Sync app manifests and app-local build contexts into the canonical
# production manifest root. The backend orchestrator loads install specs from
# /opt/archipelago/apps; updating only the binary/frontend can leave a node
# with new installer logic but stale or missing app manifests.
APPS_DEST = "/opt/archipelago/apps"
if [ -d " $REPO_DIR /apps " ] ; then
sudo mkdir -p " $APPS_DEST "
sudo rsync -a --delete " $REPO_DIR /apps/ " " $APPS_DEST / "
ok "App manifests synced"
else
warn " Apps directory not found at $REPO_DIR /apps — install manifests may be stale "
fi
feat(self-update): sync and rebuild UI containers on OTA
self-update.sh previously rebuilt only the backend binary and Vue
frontend. The custom UI containers (archy-bitcoin-ui, archy-lnd-ui,
archy-electrs-ui) were left untouched forever. That meant any change to
docker/<ui>/{Dockerfile, nginx.conf, index.html, ...} never reached a
running node through OTA; it required a manual SSH + rebuild. This is
exactly why the lnd-ui port fix didnt reach .228 in v1.7.43-alpha.
Add a sync-and-rebuild stage:
1. Hash each docker/<ui>/ tree (content-only, path-stable via
`cd && find` so src and dst compare equal when identical).
2. rsync changed trees to /opt/archipelago/docker/<ui>/.
3. For each changed UI: rebuild image as the archipelago user
(rootless podman), then stop+remove+recreate the container using
the canonical spec from scripts/container-specs.sh. Port mappings,
caps, memory, and security opts all come from the spec, so the
runtime cant drift from the tree.
Also install first-boot-containers.sh into /opt/archipelago/scripts/ so
a later reconciler run or reboot picks up current orchestration logic.
Idempotent: if no UI tree changed since the last update, the whole stage
is a no-op beyond the hash compare. Verified end-to-end on .228 with a
synthetic change to lnd-ui: detection, sync, build, recreate, and HTTP
200 on both the direct container port and the host-nginx /app/lnd/
proxy.
2026-04-23 15:48:53 -04:00
# Update first-boot-containers.sh too (the canonical first-boot orchestrator).
# Nodes run it once on install, but keeping a fresh copy on disk means any
# future boot or reconciler invocation uses current port specs and caps.
if [ -f " $REPO_DIR /scripts/first-boot-containers.sh " ] ; then
sudo install -m 755 " $REPO_DIR /scripts/first-boot-containers.sh " \
" $SCRIPTS_DEST /first-boot-containers.sh "
fi
# Sync UI container source trees (docker/bitcoin-ui, docker/lnd-ui,
# docker/electrs-ui) into /opt/archipelago/docker/<name>/. If any file in a
# UI tree changed since last update, rebuild that image and recreate its
# container using the spec from container-specs.sh. This is what prevented
# the lnd-ui port mismatch from reaching nodes through OTA: self-update used
# to update only the backend + frontend, never the UI container images.
UI_DOCKER_DEST = "/opt/archipelago/docker"
sudo mkdir -p " $UI_DOCKER_DEST "
UI_REBUILD_LIST = ""
for ui in bitcoin-ui lnd-ui electrs-ui; do
src = " $REPO_DIR /docker/ $ui "
dst = " $UI_DOCKER_DEST / $ui "
[ -d " $src " ] || continue
# Hash source tree to decide if rebuild is needed. Any content change
# (Dockerfile, nginx.conf, index.html, assets) triggers a rebuild.
# Hash file contents only (not paths or metadata) so src and dst match
# when their contents are identical regardless of directory prefix.
src_hash = $( ( cd " $src " && find . -type f | LC_ALL = C sort | xargs sha256sum 2>/dev/null) | sha256sum | cut -d' ' -f1)
dst_hash = ""
if [ -d " $dst " ] ; then
dst_hash = $( ( cd " $dst " && find . -type f | LC_ALL = C sort | xargs sha256sum 2>/dev/null) | sha256sum | cut -d' ' -f1)
fi
if [ " $src_hash " != " $dst_hash " ] ; then
log " UI source changed for $ui ; syncing and marking for rebuild "
sudo rsync -a --delete " $src / " " $dst / "
UI_REBUILD_LIST = " $UI_REBUILD_LIST $ui "
else
ok " UI source unchanged for $ui "
fi
done
# Rebuild changed UI images + recreate containers as the archipelago user
# (rootless podman storage lives under ~archipelago). Port mappings and caps
# come from scripts/container-specs.sh so spec drift can't sneak in.
if [ -n " $UI_REBUILD_LIST " ] ; then
log " Rebuilding UI containers: $UI_REBUILD_LIST "
# shellcheck disable=SC1091
# container-specs.sh provides load_spec_archy-<ui> and mem_limit <name>.
SPECS = " $SCRIPTS_DEST /container-specs.sh "
if [ ! -f " $SPECS " ] ; then
warn " container-specs.sh missing at $SPECS ; skipping UI rebuild "
else
for ui in $UI_REBUILD_LIST ; do
cname = " archy- $ui "
log " rebuilding $cname from $UI_DOCKER_DEST / $ui "
# Build image as archipelago user so it lands in the right store.
if ! sudo -u archipelago bash -c "
export XDG_RUNTIME_DIR = /run/user/\$ ( id -u archipelago)
cd '$UI_DOCKER_DEST/$ui' &&
podman build --no-cache -t 'localhost/$ui:local' . >>'$LOG_FILE' 2>& 1
" ; then
err " build failed for $ui ; keeping existing container "
continue
fi
# Recreate container using spec from container-specs.sh.
if ! sudo -u archipelago bash -c "
export XDG_RUNTIME_DIR = /run/user/\$ ( id -u archipelago)
source '$SPECS'
load_spec_$cname || { echo 'spec load failed for $cname' ; exit 1; }
podman stop '$cname' 2>/dev/null || true
podman rm '$cname' 2>/dev/null || true
PORT_ARG = ''
[ -n \" \$ SPEC_PORTS\" ] && PORT_ARG = \" -p \$ SPEC_PORTS\"
NET_ARG = ''
[ \" \$ SPEC_NETWORK\" = 'host' ] && NET_ARG = '--network host'
CAP_ARGS = '--cap-drop ALL'
for c in \$ SPEC_CAPS; do CAP_ARGS = \" \$ CAP_ARGS --cap-add \$ c\" ; done
podman run -d --name '$cname' \$ PORT_ARG \$ NET_ARG \\
--user 0:0 \$ CAP_ARGS \\
--memory= \" \$ SPEC_MEMORY\" \\
--restart unless-stopped \\
--security-opt \" \$ SPEC_SECURITY\" \\
'localhost/$ui:local' >>'$LOG_FILE' 2>& 1
" ; then
err " recreate failed for $cname "
continue
fi
ok " $cname rebuilt and running "
done
fi
fi
2026-06-12 03:00:15 -04:00
# Update kiosk display helpers used by HDMI/TV installs.
if [ -f " $REPO_DIR /image-recipe/configs/archipelago-kiosk-launcher.sh " ] ; then
sudo install -m 755 " $REPO_DIR /image-recipe/configs/archipelago-kiosk-launcher.sh " \
/usr/local/bin/archipelago-kiosk-launcher
ok "Updated archipelago-kiosk-launcher"
fi
2026-05-13 15:09:22 -04:00
# Update systemd services if changed
SYSTEMD_UNITS_CHANGED = false
2026-06-12 03:00:15 -04:00
for unit in archipelago.service archipelago-fips.service archipelago-kiosk.service archipelago-kiosk-watchdog.service; do
2026-05-13 15:09:22 -04:00
src = " $REPO_DIR /image-recipe/configs/ $unit "
dst = " /etc/systemd/system/ $unit "
[ -f " $src " ] || continue
if [ ! -f " $dst " ] || ! diff -q " $src " " $dst " & >/dev/null; then
sudo install -m 644 " $src " " $dst "
SYSTEMD_UNITS_CHANGED = true
ok " Updated $unit "
2026-03-25 15:52:26 +00:00
fi
2026-05-13 15:09:22 -04:00
done
if [ " $SYSTEMD_UNITS_CHANGED " = "true" ] ; then
sudo systemctl daemon-reload
2026-03-25 15:52:26 +00:00
fi
2026-04-30 16:29:56 -04:00
# Keep the doctor timer/service current too. Container uptime fixes rely on
# these units as much as on the helper scripts themselves.
DOCTOR_UNITS_CHANGED = false
for unit in archipelago-doctor.service archipelago-doctor.timer; do
src = " $REPO_DIR /image-recipe/configs/ $unit "
dst = " /etc/systemd/system/ $unit "
[ -f " $src " ] || continue
if [ ! -f " $dst " ] || ! diff -q " $src " " $dst " & >/dev/null; then
sudo install -m 644 " $src " " $dst "
DOCTOR_UNITS_CHANGED = true
ok " Updated $unit "
fi
done
if [ " $DOCTOR_UNITS_CHANGED " = "true" ] ; then
sudo systemctl daemon-reload
sudo systemctl enable --now archipelago-doctor.timer 2>>" $LOG_FILE " || \
warn "Failed to enable archipelago-doctor.timer"
fi
2026-04-23 12:02:46 -04:00
# Install/refresh tmpfiles.d rules. The logs rule creates
# /var/log/archipelago/ + container-installs.log with archipelago:archipelago
# ownership so the non-root backend can append install audit lines.
# Apply immediately so existing nodes don't need a reboot.
if [ -f " $REPO_DIR /image-recipe/configs/archipelago-tmpfiles.conf " ] ; then
sudo install -m 644 " $REPO_DIR /image-recipe/configs/archipelago-tmpfiles.conf " \
/usr/lib/tmpfiles.d/archipelago-logs.conf
sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/archipelago-logs.conf 2>/dev/null || true
ok "Log tmpfiles rule installed"
fi
2026-03-25 15:52:26 +00:00
# Restart service
log "Restarting archipelago service..."
sudo systemctl restart archipelago
# Wait for health
log "Waiting for backend health..."
for i in $( seq 1 30) ; do
if curl -sf http://127.0.0.1:5678/health > /dev/null 2>& 1; then
ok " Backend healthy after ${ i } s "
break
fi
if [ " $i " = "30" ] ; then
err "Backend failed to start within 30s"
warn "Rolling back binary..."
if [ -f " $BACKUP_DIR /archipelago.bak " ] ; then
sudo cp " $BACKUP_DIR /archipelago.bak " " $INSTALL_BIN "
sudo systemctl restart archipelago
err "Rolled back to previous binary"
fi
exit 1
fi
sleep 1
done
# Update state file for the UI
python3 -c "
import json, datetime
state = {
'current_version' : '$NEW_VERSION' ,
'last_check' : datetime.datetime.utcnow( ) .isoformat( ) + 'Z' ,
'available_update' : None,
'update_in_progress' : False,
'rollback_available' : True,
'schedule' : 'daily_check'
}
with open( '$STATE_FILE' , 'w' ) as f:
json.dump( state, f, indent = 2)
" 2>/dev/null || true
echo ""
ok " Update complete: $LOCAL -> $NEW_VERSION "
log "Changelog:"
git log " $LOCAL " .." $NEW_VERSION " --oneline --no-merges | head -10 | tee -a " $LOG_FILE "