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.
167 lines
6.7 KiB
Bash
Executable File
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 ==="
|