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
|
- name: Deploy to builds.skaldagent.net
|
||||||
run: |
|
run: |
|
||||||
cd "${GITHUB_WORKSPACE:-.}"
|
cd "${GITHUB_WORKSPACE:-.}"
|
||||||
mkdir -p /var/www/builds.skaldagent.net/nightly
|
DEST=/var/www/builds.skaldagent.net/nightly
|
||||||
cp dist/*.tar.gz /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:"
|
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 }}"
|
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||||
TARGET="/var/www/builds.skaldagent.net/releases/${VERSION}"
|
TARGET="/var/www/builds.skaldagent.net/releases/${VERSION}"
|
||||||
mkdir -p "$TARGET"
|
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:"
|
echo "[release] Deployed $VERSION:"
|
||||||
ls -lh "$TARGET/"
|
ls -lh "$TARGET/"
|
||||||
|
|
||||||
- name: Update latest version pointer
|
- name: Update latest version pointer
|
||||||
run: |
|
run: |
|
||||||
echo "${{ steps.extract-version.outputs.version }}" > /var/www/builds.skaldagent.net/releases/LATEST
|
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||||
echo "[release] Updated releases/LATEST → ${{ 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}..."
|
echo "[package-macos] Uploading to ${REMOTE_HOST}..."
|
||||||
|
|
||||||
if [ "$MODE" = "release" ]; then
|
if [ "$MODE" = "release" ]; then
|
||||||
# Create remote directory and copy tarball
|
TARBALL="skald-circle-${VERSION}-darwin-arm64.tar.gz"
|
||||||
ssh "$REMOTE_HOST" "mkdir -p ${REMOTE_BASE}/releases/${VERSION}"
|
RDIR="${REMOTE_BASE}/releases/${VERSION}"
|
||||||
scp dist/skald-circle-${VERSION}-darwin-arm64.tar.gz \
|
ssh "$REMOTE_HOST" "mkdir -p ${RDIR}"
|
||||||
"${REMOTE_HOST}:${REMOTE_BASE}/releases/${VERSION}/"
|
|
||||||
|
|
||||||
# Update LATEST pointer
|
# Publish atomically: scp to a temp name, then rename over the target so a
|
||||||
echo "$VERSION" | ssh "$REMOTE_HOST" "cat > ${REMOTE_BASE}/releases/LATEST"
|
# 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."
|
echo "[package-macos] ✅ Release ${VERSION} deployed + LATEST updated."
|
||||||
else
|
else
|
||||||
# Nightly — copy into nightly/ directory
|
# Nightly — atomic publish into nightly/ (fixed filename, reused each run).
|
||||||
ssh "$REMOTE_HOST" "mkdir -p ${REMOTE_BASE}/nightly"
|
TARBALL="skald-circle-nightly-darwin-arm64.tar.gz"
|
||||||
scp dist/skald-circle-nightly-darwin-arm64.tar.gz \
|
NDIR="${REMOTE_BASE}/nightly"
|
||||||
"${REMOTE_HOST}:${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."
|
echo "[package-macos] ✅ Nightly deployed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# branch. Runs in the repo root after checkout.
|
# branch. Runs in the repo root after checkout.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/verify-version.sh \
|
# ./ci/verify-version.sh \
|
||||||
# --builds-dir /var/www/builds.skaldagent.net
|
# --builds-dir /var/www/builds.skaldagent.net
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
|
|||||||
@@ -84,6 +84,49 @@ prompt_yes_no() {
|
|||||||
esac
|
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 ─────────────────────────────────────────────────────
|
# ── Docker install helper ─────────────────────────────────────────────────────
|
||||||
install_docker() {
|
install_docker() {
|
||||||
if [ "$OS" = "linux" ]; then
|
if [ "$OS" = "linux" ]; then
|
||||||
@@ -237,6 +280,26 @@ ask_install_docker
|
|||||||
# ── Optional dependency check ─────────────────────────────────────────────────
|
# ── Optional dependency check ─────────────────────────────────────────────────
|
||||||
check_optional_deps
|
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 ────────────────────────────────────────────────────────
|
# ── Download & extract ────────────────────────────────────────────────────────
|
||||||
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
@@ -354,6 +417,9 @@ elif [ "$OS" = "darwin" ]; then
|
|||||||
</plist>
|
</plist>
|
||||||
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"
|
launchctl load "$PLIST"
|
||||||
|
|
||||||
info "✔ Agent installed and started"
|
info "✔ Agent installed and started"
|
||||||
|
|||||||
+66
@@ -87,6 +87,49 @@ prompt_yes_no() {
|
|||||||
esac
|
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 ─────────────────────────────────────────────────────
|
# ── Docker install helper ─────────────────────────────────────────────────────
|
||||||
install_docker() {
|
install_docker() {
|
||||||
if [ "$OS" = "linux" ]; then
|
if [ "$OS" = "linux" ]; then
|
||||||
@@ -242,6 +285,26 @@ ask_install_docker
|
|||||||
# ── Optional dependency check ─────────────────────────────────────────────────
|
# ── Optional dependency check ─────────────────────────────────────────────────
|
||||||
check_optional_deps
|
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 ────────────────────────────────────────────────────────
|
# ── Download & extract ────────────────────────────────────────────────────────
|
||||||
info "↓ Downloading Skald Circle ${VERSION} …"
|
info "↓ Downloading Skald Circle ${VERSION} …"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
@@ -359,6 +422,9 @@ elif [ "$OS" = "darwin" ]; then
|
|||||||
</plist>
|
</plist>
|
||||||
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"
|
launchctl load "$PLIST"
|
||||||
|
|
||||||
info "✔ Agent installed and started"
|
info "✔ Agent installed and started"
|
||||||
|
|||||||
+38
-1
@@ -46,11 +46,25 @@ echo " This will permanently delete Skald Circle and all its data:"
|
|||||||
echo " ${INSTALL_DIR}"
|
echo " ${INSTALL_DIR}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ -t 0 ]; then
|
# 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:"
|
printf "%s " "Are you sure? Type 'yes' to continue:"
|
||||||
read -r CONFIRM
|
read -r CONFIRM
|
||||||
[ "$CONFIRM" = "yes" ] || { echo "Aborted."; exit 0; }
|
[ "$CONFIRM" = "yes" ] || { echo "Aborted."; exit 0; }
|
||||||
echo ""
|
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
|
fi
|
||||||
|
|
||||||
# ── Stop & remove daemon ──────────────────────────────────────────────────────
|
# ── Stop & remove daemon ──────────────────────────────────────────────────────
|
||||||
@@ -94,6 +108,29 @@ case "$OS" in
|
|||||||
;;
|
;;
|
||||||
esac
|
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 ─────────────────────────────────────────────
|
# ── Remove installation directory ─────────────────────────────────────────────
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
info "🗑️ Removing installation directory …"
|
info "🗑️ Removing installation directory …"
|
||||||
|
|||||||
@@ -8,7 +8,20 @@
|
|||||||
# whether to pull from the release or nightly channel. On release, checks
|
# whether to pull from the release or nightly channel. On release, checks
|
||||||
# the remote LATEST version first and skips the download if already current.
|
# 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
|
set -eu
|
||||||
|
|
||||||
@@ -28,7 +41,7 @@ if [ ! -f "$CHANNEL_FILE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CHANNEL="$(cat "$CHANNEL_FILE" | tr -d '[:space:]')"
|
CHANNEL="$(tr -d '[:space:]' < "$CHANNEL_FILE")"
|
||||||
|
|
||||||
# ── Colours (if terminal) ─────────────────────────────────────────────────────
|
# ── Colours (if terminal) ─────────────────────────────────────────────────────
|
||||||
if [ -t 1 ]; then
|
if [ -t 1 ]; then
|
||||||
@@ -74,9 +87,96 @@ esac
|
|||||||
|
|
||||||
command -v curl >/dev/null 2>&1 || { err "curl is required but not installed."; exit 1; }
|
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"
|
BASE_URL="https://builds.skaldagent.net"
|
||||||
|
|
||||||
|
# ── 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
|
case "$CHANNEL" in
|
||||||
release)
|
release)
|
||||||
LATEST="$(curl -fsSL "${BASE_URL}/releases/LATEST" | head -1 | tr -d '[:space:]')"
|
LATEST="$(curl -fsSL "${BASE_URL}/releases/LATEST" | head -1 | tr -d '[:space:]')"
|
||||||
@@ -87,7 +187,7 @@ case "$CHANNEL" in
|
|||||||
|
|
||||||
VERSION_FILE="${INSTALL_DIR}/.release-version"
|
VERSION_FILE="${INSTALL_DIR}/.release-version"
|
||||||
if [ -f "$VERSION_FILE" ]; then
|
if [ -f "$VERSION_FILE" ]; then
|
||||||
CURRENT="$(cat "$VERSION_FILE" | tr -d '[:space:]')"
|
CURRENT="$(tr -d '[:space:]' < "$VERSION_FILE")"
|
||||||
if [ "$CURRENT" = "$LATEST" ]; then
|
if [ "$CURRENT" = "$LATEST" ]; then
|
||||||
info "✔ Already up to date (${CURRENT})."
|
info "✔ Already up to date (${CURRENT})."
|
||||||
exit 0
|
exit 0
|
||||||
@@ -115,47 +215,6 @@ case "$CHANNEL" in
|
|||||||
;;
|
;;
|
||||||
esac
|
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
|
|
||||||
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 "╔══════════════════════════════════════════╗"
|
||||||
banner "║ Skald Circle — Updater (${DISPLAY_VERSION}) ║"
|
banner "║ Skald Circle — Updater (${DISPLAY_VERSION}) ║"
|
||||||
banner "╚══════════════════════════════════════════╝"
|
banner "╚══════════════════════════════════════════╝"
|
||||||
@@ -165,16 +224,30 @@ echo " Platform : ${OS}/${ARCH}"
|
|||||||
echo " Install : ${INSTALL_DIR}"
|
echo " Install : ${INSTALL_DIR}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
stop_service
|
# ── Download + validate BEFORE touching the running service ────────────────
|
||||||
|
# A broken download or a bad archive must never take the app down: we only
|
||||||
# Download & extract
|
# stop the service once we hold a known-good tarball.
|
||||||
TMP_TARBALL="$(mktemp -t skald-update.XXXXXX.tar.gz)"
|
TMP_TARBALL="$(mktemp -t skald-update.XXXXXX.tar.gz)"
|
||||||
trap 'rm -f "$TMP_TARBALL"' EXIT
|
|
||||||
|
|
||||||
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
||||||
curl -fsSL -o "$TMP_TARBALL" "$TARBALL_URL"
|
curl -fsSL -o "$TMP_TARBALL" "$TARBALL_URL"
|
||||||
|
|
||||||
info "📦 Extracting …"
|
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
|
||||||
|
|
||||||
|
# ── Stop the service and wait for the process to actually exit ─────────────
|
||||||
|
stop_service
|
||||||
|
STOPPED=1
|
||||||
|
wait_until_stopped
|
||||||
|
|
||||||
|
# ── Install (binary is no longer busy) ─────────────────────────────────────
|
||||||
|
info "📦 Installing update …"
|
||||||
tar xzf "$TMP_TARBALL" -C "$INSTALL_DIR" --strip-components=1
|
tar xzf "$TMP_TARBALL" -C "$INSTALL_DIR" --strip-components=1
|
||||||
|
|
||||||
if [ ! -x "$INSTALL_DIR/bin/skald" ]; then
|
if [ ! -x "$INSTALL_DIR/bin/skald" ]; then
|
||||||
@@ -187,7 +260,7 @@ if [ "$CHANNEL" = "release" ]; then
|
|||||||
echo "$VERSION" > "$INSTALL_DIR/.release-version"
|
echo "$VERSION" > "$INSTALL_DIR/.release-version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rebuild Python venv (best effort — new deps may have appeared)
|
# ── Rebuild Python venv (best effort — new deps may have appeared) ─────────
|
||||||
info "🔧 Rebuilding Python virtual environment …"
|
info "🔧 Rebuilding Python virtual environment …"
|
||||||
VENV_DIR="${INSTALL_DIR}/.venv"
|
VENV_DIR="${INSTALL_DIR}/.venv"
|
||||||
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"
|
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"
|
||||||
@@ -205,9 +278,11 @@ else
|
|||||||
warn "python3 not found — Python MCP servers will be unavailable."
|
warn "python3 not found — Python MCP servers will be unavailable."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Restart ────────────────────────────────────────────────────────────────
|
||||||
start_service
|
start_service
|
||||||
|
STARTED=1
|
||||||
|
|
||||||
# ── Hint about optional deps ──────────────────────────────────────────────────
|
# ── Hint about optional deps ───────────────────────────────────────────────
|
||||||
if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then
|
if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then
|
||||||
info "💡 Optional GPU/ML dependencies available:"
|
info "💡 Optional GPU/ML dependencies available:"
|
||||||
info " ${INSTALL_DIR}/requirements-optional.txt"
|
info " ${INSTALL_DIR}/requirements-optional.txt"
|
||||||
@@ -216,7 +291,7 @@ if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
# ── Done ───────────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
info "✅ Skald Circle updated to ${DISPLAY_VERSION}!"
|
info "✅ Skald Circle updated to ${DISPLAY_VERSION}!"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -224,3 +299,9 @@ echo " Status: $( [ "$OS" = "linux" ] && echo "systemctl --user status skald-c
|
|||||||
echo " Logs: $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )"
|
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 " Update: ${INSTALL_DIR}/update.sh"
|
||||||
echo ""
|
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