ci, install: update workflows, packaging scripts, and installer suite
Nightly Build / build (push) Successful in 6m43s
Nightly Build / build (push) Successful in 6m43s
Nightly/release workflows: matrix tweaks, artifact path fixes. Package-macos: streamline dmg build, codesigning improvements. Install scripts: rewrite install.sh and install-nightly.sh with robust error handling, add uninstall.sh, overhaul update.sh.
This commit is contained in:
@@ -52,7 +52,15 @@ jobs:
|
||||
- name: Deploy to builds.skaldagent.net
|
||||
run: |
|
||||
cd "${GITHUB_WORKSPACE:-.}"
|
||||
mkdir -p /var/www/builds.skaldagent.net/nightly
|
||||
cp dist/*.tar.gz /var/www/builds.skaldagent.net/nightly/
|
||||
DEST=/var/www/builds.skaldagent.net/nightly
|
||||
mkdir -p "$DEST"
|
||||
# Nightly reuses a fixed filename, so publish atomically: copy to a
|
||||
# temp name on the same filesystem, then rename over the target. A
|
||||
# concurrent download never sees a half-written tarball.
|
||||
for f in dist/*.tar.gz; do
|
||||
name="$(basename "$f")"
|
||||
cp "$f" "$DEST/.$name.tmp"
|
||||
mv -f "$DEST/.$name.tmp" "$DEST/$name"
|
||||
done
|
||||
echo "[nightly] Deployed:"
|
||||
ls -lh /var/www/builds.skaldagent.net/nightly/
|
||||
ls -lh "$DEST/"
|
||||
|
||||
@@ -85,11 +85,22 @@ jobs:
|
||||
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||
TARGET="/var/www/builds.skaldagent.net/releases/${VERSION}"
|
||||
mkdir -p "$TARGET"
|
||||
cp dist/*.tar.gz "$TARGET/"
|
||||
# Publish each tarball atomically (temp name + rename) so a client can
|
||||
# never fetch a half-written file.
|
||||
for f in dist/*.tar.gz; do
|
||||
name="$(basename "$f")"
|
||||
cp "$f" "$TARGET/.$name.tmp"
|
||||
mv -f "$TARGET/.$name.tmp" "$TARGET/$name"
|
||||
done
|
||||
echo "[release] Deployed $VERSION:"
|
||||
ls -lh "$TARGET/"
|
||||
|
||||
- name: Update latest version pointer
|
||||
run: |
|
||||
echo "${{ steps.extract-version.outputs.version }}" > /var/www/builds.skaldagent.net/releases/LATEST
|
||||
echo "[release] Updated releases/LATEST → ${{ steps.extract-version.outputs.version }}"
|
||||
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||
DEST=/var/www/builds.skaldagent.net/releases
|
||||
# Flip LATEST atomically — install.sh/update.sh read it to decide
|
||||
# whether to upgrade, so it must never be observed empty or partial.
|
||||
printf '%s\n' "$VERSION" > "$DEST/.LATEST.tmp"
|
||||
mv -f "$DEST/.LATEST.tmp" "$DEST/LATEST"
|
||||
echo "[release] Updated releases/LATEST → $VERSION"
|
||||
|
||||
+17
-10
@@ -90,19 +90,26 @@ fi
|
||||
echo "[package-macos] Uploading to ${REMOTE_HOST}..."
|
||||
|
||||
if [ "$MODE" = "release" ]; then
|
||||
# Create remote directory and copy tarball
|
||||
ssh "$REMOTE_HOST" "mkdir -p ${REMOTE_BASE}/releases/${VERSION}"
|
||||
scp dist/skald-circle-${VERSION}-darwin-arm64.tar.gz \
|
||||
"${REMOTE_HOST}:${REMOTE_BASE}/releases/${VERSION}/"
|
||||
TARBALL="skald-circle-${VERSION}-darwin-arm64.tar.gz"
|
||||
RDIR="${REMOTE_BASE}/releases/${VERSION}"
|
||||
ssh "$REMOTE_HOST" "mkdir -p ${RDIR}"
|
||||
|
||||
# Update LATEST pointer
|
||||
echo "$VERSION" | ssh "$REMOTE_HOST" "cat > ${REMOTE_BASE}/releases/LATEST"
|
||||
# Publish atomically: scp to a temp name, then rename over the target so a
|
||||
# concurrent download never sees a half-transferred tarball.
|
||||
scp "dist/${TARBALL}" "${REMOTE_HOST}:${RDIR}/.${TARBALL}.tmp"
|
||||
ssh "$REMOTE_HOST" "mv -f ${RDIR}/.${TARBALL}.tmp ${RDIR}/${TARBALL}"
|
||||
|
||||
# Flip LATEST atomically (write temp + rename) — clients read it to decide
|
||||
# whether to upgrade, so it must never be observed empty or partial.
|
||||
ssh "$REMOTE_HOST" "printf '%s\n' '${VERSION}' > ${REMOTE_BASE}/releases/.LATEST.tmp && mv -f ${REMOTE_BASE}/releases/.LATEST.tmp ${REMOTE_BASE}/releases/LATEST"
|
||||
echo "[package-macos] ✅ Release ${VERSION} deployed + LATEST updated."
|
||||
else
|
||||
# Nightly — copy into nightly/ directory
|
||||
ssh "$REMOTE_HOST" "mkdir -p ${REMOTE_BASE}/nightly"
|
||||
scp dist/skald-circle-nightly-darwin-arm64.tar.gz \
|
||||
"${REMOTE_HOST}:${REMOTE_BASE}/nightly/"
|
||||
# Nightly — atomic publish into nightly/ (fixed filename, reused each run).
|
||||
TARBALL="skald-circle-nightly-darwin-arm64.tar.gz"
|
||||
NDIR="${REMOTE_BASE}/nightly"
|
||||
ssh "$REMOTE_HOST" "mkdir -p ${NDIR}"
|
||||
scp "dist/${TARBALL}" "${REMOTE_HOST}:${NDIR}/.${TARBALL}.tmp"
|
||||
ssh "$REMOTE_HOST" "mv -f ${NDIR}/.${TARBALL}.tmp ${NDIR}/${TARBALL}"
|
||||
echo "[package-macos] ✅ Nightly deployed."
|
||||
fi
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# branch. Runs in the repo root after checkout.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/verify-version.sh \
|
||||
# ./ci/verify-version.sh \
|
||||
# --builds-dir /var/www/builds.skaldagent.net
|
||||
#
|
||||
# Exit codes:
|
||||
|
||||
@@ -84,6 +84,49 @@ prompt_yes_no() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Like prompt_yes_no but defaults to NO — for destructive confirmations.
|
||||
prompt_yes_no_default_no() {
|
||||
local msg="${1:-Continue? [y/N]}"
|
||||
local ans=""
|
||||
if [ "$IS_INTERACTIVE" = true ]; then
|
||||
printf "%s" "$msg" >&2
|
||||
read -r ans || ans=""
|
||||
elif (: </dev/tty) 2>/dev/null; then
|
||||
printf "%s" "$msg" >/dev/tty
|
||||
IFS= read -r ans </dev/tty || ans=""
|
||||
fi
|
||||
case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
|
||||
"y"|"yes") return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Stop a running instance (for reinstall over an existing install) ──────────
|
||||
# Stops the service and waits, bounded, for the process to exit — so the
|
||||
# extraction never overwrites a live binary (ETXTBSY on Linux).
|
||||
stop_existing_service() {
|
||||
case "$OS" in
|
||||
linux)
|
||||
command -v systemctl >/dev/null 2>&1 && \
|
||||
systemctl --user stop skald-circle.service 2>/dev/null || true
|
||||
;;
|
||||
darwin)
|
||||
command -v launchctl >/dev/null 2>&1 && \
|
||||
launchctl unload "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
local i=0
|
||||
while pgrep -f "${INSTALL_DIR}/bin/skald" >/dev/null 2>&1; do
|
||||
i=$((i + 1))
|
||||
[ "$i" -gt 20 ] && break
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
sleep 2
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Docker install helper ─────────────────────────────────────────────────────
|
||||
install_docker() {
|
||||
if [ "$OS" = "linux" ]; then
|
||||
@@ -237,6 +280,26 @@ ask_install_docker
|
||||
# ── Optional dependency check ─────────────────────────────────────────────────
|
||||
check_optional_deps
|
||||
|
||||
# ── Existing installation? ────────────────────────────────────────────────────
|
||||
# This installer targets a fresh install; the supported in-place upgrade path is
|
||||
# update.sh (keeps your data, restarts the service safely). If an install is
|
||||
# already here, point the user there and only reinstall over it on request.
|
||||
if [ -x "$INSTALL_DIR/bin/skald" ]; then
|
||||
echo ""
|
||||
warn "An existing installation was found at ${INSTALL_DIR}."
|
||||
echo " To upgrade in place (keeping your database and config), use:"
|
||||
echo " ${INSTALL_DIR}/update.sh"
|
||||
echo ""
|
||||
if prompt_yes_no_default_no " Reinstall over it instead? Data is kept, the service restarts. [y/N] "; then
|
||||
info "⏹️ Stopping the running instance before reinstalling …"
|
||||
stop_existing_service
|
||||
else
|
||||
info "Aborted — run ${INSTALL_DIR}/update.sh to upgrade."
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Download & extract ────────────────────────────────────────────────────────
|
||||
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
@@ -354,6 +417,9 @@ elif [ "$OS" = "darwin" ]; then
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
# Idempotent: unload any previously-loaded agent first, so a re-install
|
||||
# reloads the freshly written plist instead of failing on "already loaded".
|
||||
launchctl unload "$PLIST" 2>/dev/null || true
|
||||
launchctl load "$PLIST"
|
||||
|
||||
info "✔ Agent installed and started"
|
||||
|
||||
+66
@@ -87,6 +87,49 @@ prompt_yes_no() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Like prompt_yes_no but defaults to NO — for destructive confirmations.
|
||||
prompt_yes_no_default_no() {
|
||||
local msg="${1:-Continue? [y/N]}"
|
||||
local ans=""
|
||||
if [ "$IS_INTERACTIVE" = true ]; then
|
||||
printf "%s" "$msg" >&2
|
||||
read -r ans || ans=""
|
||||
elif (: </dev/tty) 2>/dev/null; then
|
||||
printf "%s" "$msg" >/dev/tty
|
||||
IFS= read -r ans </dev/tty || ans=""
|
||||
fi
|
||||
case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
|
||||
"y"|"yes") return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Stop a running instance (for reinstall over an existing install) ──────────
|
||||
# Stops the service and waits, bounded, for the process to exit — so the
|
||||
# extraction never overwrites a live binary (ETXTBSY on Linux).
|
||||
stop_existing_service() {
|
||||
case "$OS" in
|
||||
linux)
|
||||
command -v systemctl >/dev/null 2>&1 && \
|
||||
systemctl --user stop skald-circle.service 2>/dev/null || true
|
||||
;;
|
||||
darwin)
|
||||
command -v launchctl >/dev/null 2>&1 && \
|
||||
launchctl unload "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
local i=0
|
||||
while pgrep -f "${INSTALL_DIR}/bin/skald" >/dev/null 2>&1; do
|
||||
i=$((i + 1))
|
||||
[ "$i" -gt 20 ] && break
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
sleep 2
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Docker install helper ─────────────────────────────────────────────────────
|
||||
install_docker() {
|
||||
if [ "$OS" = "linux" ]; then
|
||||
@@ -242,6 +285,26 @@ ask_install_docker
|
||||
# ── Optional dependency check ─────────────────────────────────────────────────
|
||||
check_optional_deps
|
||||
|
||||
# ── Existing installation? ────────────────────────────────────────────────────
|
||||
# This installer targets a fresh install; the supported in-place upgrade path is
|
||||
# update.sh (keeps your data, restarts the service safely). If an install is
|
||||
# already here, point the user there and only reinstall over it on request.
|
||||
if [ -x "$INSTALL_DIR/bin/skald" ]; then
|
||||
echo ""
|
||||
warn "An existing installation was found at ${INSTALL_DIR}."
|
||||
echo " To upgrade in place (keeping your database and config), use:"
|
||||
echo " ${INSTALL_DIR}/update.sh"
|
||||
echo ""
|
||||
if prompt_yes_no_default_no " Reinstall over it instead? Data is kept, the service restarts. [y/N] "; then
|
||||
info "⏹️ Stopping the running instance before reinstalling …"
|
||||
stop_existing_service
|
||||
else
|
||||
info "Aborted — run ${INSTALL_DIR}/update.sh to upgrade."
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Download & extract ────────────────────────────────────────────────────────
|
||||
info "↓ Downloading Skald Circle ${VERSION} …"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
@@ -359,6 +422,9 @@ elif [ "$OS" = "darwin" ]; then
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
# Idempotent: unload any previously-loaded agent first, so a re-install
|
||||
# reloads the freshly written plist instead of failing on "already loaded".
|
||||
launchctl unload "$PLIST" 2>/dev/null || true
|
||||
launchctl load "$PLIST"
|
||||
|
||||
info "✔ Agent installed and started"
|
||||
|
||||
+39
-2
@@ -46,11 +46,25 @@ echo " This will permanently delete Skald Circle and all its data:"
|
||||
echo " ${INSTALL_DIR}"
|
||||
echo ""
|
||||
|
||||
if [ -t 0 ]; then
|
||||
printf "%s " "Are you sure? Type 'yes' to continue: "
|
||||
# Confirm before a destructive delete. Read from the terminal even when stdin is
|
||||
# piped (curl | sh); if there's no terminal at all, require SKALD_YES=1 so an
|
||||
# install is never wiped with no confirmation.
|
||||
if [ "${SKALD_YES:-}" = "1" ]; then
|
||||
:
|
||||
elif [ -t 0 ]; then
|
||||
printf "%s " "Are you sure? Type 'yes' to continue:"
|
||||
read -r CONFIRM
|
||||
[ "$CONFIRM" = "yes" ] || { echo "Aborted."; exit 0; }
|
||||
echo ""
|
||||
elif (: </dev/tty) 2>/dev/null; then
|
||||
printf "%s " "Are you sure? Type 'yes' to continue:" >/dev/tty
|
||||
IFS= read -r CONFIRM </dev/tty
|
||||
[ "$CONFIRM" = "yes" ] || { echo "Aborted."; exit 0; }
|
||||
echo ""
|
||||
else
|
||||
err "No terminal available for confirmation."
|
||||
err "Re-run with SKALD_YES=1 to uninstall non-interactively."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Stop & remove daemon ──────────────────────────────────────────────────────
|
||||
@@ -94,6 +108,29 @@ case "$OS" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Remove Docker sandboxes ───────────────────────────────────────────────────
|
||||
# Each user has a container named skald-{userid} that bind-mounts files under the
|
||||
# install dir. The daemon is stopped above, so the server won't recreate them
|
||||
# mid-cleanup; removing them here also frees the (sometimes root-owned) mount
|
||||
# files that would otherwise force the sudo fallback on the rm below.
|
||||
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
CONTAINERS="$(docker ps -aq --filter 'name=skald-' 2>/dev/null || true)"
|
||||
if [ -n "$CONTAINERS" ]; then
|
||||
info "🐳 Removing Skald Docker containers …"
|
||||
# shellcheck disable=SC2086 # word-splitting is intentional (multiple IDs)
|
||||
docker rm -f $CONTAINERS >/dev/null 2>&1 || true
|
||||
fi
|
||||
IMAGES="$(docker images -q skald-runtime 2>/dev/null || true)"
|
||||
if [ -n "$IMAGES" ]; then
|
||||
info "🐳 Removing Skald runtime image …"
|
||||
# shellcheck disable=SC2086
|
||||
docker rmi -f $IMAGES >/dev/null 2>&1 || true
|
||||
fi
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
warn "Docker daemon not reachable — skipping container cleanup."
|
||||
warn "Remove leftovers later with: docker rm -f \$(docker ps -aq --filter name=skald-)"
|
||||
fi
|
||||
|
||||
# ── Remove installation directory ─────────────────────────────────────────────
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
info "🗑️ Removing installation directory …"
|
||||
|
||||
@@ -8,7 +8,20 @@
|
||||
# whether to pull from the release or nightly channel. On release, checks
|
||||
# the remote LATEST version first and skips the download if already current.
|
||||
#
|
||||
# Stops the service before extracting, then restarts it afterwards.
|
||||
# Robustness notes (why the flow looks the way it does):
|
||||
# * The tarball is downloaded and validated in a *staging* directory BEFORE
|
||||
# the running service is touched — a broken download never takes the app
|
||||
# down.
|
||||
# * The service is stopped and we WAIT until the process is actually gone
|
||||
# before extracting: overwriting a live binary in place fails with ETXTBSY
|
||||
# (Linux) or on a running Mach-O (macOS), which would abort the update with
|
||||
# the service left down.
|
||||
# * The whole flow runs inside main(), invoked on the very last line, so the
|
||||
# shell has parsed the entire script into memory before `tar` overwrites
|
||||
# update.sh with its own new copy (the tarball ships this script). Without
|
||||
# this, the shell would read garbage for the tail and never restart.
|
||||
# * A trap restarts the service if the update fails after the stop, so a
|
||||
# failed update never leaves the box down.
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -28,7 +41,7 @@ if [ ! -f "$CHANNEL_FILE" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHANNEL="$(cat "$CHANNEL_FILE" | tr -d '[:space:]')"
|
||||
CHANNEL="$(tr -d '[:space:]' < "$CHANNEL_FILE")"
|
||||
|
||||
# ── Colours (if terminal) ─────────────────────────────────────────────────────
|
||||
if [ -t 1 ]; then
|
||||
@@ -74,10 +87,97 @@ esac
|
||||
|
||||
command -v curl >/dev/null 2>&1 || { err "curl is required but not installed."; exit 1; }
|
||||
|
||||
# ── Determine download URL ────────────────────────────────────────────────────
|
||||
BASE_URL="https://builds.skaldagent.net"
|
||||
|
||||
case "$CHANNEL" in
|
||||
# ── Global state (referenced by the EXIT trap) ────────────────────────────────
|
||||
TMP_TARBALL=""
|
||||
STAGING=""
|
||||
STOPPED=0 # set once the service has been stopped
|
||||
STARTED=0 # set once it has been (re)started
|
||||
|
||||
# ── Stop service ──────────────────────────────────────────────────────────────
|
||||
stop_service() {
|
||||
case "$OS" in
|
||||
Linux)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl --user is-active skald-circle.service >/dev/null 2>&1; then
|
||||
info "⏹️ Stopping service …"
|
||||
systemctl --user stop skald-circle.service
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
if command -v launchctl >/dev/null 2>&1; then
|
||||
if launchctl list com.skald.circle >/dev/null 2>&1; then
|
||||
info "⏹️ Stopping agent …"
|
||||
launchctl unload "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Wait until the server process is actually gone ────────────────────────────
|
||||
# systemctl stop is synchronous, but launchctl unload kills the process group
|
||||
# asynchronously — so we poll for the real binary before overwriting it.
|
||||
wait_until_stopped() {
|
||||
# Match the server binary by path prefix. Deliberately unanchored: the
|
||||
# server runs with no args today, but we'd rather over-match (a harmless
|
||||
# extra wait) than miss a still-running process and race the extraction.
|
||||
# `skald-setup` only runs at first-run setup, never during an update.
|
||||
pat="${INSTALL_DIR}/bin/skald"
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
i=0
|
||||
while pgrep -f "$pat" >/dev/null 2>&1; do
|
||||
i=$((i + 1))
|
||||
if [ "$i" -gt 20 ]; then
|
||||
warn "Server still running after ~20s; proceeding anyway."
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
# No pgrep: give the service manager a moment to tear the process down.
|
||||
sleep 3
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Start service ─────────────────────────────────────────────────────────────
|
||||
start_service() {
|
||||
case "$OS" in
|
||||
Linux)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
info "▶ Starting service …"
|
||||
systemctl --user start skald-circle.service
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
if command -v launchctl >/dev/null 2>&1; then
|
||||
info "▶ Starting agent …"
|
||||
launchctl load "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Cleanup + safety net ──────────────────────────────────────────────────────
|
||||
# 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
|
||||
# the box is never left down.
|
||||
cleanup() {
|
||||
[ -n "${TMP_TARBALL:-}" ] && rm -f "$TMP_TARBALL" 2>/dev/null || true
|
||||
[ -n "${STAGING:-}" ] && rm -rf "$STAGING" 2>/dev/null || true
|
||||
if [ "$STOPPED" = "1" ] && [ "$STARTED" != "1" ]; then
|
||||
warn "Update failed after the service was stopped — attempting to restart it …"
|
||||
start_service || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
# ── Resolve download URL + version (may exit early if already current) ─────
|
||||
case "$CHANNEL" in
|
||||
release)
|
||||
LATEST="$(curl -fsSL "${BASE_URL}/releases/LATEST" | head -1 | tr -d '[:space:]')"
|
||||
if [ -z "$LATEST" ]; then
|
||||
@@ -87,7 +187,7 @@ case "$CHANNEL" in
|
||||
|
||||
VERSION_FILE="${INSTALL_DIR}/.release-version"
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
CURRENT="$(cat "$VERSION_FILE" | tr -d '[:space:]')"
|
||||
CURRENT="$(tr -d '[:space:]' < "$VERSION_FILE")"
|
||||
if [ "$CURRENT" = "$LATEST" ]; then
|
||||
info "✔ Already up to date (${CURRENT})."
|
||||
exit 0
|
||||
@@ -113,114 +213,95 @@ case "$CHANNEL" in
|
||||
err "Expected 'release' or 'nightly'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Stop service ──────────────────────────────────────────────────────────────
|
||||
stop_service() {
|
||||
case "$OS" in
|
||||
Linux)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl --user is-active skald-circle.service >/dev/null 2>&1; then
|
||||
info "⏹️ Stopping service …"
|
||||
systemctl --user stop skald-circle.service
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
if command -v launchctl >/dev/null 2>&1; then
|
||||
if launchctl list com.skald.circle >/dev/null 2>&1; then
|
||||
info "⏹️ Stopping agent …"
|
||||
launchctl unload "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Start service ─────────────────────────────────────────────────────────────
|
||||
start_service() {
|
||||
case "$OS" in
|
||||
Linux)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
info "▶ Starting service …"
|
||||
systemctl --user start skald-circle.service
|
||||
banner "╔══════════════════════════════════════════╗"
|
||||
banner "║ Skald Circle — Updater (${DISPLAY_VERSION}) ║"
|
||||
banner "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " Channel : ${CHANNEL}"
|
||||
echo " Platform : ${OS}/${ARCH}"
|
||||
echo " Install : ${INSTALL_DIR}"
|
||||
echo ""
|
||||
|
||||
# ── Download + validate BEFORE touching the running service ────────────────
|
||||
# A broken download or a bad archive must never take the app down: we only
|
||||
# stop the service once we hold a known-good tarball.
|
||||
TMP_TARBALL="$(mktemp -t skald-update.XXXXXX.tar.gz)"
|
||||
|
||||
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
||||
curl -fsSL -o "$TMP_TARBALL" "$TARBALL_URL"
|
||||
|
||||
info "🔎 Verifying archive …"
|
||||
STAGING="$(mktemp -d -t skald-update-staging.XXXXXX)"
|
||||
tar xzf "$TMP_TARBALL" -C "$STAGING" --strip-components=1
|
||||
if [ ! -x "$STAGING/bin/skald" ]; then
|
||||
err "Downloaded archive is invalid — skald binary not found."
|
||||
err "The running server was left untouched."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
if command -v launchctl >/dev/null 2>&1; then
|
||||
info "▶ Starting agent …"
|
||||
launchctl load "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
banner "╔══════════════════════════════════════════╗"
|
||||
banner "║ Skald Circle — Updater (${DISPLAY_VERSION}) ║"
|
||||
banner "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " Channel : ${CHANNEL}"
|
||||
echo " Platform : ${OS}/${ARCH}"
|
||||
echo " Install : ${INSTALL_DIR}"
|
||||
echo ""
|
||||
# ── Stop the service and wait for the process to actually exit ─────────────
|
||||
stop_service
|
||||
STOPPED=1
|
||||
wait_until_stopped
|
||||
|
||||
stop_service
|
||||
# ── Install (binary is no longer busy) ─────────────────────────────────────
|
||||
info "📦 Installing update …"
|
||||
tar xzf "$TMP_TARBALL" -C "$INSTALL_DIR" --strip-components=1
|
||||
|
||||
# Download & extract
|
||||
TMP_TARBALL="$(mktemp -t skald-update.XXXXXX.tar.gz)"
|
||||
trap 'rm -f "$TMP_TARBALL"' EXIT
|
||||
|
||||
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
||||
curl -fsSL -o "$TMP_TARBALL" "$TARBALL_URL"
|
||||
|
||||
info "📦 Extracting …"
|
||||
tar xzf "$TMP_TARBALL" -C "$INSTALL_DIR" --strip-components=1
|
||||
|
||||
if [ ! -x "$INSTALL_DIR/bin/skald" ]; then
|
||||
if [ ! -x "$INSTALL_DIR/bin/skald" ]; then
|
||||
err "Extraction failed — skald binary not found."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update version file for release channel
|
||||
if [ "$CHANNEL" = "release" ]; then
|
||||
# Update version file for release channel
|
||||
if [ "$CHANNEL" = "release" ]; then
|
||||
echo "$VERSION" > "$INSTALL_DIR/.release-version"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Rebuild Python venv (best effort — new deps may have appeared)
|
||||
info "🔧 Rebuilding Python virtual environment …"
|
||||
VENV_DIR="${INSTALL_DIR}/.venv"
|
||||
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"
|
||||
# ── Rebuild Python venv (best effort — new deps may have appeared) ─────────
|
||||
info "🔧 Rebuilding Python virtual environment …"
|
||||
VENV_DIR="${INSTALL_DIR}/.venv"
|
||||
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"
|
||||
|
||||
rm -rf "$VENV_DIR"
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
rm -rf "$VENV_DIR"
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
uv venv --seed "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \
|
||||
&& info "✔ Python venv ready (uv)" \
|
||||
|| warn "Python venv setup failed — Python MCP servers will be unavailable."
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
python3 -m venv "$VENV_DIR" && "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" \
|
||||
&& info "✔ Python venv ready (pip)" \
|
||||
|| warn "Python venv setup failed — Python MCP servers will be unavailable."
|
||||
else
|
||||
else
|
||||
warn "python3 not found — Python MCP servers will be unavailable."
|
||||
fi
|
||||
fi
|
||||
|
||||
start_service
|
||||
# ── Restart ────────────────────────────────────────────────────────────────
|
||||
start_service
|
||||
STARTED=1
|
||||
|
||||
# ── Hint about optional deps ──────────────────────────────────────────────────
|
||||
if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then
|
||||
# ── Hint about optional deps ───────────────────────────────────────────────
|
||||
if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then
|
||||
info "💡 Optional GPU/ML dependencies available:"
|
||||
info " ${INSTALL_DIR}/requirements-optional.txt"
|
||||
echo " Install them manually if you use the Orpheus TTS plugin:"
|
||||
echo " cd ${INSTALL_DIR} && .venv/bin/pip install -r requirements-optional.txt"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
info "✅ Skald Circle updated to ${DISPLAY_VERSION}!"
|
||||
echo ""
|
||||
echo " Status: $( [ "$OS" = "linux" ] && echo "systemctl --user status skald-circle" || echo "launchctl list com.skald.circle" )"
|
||||
echo " Logs: $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )"
|
||||
echo " Update: ${INSTALL_DIR}/update.sh"
|
||||
echo ""
|
||||
# ── Done ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
info "✅ Skald Circle updated to ${DISPLAY_VERSION}!"
|
||||
echo ""
|
||||
echo " Status: $( [ "$OS" = "linux" ] && echo "systemctl --user status skald-circle" || echo "launchctl list com.skald.circle" )"
|
||||
echo " Logs: $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )"
|
||||
echo " Update: ${INSTALL_DIR}/update.sh"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Invoked on the very last line: by the time `tar` overwrites this file on disk
|
||||
# (the tarball ships update.sh), the shell has already parsed the whole script,
|
||||
# so the restart tail always runs. Do not add executable code below this line.
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user