paperclip/scripts/service-onboard-smoke.sh

101 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Prove that `onboard --install-service` on a released artifact leaves a
# working background service. The Docker onboard smoke can never cover this
# leg: containers have no service manager, so a release whose service install
# crash-loops on a missing shim (v2026.824.0) still passes every golden-path
# check. This script runs the published npm artifact on a real systemd user
# session and fails unless the installed service itself ends up serving
# /api/health.
#
# Requirements: a Linux host with a user systemd session. In CI that means
# `loginctl enable-linger` plus XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS
# pointing at /run/user/<uid>; see the smoke_service job in
# .github/workflows/release-smoke.yml.
PAPERCLIPAI_VERSION="${PAPERCLIPAI_VERSION:-latest}"
DATA_DIR="${DATA_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/paperclip-service-smoke.XXXXXX")}"
ONBOARD_TIMEOUT_SECONDS="${ONBOARD_TIMEOUT_SECONDS:-600}"
SMOKE_READY_TIMEOUT_SECONDS="${SMOKE_READY_TIMEOUT_SECONDS:-420}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:3100/api/health}"
SERVICE_NAME="paperclipai.service"
SHIM_PATH="${PAPERCLIP_SHIM_PATH:-$HOME/.local/bin/paperclipai}"
# Cleanup defaults to on so a local run does not leave a service behind; CI
# disables it so the diagnostics step can still inspect the unit.
SMOKE_CLEANUP="${SMOKE_CLEANUP:-true}"
SMOKE_FORCE="${SMOKE_FORCE:-false}"
fail() {
echo "Service smoke failed: $*" >&2
exit 1
}
diagnostics() {
echo "--- systemctl --user status $SERVICE_NAME ---" >&2
systemctl --user --no-pager status "$SERVICE_NAME" >&2 || true
echo "--- journalctl --user -u $SERVICE_NAME (last 100 lines) ---" >&2
journalctl --user -u "$SERVICE_NAME" --no-pager -n 100 >&2 || true
}
cleanup() {
if [[ "$SMOKE_CLEANUP" == "true" ]]; then
if [[ -x "$SHIM_PATH" ]]; then
"$SHIM_PATH" service uninstall >/dev/null 2>&1 || true
fi
systemctl --user stop "$SERVICE_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT INT TERM
command -v systemctl >/dev/null 2>&1 || fail "systemctl is not available on this host"
systemctl --user show-environment >/dev/null 2>&1 \
|| fail "no user systemd session; enable lingering and export XDG_RUNTIME_DIR first"
# Refuse to smoke over a host that already has a managed install: the
# assertions below would prove nothing, and cleanup would tear down a real
# service.
if [[ "$SMOKE_FORCE" != "true" ]]; then
if [[ -e "$SHIM_PATH" ]]; then
fail "$SHIM_PATH already exists; set SMOKE_FORCE=true to smoke over it"
fi
if systemctl --user cat "$SERVICE_NAME" >/dev/null 2>&1; then
fail "$SERVICE_NAME is already installed; set SMOKE_FORCE=true to smoke over it"
fi
fi
echo "==> Onboarding paperclipai@$PAPERCLIPAI_VERSION with --install-service"
echo " Data dir: $DATA_DIR"
if ! timeout "$ONBOARD_TIMEOUT_SECONDS" \
npx --yes "paperclipai@${PAPERCLIPAI_VERSION}" onboard --yes --install-service --data-dir "$DATA_DIR"; then
diagnostics
fail "onboard exited non-zero"
fi
echo "==> Verifying the managed shim"
if [[ ! -x "$SHIM_PATH" ]]; then
diagnostics
fail "no executable shim at $SHIM_PATH after onboarding"
fi
echo "==> Waiting for $SERVICE_NAME to serve $HEALTH_URL"
for ((i = 1; i <= SMOKE_READY_TIMEOUT_SECONDS; i += 1)); do
state="$(systemctl --user is-active "$SERVICE_NAME" 2>/dev/null || true)"
if [[ "$state" == "failed" ]]; then
diagnostics
fail "$SERVICE_NAME entered the failed state"
fi
if curl -fsS "$HEALTH_URL" >/dev/null 2>&1; then
if [[ "$state" != "active" ]]; then
diagnostics
fail "$HEALTH_URL answers but $SERVICE_NAME is '$state' - something other than the service is serving"
fi
echo "==> Service smoke passed: $SERVICE_NAME is active and serving $HEALTH_URL"
exit 0
fi
sleep 1
done
diagnostics
fail "$HEALTH_URL not ready after ${SMOKE_READY_TIMEOUT_SECONDS}s (unit state: $(systemctl --user is-active "$SERVICE_NAME" 2>/dev/null || echo unknown))"