Release 0.2.0 #4

Merged
dguiducci merged 96 commits from main into release 2026-08-17 18:07:20 +01:00
6 changed files with 192 additions and 8 deletions
Showing only changes of commit bb5226a9a9 - Show all commits
+15
View File
@@ -64,3 +64,18 @@ jobs:
done done
echo "[nightly] Deployed:" echo "[nightly] Deployed:"
ls -lh "$DEST/" ls -lh "$DEST/"
- name: Publish the nightly installer
run: |
cd "${GITHUB_WORKSPACE:-.}"
# install-nightly.sh is served straight from the web root
# (curl -fsSL https://builds.skaldagent.net/install-nightly.sh | bash),
# so without this it stays whatever was copied there by hand and drifts
# from the repo — a fix to the installer would reach every existing box
# through update.sh but never a new one. Same atomic publish as the
# tarballs: a client mid-download never sees a half-written script.
ROOT=/var/www/builds.skaldagent.net
cp install-nightly.sh "$ROOT/.install-nightly.sh.tmp"
chmod 644 "$ROOT/.install-nightly.sh.tmp"
mv -f "$ROOT/.install-nightly.sh.tmp" "$ROOT/install-nightly.sh"
echo "[nightly] Published install-nightly.sh"
+15
View File
@@ -104,3 +104,18 @@ jobs:
printf '%s\n' "$VERSION" > "$DEST/.LATEST.tmp" printf '%s\n' "$VERSION" > "$DEST/.LATEST.tmp"
mv -f "$DEST/.LATEST.tmp" "$DEST/LATEST" mv -f "$DEST/.LATEST.tmp" "$DEST/LATEST"
echo "[release] Updated releases/LATEST → $VERSION" echo "[release] Updated releases/LATEST → $VERSION"
- name: Publish the release installer
run: |
cd "${GITHUB_WORKSPACE:-.}"
# install.sh is served straight from the web root
# (curl -fsSL https://builds.skaldagent.net/install.sh | bash), so
# without this it stays whatever was copied there by hand and drifts
# from the repo — a fix to the installer would reach every existing box
# through update.sh but never a new one. Published here rather than on
# every push so the served installer always matches a real release.
ROOT=/var/www/builds.skaldagent.net
cp install.sh "$ROOT/.install.sh.tmp"
chmod 644 "$ROOT/.install.sh.tmp"
mv -f "$ROOT/.install.sh.tmp" "$ROOT/install.sh"
echo "[release] Published install.sh"
+16
View File
@@ -98,6 +98,22 @@ systemd service → ExecStart=run.sh
**Fix**: removed `Requires=docker.service` from the user unit template in both install scripts. Kept `After=docker.service` (advisory, doesn't block if the unit isn't found). **Fix**: removed `Requires=docker.service` from the user unit template in both install scripts. Kept `After=docker.service` (advisory, doesn't block if the unit isn't found).
**Follow-up**: `After=docker.service` was dropped too. It never did anything — a _user_ manager has no view of system units, so the ordering was silently ignored rather than merely advisory, and keeping it suggested a guarantee that was not there. What actually handles the boot race is `Restart` (see below): the server fails fast when the Docker daemon is unreachable, and systemd brings it back a few seconds later.
## Bug fix: the server dies when you log out ✅
**Problem**: `systemctl --user start skald-circle` worked, but closing the SSH session killed the server — and it never came up at boot. Not an application bug: a `--user` unit runs under the per-user manager (`user@UID.service`), which systemd starts at first login and **stops when the user's last session ends**, tearing down every user service in the cgroup. No crash, no error in the journal — the whole cgroup is simply killed.
**Fix**: both installers now run `loginctl enable-linger $USER` after installing the unit (helper `enable_linger`, tried unprivileged first, then `sudo -n`, then interactive `sudo`, and only warns if all three fail — a missing linger must never abort an install). `update.sh` carries the same helper so an installation predating this fix is healed by an ordinary update.
**Also**: `Restart=on-failure``Restart=always`. `run.sh` exits 0 on _any_ graceful shutdown, including one nobody asked for (a stray SIGTERM to the server), which `on-failure` reads as a clean stop and leaves the box down. An explicit `systemctl --user stop` is unaffected — systemd never restarts after a requested stop. With lingering on, this is also what absorbs the boot race against Docker.
## Bug fix: update.sh never stopped or restarted the service ✅
**Problem**: `stop_service` and `start_service` matched `case "$OS" in Linux) … Darwin)`, but `$OS` had already been normalized to `linux`/`darwin` at the top of the script. Every branch fell through: both functions were no-ops. So the updater extracted the tarball **over the running binary** (`ETXTBSY` on Linux, aborting the update mid-way) and, when extraction did succeed, left the old build running in memory with the safety-net trap firing a restart that was itself a no-op. The careful stop → wait-for-exit → extract ordering the file documents at the top had not been executing at all.
**Fix**: matched the normalized lowercase values, with a comment at the seam saying why the capitalization is load-bearing.
## Bug fix: skald-setup non interattivo con curl | bash ✅ ## Bug fix: skald-setup non interattivo con curl | bash ✅
**Problem**: `skald-setup` controlla `isatty(0)`, ma con `curl ... | bash` stdin è un pipe, quindi saltava senza chiedere username/password. L'installer arrivava fino in fondo ma senza aver creato l'admin. **Problem**: `skald-setup` controlla `isatty(0)`, ma con `curl ... | bash` stdin è un pipe, quindi saltava senza chiedere username/password. L'installer arrivava fino in fondo ma senza aver creato l'admin.
+50 -2
View File
@@ -127,6 +127,43 @@ stop_existing_service() {
fi fi
} }
# ── systemd user lingering ────────────────────────────────────────────────────
# A `systemctl --user` unit runs under the per-user manager (user@UID.service),
# which systemd starts at first login and STOPS when the user's last session
# ends — taking every user service down with it. So without lingering the server
# dies the moment you close the SSH session that started it, and never comes up
# at boot. Enabling it is the whole difference between "runs while I'm logged
# in" and "is a daemon".
enable_linger() {
local target="${USER:-$(id -un)}"
if ! command -v loginctl >/dev/null 2>&1; then
warn "loginctl not found — cannot enable lingering."
echo " The server will stop when you log out of this machine."
return 0
fi
case "$(loginctl show-user "$target" --property=Linger 2>/dev/null || true)" in
*=yes) info "✔ Lingering already enabled for ${target}"; return 0 ;;
esac
# Enabling linger for yourself is normally allowed without elevation; fall
# back to sudo, non-interactive first so `curl | bash` never blocks on a
# password prompt it has no terminal to answer.
if loginctl enable-linger "$target" 2>/dev/null \
|| sudo -n loginctl enable-linger "$target" 2>/dev/null \
|| { [ "$IS_INTERACTIVE" = true ] && sudo loginctl enable-linger "$target"; }; then
info "✔ Lingering enabled — the server keeps running after you log out"
else
warn "Could not enable lingering for ${target}."
echo " Without it, the server stops as soon as your last session ends"
echo " and does not start at boot. Run this once, as an administrator:"
echo ""
echo " sudo loginctl enable-linger ${target}"
echo ""
fi
}
# ── Docker install helper ───────────────────────────────────────────────────── # ── Docker install helper ─────────────────────────────────────────────────────
install_docker() { install_docker() {
if [ "$OS" = "linux" ]; then if [ "$OS" = "linux" ]; then
@@ -349,13 +386,21 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
[Unit] [Unit]
Description=Skald Circle (${DISPLAY_VERSION}) Description=Skald Circle (${DISPLAY_VERSION})
Documentation=https://skaldagent.net Documentation=https://skaldagent.net
After=network.target docker.service # No After=docker.service here: this is a *user* unit, and docker.service is a
# system unit the user manager knows nothing about — the dependency would be
# silently ignored. Docker may therefore still be starting when we do; the
# server fails fast when the daemon is unreachable and Restart brings it back a
# few seconds later, so boot ordering settles itself.
[Service] [Service]
Type=simple Type=simple
ExecStart=${INSTALL_DIR}/run.sh ExecStart=${INSTALL_DIR}/run.sh
WorkingDirectory=${INSTALL_DIR} WorkingDirectory=${INSTALL_DIR}
Restart=on-failure # always, not on-failure: run.sh exits 0 on any graceful shutdown, including one
# nobody asked for (a stray SIGTERM to the server), which on-failure would treat
# as a clean stop and leave the box down. An explicit "systemctl --user stop"
# is unaffected — systemd never restarts after a requested stop.
Restart=always
RestartSec=5 RestartSec=5
Environment=SKALD_BIN=${INSTALL_DIR}/bin/skald Environment=SKALD_BIN=${INSTALL_DIR}/bin/skald
Environment=SKALD_SETUP_BIN=${INSTALL_DIR}/bin/skald-setup Environment=SKALD_SETUP_BIN=${INSTALL_DIR}/bin/skald-setup
@@ -368,6 +413,9 @@ SERVICE
systemctl --user enable --now skald-circle.service systemctl --user enable --now skald-circle.service
info "✔ Service installed and started" info "✔ Service installed and started"
enable_linger
echo "" echo ""
echo " Status: systemctl --user status skald-circle" echo " Status: systemctl --user status skald-circle"
echo " Logs: journalctl --user -u skald-circle -f" echo " Logs: journalctl --user -u skald-circle -f"
+50 -2
View File
@@ -130,6 +130,43 @@ stop_existing_service() {
fi fi
} }
# ── systemd user lingering ────────────────────────────────────────────────────
# A `systemctl --user` unit runs under the per-user manager (user@UID.service),
# which systemd starts at first login and STOPS when the user's last session
# ends — taking every user service down with it. So without lingering the server
# dies the moment you close the SSH session that started it, and never comes up
# at boot. Enabling it is the whole difference between "runs while I'm logged
# in" and "is a daemon".
enable_linger() {
local target="${USER:-$(id -un)}"
if ! command -v loginctl >/dev/null 2>&1; then
warn "loginctl not found — cannot enable lingering."
echo " The server will stop when you log out of this machine."
return 0
fi
case "$(loginctl show-user "$target" --property=Linger 2>/dev/null || true)" in
*=yes) info "✔ Lingering already enabled for ${target}"; return 0 ;;
esac
# Enabling linger for yourself is normally allowed without elevation; fall
# back to sudo, non-interactive first so `curl | bash` never blocks on a
# password prompt it has no terminal to answer.
if loginctl enable-linger "$target" 2>/dev/null \
|| sudo -n loginctl enable-linger "$target" 2>/dev/null \
|| { [ "$IS_INTERACTIVE" = true ] && sudo loginctl enable-linger "$target"; }; then
info "✔ Lingering enabled — the server keeps running after you log out"
else
warn "Could not enable lingering for ${target}."
echo " Without it, the server stops as soon as your last session ends"
echo " and does not start at boot. Run this once, as an administrator:"
echo ""
echo " sudo loginctl enable-linger ${target}"
echo ""
fi
}
# ── Docker install helper ───────────────────────────────────────────────────── # ── Docker install helper ─────────────────────────────────────────────────────
install_docker() { install_docker() {
if [ "$OS" = "linux" ]; then if [ "$OS" = "linux" ]; then
@@ -354,13 +391,21 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
[Unit] [Unit]
Description=Skald Circle (release ${VERSION}) Description=Skald Circle (release ${VERSION})
Documentation=https://skaldagent.net Documentation=https://skaldagent.net
After=network.target docker.service # No After=docker.service here: this is a *user* unit, and docker.service is a
# system unit the user manager knows nothing about — the dependency would be
# silently ignored. Docker may therefore still be starting when we do; the
# server fails fast when the daemon is unreachable and Restart brings it back a
# few seconds later, so boot ordering settles itself.
[Service] [Service]
Type=simple Type=simple
ExecStart=${INSTALL_DIR}/run.sh ExecStart=${INSTALL_DIR}/run.sh
WorkingDirectory=${INSTALL_DIR} WorkingDirectory=${INSTALL_DIR}
Restart=on-failure # always, not on-failure: run.sh exits 0 on any graceful shutdown, including one
# nobody asked for (a stray SIGTERM to the server), which on-failure would treat
# as a clean stop and leave the box down. An explicit "systemctl --user stop"
# is unaffected — systemd never restarts after a requested stop.
Restart=always
RestartSec=5 RestartSec=5
Environment=SKALD_BIN=${INSTALL_DIR}/bin/skald Environment=SKALD_BIN=${INSTALL_DIR}/bin/skald
Environment=SKALD_SETUP_BIN=${INSTALL_DIR}/bin/skald-setup Environment=SKALD_SETUP_BIN=${INSTALL_DIR}/bin/skald-setup
@@ -373,6 +418,9 @@ SERVICE
systemctl --user enable --now skald-circle.service systemctl --user enable --now skald-circle.service
info "✔ Service installed and started" info "✔ Service installed and started"
enable_linger
echo "" echo ""
echo " Status: systemctl --user status skald-circle" echo " Status: systemctl --user status skald-circle"
echo " Logs: journalctl --user -u skald-circle -f" echo " Logs: journalctl --user -u skald-circle -f"
+46 -4
View File
@@ -43,6 +43,13 @@ fi
CHANNEL="$(tr -d '[:space:]' < "$CHANNEL_FILE")" CHANNEL="$(tr -d '[:space:]' < "$CHANNEL_FILE")"
# ── Detect interactive stdin ──────────────────────────────────────────────────
if [ -t 0 ]; then
IS_INTERACTIVE=true
else
IS_INTERACTIVE=false
fi
# ── Colours (if terminal) ───────────────────────────────────────────────────── # ── Colours (if terminal) ─────────────────────────────────────────────────────
if [ -t 1 ]; then if [ -t 1 ]; then
RED='\033[0;31m' RED='\033[0;31m'
@@ -96,9 +103,15 @@ STOPPED=0 # set once the service has been stopped
STARTED=0 # set once it has been (re)started STARTED=0 # set once it has been (re)started
# ── Stop service ────────────────────────────────────────────────────────────── # ── Stop service ──────────────────────────────────────────────────────────────
# NOTE: match the *normalized* OS values set above (linux/darwin), not uname's
# capitalized output. Getting this wrong turns both stop_service and
# start_service into silent no-ops, and then none of the ordering this file
# documents at the top actually happens: the tarball is extracted over the
# running binary (ETXTBSY on Linux, aborting the update mid-way), and the
# safety-net restart in cleanup() is a no-op too, so the box stays down.
stop_service() { stop_service() {
case "$OS" in case "$OS" in
Linux) linux)
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
if systemctl --user is-active skald-circle.service >/dev/null 2>&1; then if systemctl --user is-active skald-circle.service >/dev/null 2>&1; then
info "⏹️ Stopping service …" info "⏹️ Stopping service …"
@@ -106,7 +119,7 @@ stop_service() {
fi fi
fi fi
;; ;;
Darwin) darwin)
if command -v launchctl >/dev/null 2>&1; then if command -v launchctl >/dev/null 2>&1; then
if launchctl list com.skald.circle >/dev/null 2>&1; then if launchctl list com.skald.circle >/dev/null 2>&1; then
info "⏹️ Stopping agent …" info "⏹️ Stopping agent …"
@@ -145,13 +158,13 @@ wait_until_stopped() {
# ── Start service ───────────────────────────────────────────────────────────── # ── Start service ─────────────────────────────────────────────────────────────
start_service() { start_service() {
case "$OS" in case "$OS" in
Linux) linux)
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
info "▶ Starting service …" info "▶ Starting service …"
systemctl --user start skald-circle.service systemctl --user start skald-circle.service
fi fi
;; ;;
Darwin) darwin)
if command -v launchctl >/dev/null 2>&1; then if command -v launchctl >/dev/null 2>&1; then
info "▶ Starting agent …" info "▶ Starting agent …"
launchctl load "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true launchctl load "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
@@ -160,6 +173,34 @@ start_service() {
esac esac
} }
# ── systemd user lingering ────────────────────────────────────────────────────
# Same helper the installers run, repeated here so an install predating it gets
# healed by an ordinary update: a `systemctl --user` unit lives under the
# per-user manager, which systemd stops when the user's last session ends —
# so without lingering the server dies at logout and never starts at boot.
# Idempotent, and a failure is only ever a warning: the update itself is fine.
ensure_linger() {
[ "$OS" = "linux" ] || return 0
local target="${USER:-$(id -un)}"
command -v loginctl >/dev/null 2>&1 || return 0
case "$(loginctl show-user "$target" --property=Linger 2>/dev/null || true)" in
*=yes) return 0 ;;
esac
if loginctl enable-linger "$target" 2>/dev/null \
|| sudo -n loginctl enable-linger "$target" 2>/dev/null \
|| { [ "$IS_INTERACTIVE" = true ] && sudo loginctl enable-linger "$target"; }; then
info "✔ Lingering enabled — the server now survives logout and starts at boot"
else
warn "Lingering is not enabled for ${target}."
echo " The server stops when your last session ends. Run this once:"
echo " sudo loginctl enable-linger ${target}"
fi
}
# ── Cleanup + safety net ────────────────────────────────────────────────────── # ── Cleanup + safety net ──────────────────────────────────────────────────────
# Runs on every exit. Removes temp files and, if the update died after the # Runs on every exit. Removes temp files and, if the update died after the
# service was stopped but before it came back up, makes a best-effort restart so # service was stopped but before it came back up, makes a best-effort restart so
@@ -279,6 +320,7 @@ main() {
fi fi
# ── Restart ──────────────────────────────────────────────────────────────── # ── Restart ────────────────────────────────────────────────────────────────
ensure_linger
start_service start_service
STARTED=1 STARTED=1