Release 0.2.0 #4
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user