orchestrator-host: weekly opencode auto-upgrade (latest-release check, reinstall, restart web)
opencode-install only installs when the binary is missing, so the standalone CLI aged in place (1.18.29 for weeks while 1.18.33+ was out); opencode's built-in autoupdate never fires here because every agent runs inside the long-lived opencode serve. New opencode-upgrade.service + weekly timer (Tue 02:00 UTC, an hour before the host auto-update) compares the installed version with the latest GitHub release, reinstalls via the official installer when they differ, restarts opencode-web so the new binary takes effect, and verifies the UI answers its 401 challenge. The auto-update.nix busy gate is replicated so a mid-flight CI run / weekly upgrade / sweep is never cut (skipped runs retry next week). Deploying this module does not itself bump opencode: boot installs stay install-if-missing and the upgrade is timer-driven only. State per run: .cc-ci-logs/opencode-update-state.
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user