orchestrator-host: weekly opencode auto-upgrade (latest-release check, reinstall, restart web) #29

Merged
autonomic-bot merged 1 commits from opencode-auto-upgrade into main 2026-10-05 18:40:32 +00:00
Showing only changes of commit 88e5a93f94 - Show all commits
+88
View File
@@ -188,6 +188,94 @@ SSHCFG
'';
};
# ---- weekly opencode CLI auto-upgrade ----------------------------------------------------
# opencode is not in nixpkgs: opencode-install (above) only installs the standalone CLI when
# the binary is MISSING, so the installed version just ages in place (it sat on 1.18.29 for
# weeks while 1.18.33 was out). opencode's built-in autoupdate does not cover this host: it
# auto-applies patch releases only and only fires on a fresh interactive TUI start, and every
# agent here lives inside the long-lived `opencode serve` (opencode-web). This unit is the
# mechanism instead. It runs weekly in the Tuesday maintenance window (an hour BEFORE the
# host auto-update at Tue 03:00 UTC, and clear of the Thursday recipe-upgrade run and the
# Sunday canonical sweep), and:
# 1. compares the installed version with the latest GitHub release (no-op when equal);
# 2. reinstalls via the official installer (same code path as opencode-install) when they
# differ, and re-links ~/.local/bin/opencode;
# 3. restarts opencode-web so the new binary actually takes effect — sessions attached to
# it (orchestrator, upgrader, report) drop and their supervisors re-attach/resume, which
# is why the busy gate below must hold: a mid-flight CI/upgrade run is never cut, and a
# skipped run simply retries next week.
# Deploying this module never itself bumps opencode: boot/activation installs stay
# install-if-missing in opencode-install, and this unit is timer-driven only.
# The outcome of each run lands in .cc-ci-logs/opencode-update-state (read by /cc-ci-status).
systemd.services.opencode-upgrade = {
description = "Weekly opencode CLI auto-upgrade (latest release → reinstall → restart opencode-web)";
after = [ "network-online.target" "opencode-install.service" "opencode-web.service" ];
wants = [ "network-online.target" ];
serviceConfig = { Type = "oneshot"; TimeoutStartSec = "30min"; };
path = with pkgs; [ curl bash coreutils gnugrep gnutar gzip unzip systemd util-linux procps ];
environment = { HOME = "/home/loops"; OPENCODE_UI_HOST = cfg.opencodeUiHost; };
script = ''
set -u
BIN=/home/loops/.local/bin/opencode
STATE=/srv/cc-ci-orch/.cc-ci-logs/opencode-update-state
ocver() { "$BIN" --version 2>/dev/null | tail -1 || true; }
state() { printf '%s result=%s installed=%s note=%s\n' "$(date -u +%FT%TZ)" "$1" "$2" "$3" > "$STATE"; chown loops:users "$STATE" 2>/dev/null || true; }
busy() { echo "BUSY: $1 — skipping this week's opencode upgrade (retries next week)"; state skipped "$(ocver)" "$1"; exit 0; }
# Same busy gate as auto-update.nix: never cut a CI run, the weekly upgrade or the sweep.
pgrep -f run_recipe_ci >/dev/null && busy "a CI run is in flight"
systemctl is-active --quiet nightly-sweep.service && busy "the canonical sweep is running"
runuser -u loops -- tmux has-session -t cc-ci-upgrader 2>/dev/null && busy "the weekly recipe-upgrade run is in flight (tmux cc-ci-upgrader)"
runuser -u loops -- tmux has-session -t cc-ci-report 2>/dev/null && busy "the weekly report is being written (tmux cc-ci-report)"
if [ -r /run/secrets/bridge_drone_token ]; then
running=$(curl -s -m 20 -H "Authorization: Bearer $(cat /run/secrets/bridge_drone_token)" \
"https://drone.ci.commoninternet.net/api/repos/recipe-maintainers/cc-ci/builds?per_page=10" \
| grep -o '"status":"running"' | wc -l)
[ "''${running:-0}" -eq 0 ] || busy "$running Drone build(s) running"
fi
INSTALLED=$(ocver)
LATEST=$(curl -fsSL -m 30 https://api.github.com/repos/anomalyco/opencode/releases/latest \
| grep -Po '"tag_name":\s*"v?\K[0-9][0-9.]*' || true)
[ -n "$LATEST" ] || { echo "could not determine the latest opencode release"; state failed "''${INSTALLED:-none}" "latest-unresolved"; exit 1; }
echo "installed: ''${INSTALLED:-none} latest: $LATEST"
if [ "$INSTALLED" = "$LATEST" ]; then
echo "opencode is up to date"
state ok "$INSTALLED" "up-to-date"
exit 0
fi
echo "upgrading opencode: ''${INSTALLED:-none} -> $LATEST"
runuser -u loops -- env HOME=/home/loops bash -c 'curl -fsSL https://opencode.ai/install | bash' \
|| { echo "install failed — the previously installed version is untouched"; state failed "''${INSTALLED:-none}" "install-failed"; exit 1; }
mkdir -p /home/loops/.local/bin
ln -sfn /home/loops/.opencode/bin/opencode /home/loops/.local/bin/opencode
NEWVER=$(ocver)
[ "$NEWVER" = "$LATEST" ] || { echo "install ran but opencode reports $NEWVER (wanted $LATEST) — not restarting"; state failed "$NEWVER" "install-mismatch"; exit 1; }
echo "restarting opencode-web so the new binary takes effect (attached sessions drop; their supervisors resume)"
systemctl restart opencode-web.service
sleep 10
systemctl is-active --quiet opencode-web.service || { echo "opencode-web did not come back after the upgrade"; state failed "$NEWVER" "web-restart-failed"; exit 1; }
code=$(curl -s -m 20 -o /dev/null -w '%{http_code}' --resolve "$OPENCODE_UI_HOST:443:127.0.0.1" "https://$OPENCODE_UI_HOST/")
[ "$code" = "401" ] || { echo "opencode UI answered $code, not the 401 auth challenge"; state failed "$NEWVER" "ui-check-$code"; exit 1; }
echo "opencode upgraded to $NEWVER; opencode-web restarted and healthy"
state ok "$NEWVER" "upgraded; web restarted"
'';
};
systemd.timers.opencode-upgrade = {
wantedBy = [ "timers.target" ];
timerConfig = {
# Weekly slot in the Tuesday maintenance window, an hour BEFORE the host auto-update
# (cc-ci-auto-update.timer: Tue 03:00 UTC) so a restarted opencode-web has settled before
# that run's health check; Persistent=false, like the auto-update — no catch-up at boot.
OnCalendar = "Tue *-*-* 02:00:00 UTC";
Persistent = false;
RandomizedDelaySec = "10min";
};
};
# ---- opencode web server: one shared instance the opencode-backed agents attach to -------
# Provider creds come from /srv/cc-ci/.testenv (out of band, see README).
systemd.services.opencode-web = {