Files
oikos/scripts/deploy-plugins.sh
dtoro e12954323c
Some checks are pending
ci / build-test (push) Waiting to run
ci / docker-build (push) Waiting to run
feat: structured __renderer envelopes for MCP tools
All 19 oikos tools now return JSON envelopes with __renderer hints
(terminal, service_status, ping, path_report, etc.) carrying structured
data alongside the model-facing prose in a `message` field — agent
behavior is unchanged while the UI renders native cards.

Fixes: ping_service SQL (json||text precedence), timestamptz scan,
run:{...} action prefix parsing, get_execution_status target slug join.

Also: deploy.sh step 5.5 restarts dsh web after API deploy; watchdog
LaunchAgent detects API recovery and kickstarts dsh web for stale MCP
sessions.
2026-08-17 16:49:56 +02:00

167 lines
6.7 KiB
Bash
Executable File

#!/bin/sh
# Oikos-plugins deploy script — triggered by Gitea webhook on push to dtoro/oikos-plugins.
# Runs on mac-mini via launchd unit network.hubris.oikos-deploy-webhook (route: /deploy-plugins).
#
# Deployment vehicle is the in-tree clone at $DSH_DIR/packages/oikos: the web
# profile symlinks its packages, and their @deepseek-ai peer deps resolve
# through the harness workspace root node_modules. UI and node halves ship as
# committed lib/ artifacts, so no build step runs here. On failure the script
# restores the previous SHAs and notifies via the oikos API (OIKOS_API_TOKEN)
# and Matrix (MATRIX_WEBHOOK_URL) when configured.
set -e
DSH_DIR="${DSH_DIR:-$HOME/Projects/deepseek-harness}"
PLUGIN_DIR="${PLUGIN_DIR:-$DSH_DIR/packages/oikos}"
PROFILE_DIR="${PROFILE_DIR:-$HOME/.dsh/profiles/web}"
PORT="${PORT:-3080}"
LOCKDIR="${LOCKDIR:-/tmp/oikos-plugins-deploy.lock}"
DSH_BRANCH="${DSH_BRANCH:-master}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:$PORT/}"
RETRIES=${RETRIES:-90}
ROLLBACK_RETRIES=${ROLLBACK_RETRIES:-30}
SLEEP=${SLEEP:-2}
AGENT_LABEL="network.hubris.dsh-web"
UID_N=$(id -u)
notify_deploy_failure() {
reason="$1"
echo "NOTIFY: deploy failed — $reason"
if [ -n "${OIKOS_API_TOKEN:-}" ]; then
curl -sf -X POST "http://localhost:8090/api/v1/events" \
-H "Authorization: Bearer $OIKOS_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"type\":\"deploy.failed\",\"severity\":\"critical\",\"source\":\"webhook\",\"data\":{\"repo\":\"oikos-plugins\",\"reason\":\"$reason\"}}" \
>/dev/null 2>&1 || true
fi
if [ -n "${MATRIX_WEBHOOK_URL:-}" ]; then
curl -sf -X POST "$MATRIX_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"m.text\",\"body\":\"🚨 oikos-plugins deploy failed: $reason\"}" \
>/dev/null 2>&1 || true
fi
}
# Serialize deploys. mkdir is atomic on POSIX (macOS lacks flock); the
# stale-pid check recovers a lock left by a SIGKILLed or rebooted deploy.
if ! mkdir "$LOCKDIR" 2>/dev/null; then
oldpid=$(cat "$LOCKDIR/pid" 2>/dev/null || echo "")
if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then
echo "deploy already in progress (pid $oldpid) — exiting"
exit 0
fi
echo "removing stale deploy lock (pid ${oldpid:-?} not running)"
rm -rf "$LOCKDIR"
mkdir "$LOCKDIR"
fi
echo $$ > "$LOCKDIR/pid"
trap 'rc=$?; rm -rf "$LOCKDIR" 2>/dev/null || true; if [ "$_ok" != "1" ] && [ "$_notified" != "1" ]; then notify_deploy_failure "deploy aborted (exit $rc)"; fi' EXIT
_ok=0
_notified=0
# Any HTTP response counts as healthy: this probes liveness (is the port
# serving), not a specific route.
wait_healthy() {
tries="$1"
i=1
while [ "$i" -le "$tries" ]; do
if curl -s -o /dev/null --max-time 2 "$HEALTH_URL"; then
echo "healthy after $((i * SLEEP))s"
return 0
fi
sleep "$SLEEP"
i=$((i + 1))
done
return 1
}
restart_dsh() {
if ! launchctl print "gui/$UID_N/$AGENT_LABEL" >/dev/null 2>&1; then
launchctl bootstrap "gui/$UID_N" "$HOME/Library/LaunchAgents/$AGENT_LABEL.plist"
sleep 1
fi
launchctl kickstart -k "gui/$UID_N/$AGENT_LABEL"
}
echo "=== oikos-plugins deploy started ==="
[ -d "$PLUGIN_DIR/.git" ] || {
echo "ERROR: $PLUGIN_DIR is not a git clone of dtoro/oikos-plugins — refusing"
exit 1
}
PLUGIN_OLD=$(git -C "$PLUGIN_DIR" rev-parse HEAD 2>/dev/null || echo "")
DSH_OLD=$(git -C "$DSH_DIR" rev-parse HEAD 2>/dev/null || echo "")
echo "pre-deploy: plugins=${PLUGIN_OLD:-none} dsh=${DSH_OLD:-none}"
rollback() {
_notified=1
echo "=== rolling back ==="
if [ -n "$PLUGIN_OLD" ] && [ "$(git -C "$PLUGIN_DIR" rev-parse HEAD)" != "$PLUGIN_OLD" ]; then
git -C "$PLUGIN_DIR" reset --hard "$PLUGIN_OLD"
fi
if [ -n "$DSH_OLD" ] && [ "$(git -C "$DSH_DIR" rev-parse HEAD)" != "$DSH_OLD" ]; then
git -C "$DSH_DIR" reset --hard "$DSH_OLD"
pnpm -C "$DSH_DIR" install --no-frozen-lockfile >/dev/null 2>&1 || true
git -C "$DSH_DIR" checkout -- pnpm-lock.yaml 2>/dev/null || true
fi
restart_dsh
if wait_healthy "$ROLLBACK_RETRIES"; then
notify_deploy_failure "deploy failed; rolled back to plugins@${PLUGIN_OLD:-?} dsh@${DSH_OLD:-?}"
else
notify_deploy_failure "deploy failed and rollback unhealthy — dsh web DOWN on port $PORT"
fi
}
# 1. Plugins: advance the in-tree clone to the pushed state. Uncommitted local
# edits mean someone is developing here — never destroy them; deploy the dirty
# tree as-is and say so.
git -C "$PLUGIN_DIR" fetch origin master
if git -C "$PLUGIN_DIR" diff --quiet && git -C "$PLUGIN_DIR" diff --cached --quiet; then
git -C "$PLUGIN_DIR" reset --hard origin/master
echo "plugins at $(git -C "$PLUGIN_DIR" rev-parse --short HEAD)"
else
echo "WARN: $PLUGIN_DIR has uncommitted changes — deploying the dirty tree as-is"
fi
# 2. Harness: advance by fast-forward only (never discards local commits;
# refuses when diverged). The untracked packages/oikos member is invisible to
# origin/master's lockfile, so install non-frozen and restore the lockfile
# afterwards: node_modules keeps the resolution, the tree stays clean.
git -C "$DSH_DIR" fetch origin "$DSH_BRANCH"
git -C "$DSH_DIR" pull --ff-only origin "$DSH_BRANCH"
pnpm -C "$DSH_DIR" install --no-frozen-lockfile
git -C "$DSH_DIR" checkout -- pnpm-lock.yaml
echo "dsh at $(git -C "$DSH_DIR" rev-parse --short HEAD)"
# 2b. Rebuild the frontend dist: the web-app bundle serves the gitignored
# apps/web dist through workspace exports, so a harness advance without a
# rebuild keeps serving the previous UI.
pnpm -C "$DSH_DIR" --filter @deepseek-ai/dsh-web-frontend run build >/dev/null
echo "frontend dist rebuilt"
# 3. Symlink packages into dsh profile
for pkg in ui mcp-scope session-summary bundle evals; do
name=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$PLUGIN_DIR/$pkg/package.json')).name)")
ln -sf "$PLUGIN_DIR/$pkg" "$PROFILE_DIR/node_modules/$name"
echo "linked $name"
done
# 4. Patch migration: the oikos overlay used to live only in /tmp (wiped on
# reboot). If the profile patch layer is still empty and the /tmp copy exists,
# move it into the profile so launchd boots need no --patch flag.
if [ -f /tmp/oikos-mcp-patch.yml ] && ! grep -q 'id:' "$PROFILE_DIR/cordis.patch.yml" 2>/dev/null; then
cp /tmp/oikos-mcp-patch.yml "$PROFILE_DIR/cordis.patch.yml"
echo "migrated oikos patch into $PROFILE_DIR/cordis.patch.yml"
fi
# 5. Restart under launchd and health-check
restart_dsh
if ! wait_healthy "$RETRIES"; then
echo "ERROR: health check failed after $((RETRIES * SLEEP))s"
rollback
exit 1
fi
_ok=1
echo "=== oikos-plugins deploy complete ==="