220 lines
8.6 KiB
Bash
Executable File
220 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
TARGET_HOST="${1:?target host is required}"
|
|
BINARY_PATH="${2:?binary path is required}"
|
|
EXPECTED_COMMIT="${3:?expected short commit is required}"
|
|
|
|
DEPLOY_USER="${DEPLOY_USER:-ubuntu}"
|
|
REMOTE_TMP="/tmp/xworkmate-bridge-${EXPECTED_COMMIT}"
|
|
REMOTE_BINARY="${REMOTE_BINARY:-/home/${DEPLOY_USER}/.local/bin/xworkmate-go-core}"
|
|
REMOTE_WORKING_DIR="${REMOTE_WORKING_DIR:-/opt/cloud-neutral/xworkmate-bridge}"
|
|
BRIDGE_CONFIG_PATH="${BRIDGE_CONFIG_PATH:-/opt/cloud-neutral/xworkmate-bridge/config.yaml}"
|
|
SERVICE_NAME="${SERVICE_NAME:-xworkmate-bridge.service}"
|
|
SERVICE_LISTEN_ADDR="${SERVICE_LISTEN_ADDR:-127.0.0.1:8787}"
|
|
USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-/home/${DEPLOY_USER}/.config/systemd/user}"
|
|
SYSTEM_SERVICE_NAME="${SYSTEM_SERVICE_NAME:-xworkmate-bridge.service}"
|
|
MIGRATE_SYSTEM_SERVICE="${MIGRATE_SYSTEM_SERVICE:-true}"
|
|
SYSTEM_MIGRATION_USER="${SYSTEM_MIGRATION_USER:-root}"
|
|
STALE_DROPIN="${STALE_DROPIN:-/etc/systemd/system/xworkmate-bridge.service.d/10-hotfix-openclaw-artifacts.conf}"
|
|
DEPLOY_NATIVE_SKIP_PROC_CHECK="${DEPLOY_NATIVE_SKIP_PROC_CHECK:-false}"
|
|
SYSTEMD_SYSTEM_DIR="${SYSTEMD_SYSTEM_DIR:-/etc/systemd/system}"
|
|
SYSTEM_SERVICE_UNIT_PATH="${SYSTEM_SERVICE_UNIT_PATH:-${SYSTEMD_SYSTEM_DIR}/${SYSTEM_SERVICE_NAME}}"
|
|
|
|
if [[ ! "${TARGET_HOST}" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
|
echo "invalid target host: ${TARGET_HOST}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! "${DEPLOY_USER}" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
|
echo "invalid deploy user: ${DEPLOY_USER}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! "${EXPECTED_COMMIT}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
|
echo "invalid expected commit: ${EXPECTED_COMMIT}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "${BINARY_PATH}" ]]; then
|
|
echo "native bridge binary not found: ${BINARY_PATH}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
escape_systemd_env() {
|
|
python3 - "$1" <<'PY'
|
|
import sys
|
|
value = sys.argv[1]
|
|
print(value.replace("\\", "\\\\").replace('"', '\\"'))
|
|
PY
|
|
}
|
|
|
|
resolve_token_from_unit() {
|
|
local unit_path="$1"
|
|
local key="$2"
|
|
sed -n "s/^Environment=\"${key}=\\(.*\\)\"$/\\1/p" "${unit_path}" 2>/dev/null | head -n 1
|
|
}
|
|
|
|
REMOTE_SYSTEM_SERVICE_UNIT_CONTENT="$(ssh -o BatchMode=yes "${SYSTEM_MIGRATION_USER}@${TARGET_HOST}" "cat '${SYSTEM_SERVICE_UNIT_PATH}' 2>/dev/null || true" 2>/dev/null || true)"
|
|
|
|
if [[ -z "${AI_WORKSPACE_AUTH_TOKEN:-}" && -n "${REMOTE_SYSTEM_SERVICE_UNIT_CONTENT}" ]]; then
|
|
AI_WORKSPACE_AUTH_TOKEN="$(printf '%s\n' "${REMOTE_SYSTEM_SERVICE_UNIT_CONTENT}" | resolve_token_from_unit /dev/stdin "AI_WORKSPACE_AUTH_TOKEN")"
|
|
if [[ -n "${AI_WORKSPACE_AUTH_TOKEN}" ]]; then
|
|
echo "recovered AI_WORKSPACE_AUTH_TOKEN from ${SYSTEM_SERVICE_UNIT_PATH} on ${TARGET_HOST}" >&2
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "${AI_WORKSPACE_AUTH_TOKEN:-}" && -z "${BRIDGE_AUTH_TOKEN:-}" && -n "${REMOTE_SYSTEM_SERVICE_UNIT_CONTENT}" ]]; then
|
|
BRIDGE_AUTH_TOKEN="$(printf '%s\n' "${REMOTE_SYSTEM_SERVICE_UNIT_CONTENT}" | resolve_token_from_unit /dev/stdin "BRIDGE_AUTH_TOKEN")"
|
|
if [[ -n "${BRIDGE_AUTH_TOKEN}" ]]; then
|
|
echo "recovered BRIDGE_AUTH_TOKEN from ${SYSTEM_SERVICE_UNIT_PATH} on ${TARGET_HOST}" >&2
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "${AI_WORKSPACE_AUTH_TOKEN:-}" && -z "${BRIDGE_AUTH_TOKEN:-}" ]]; then
|
|
echo "::error::AI_WORKSPACE_AUTH_TOKEN is required: pass it via env, -e ai_workspace_auth_token=, or keep AI_WORKSPACE_AUTH_TOKEN/BRIDGE_AUTH_TOKEN in the existing system service unit at ${SYSTEM_SERVICE_UNIT_PATH}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${BRIDGE_REVIEW_AUTH_TOKEN:-}" && -n "${REMOTE_SYSTEM_SERVICE_UNIT_CONTENT}" ]]; then
|
|
BRIDGE_REVIEW_AUTH_TOKEN="$(printf '%s\n' "${REMOTE_SYSTEM_SERVICE_UNIT_CONTENT}" | resolve_token_from_unit /dev/stdin "BRIDGE_REVIEW_AUTH_TOKEN")"
|
|
fi
|
|
|
|
AUTH_TOKEN_LINE=""
|
|
if [[ -n "${AI_WORKSPACE_AUTH_TOKEN:-}" ]]; then
|
|
AUTH_TOKEN_LINE="Environment=\"AI_WORKSPACE_AUTH_TOKEN=$(escape_systemd_env "${AI_WORKSPACE_AUTH_TOKEN}")\""
|
|
else
|
|
AUTH_TOKEN_LINE="Environment=\"BRIDGE_AUTH_TOKEN=$(escape_systemd_env "${BRIDGE_AUTH_TOKEN}")\""
|
|
fi
|
|
|
|
REVIEW_TOKEN_LINE=""
|
|
if [[ -n "${BRIDGE_REVIEW_AUTH_TOKEN:-}" ]]; then
|
|
REVIEW_TOKEN_LINE="Environment=\"BRIDGE_REVIEW_AUTH_TOKEN=$(escape_systemd_env "${BRIDGE_REVIEW_AUTH_TOKEN}")\""
|
|
fi
|
|
UNIT_ENV_LINES_B64="$(printf '%s\n%s\n' "${AUTH_TOKEN_LINE}" "${REVIEW_TOKEN_LINE}" | base64 | tr -d '\n')"
|
|
|
|
chmod +x "${BINARY_PATH}"
|
|
|
|
if [[ "${MIGRATE_SYSTEM_SERVICE}" == "true" ]]; then
|
|
ssh "${SYSTEM_MIGRATION_USER}@${TARGET_HOST}" \
|
|
"SYSTEM_SERVICE_NAME='${SYSTEM_SERVICE_NAME}' STALE_DROPIN='${STALE_DROPIN}' bash -s" <<'REMOTE_ROOT'
|
|
set -euo pipefail
|
|
|
|
if systemctl list-unit-files "${SYSTEM_SERVICE_NAME}" >/dev/null 2>&1; then
|
|
systemctl disable --now "${SYSTEM_SERVICE_NAME}" >/dev/null 2>&1 || systemctl stop "${SYSTEM_SERVICE_NAME}" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
rm -f "${STALE_DROPIN}"
|
|
rmdir --ignore-fail-on-non-empty "$(dirname "${STALE_DROPIN}")" 2>/dev/null || true
|
|
systemctl daemon-reload
|
|
REMOTE_ROOT
|
|
fi
|
|
|
|
scp -q "${BINARY_PATH}" "${DEPLOY_USER}@${TARGET_HOST}:${REMOTE_TMP}"
|
|
|
|
ssh "${DEPLOY_USER}@${TARGET_HOST}" \
|
|
"EXPECTED_COMMIT='${EXPECTED_COMMIT}' REMOTE_TMP='${REMOTE_TMP}' REMOTE_BINARY='${REMOTE_BINARY}' REMOTE_WORKING_DIR='${REMOTE_WORKING_DIR}' BRIDGE_CONFIG_PATH='${BRIDGE_CONFIG_PATH}' SERVICE_NAME='${SERVICE_NAME}' SERVICE_LISTEN_ADDR='${SERVICE_LISTEN_ADDR}' USER_SYSTEMD_DIR='${USER_SYSTEMD_DIR}' SYSTEM_SERVICE_NAME='${SYSTEM_SERVICE_NAME}' SYSTEM_SERVICE_UNIT_PATH='${SYSTEM_SERVICE_UNIT_PATH}' UNIT_ENV_LINES_B64='${UNIT_ENV_LINES_B64}' DEPLOY_NATIVE_SKIP_PROC_CHECK='${DEPLOY_NATIVE_SKIP_PROC_CHECK}' bash -s" <<'REMOTE'
|
|
set -euo pipefail
|
|
|
|
mkdir -p "$(dirname "${REMOTE_BINARY}")" "${USER_SYSTEMD_DIR}"
|
|
install -m 0755 "${REMOTE_TMP}" "${REMOTE_BINARY}"
|
|
rm -f "${REMOTE_TMP}"
|
|
|
|
version_json="$("${REMOTE_BINARY}" version)"
|
|
actual_commit="$(VERSION_JSON="${version_json}" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
payload = json.loads(os.environ["VERSION_JSON"])
|
|
print(str(payload.get("commit", "")))
|
|
PY
|
|
)"
|
|
if [[ "${actual_commit}" != "${EXPECTED_COMMIT}" ]]; then
|
|
echo "deployed binary commit mismatch: expected ${EXPECTED_COMMIT}, got ${actual_commit}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
unit_env_lines="$(printf '%s' "${UNIT_ENV_LINES_B64}" | base64 -d | sed '/^$/d')"
|
|
existing_env="$(
|
|
{
|
|
systemctl --user show -p Environment --value "${SERVICE_NAME}" 2>/dev/null || true
|
|
systemctl show -p Environment --value "${SYSTEM_SERVICE_NAME}" 2>/dev/null || true
|
|
if [[ -f "${SYSTEM_SERVICE_UNIT_PATH}" ]]; then
|
|
sed -n 's/^Environment="\(AI_WORKSPACE_AUTH_TOKEN=[^"]*\|BRIDGE_AUTH_TOKEN=[^"]*\|BRIDGE_REVIEW_AUTH_TOKEN=[^"]*\)"$/\1/p' "${SYSTEM_SERVICE_UNIT_PATH}"
|
|
fi
|
|
} | sed '/^$/d' | head -n 1
|
|
)"
|
|
unit_env_lines="$(
|
|
UNIT_ENV_LINES="${unit_env_lines}" EXISTING_ENV="${existing_env}" python3 - <<'PY'
|
|
import os
|
|
import shlex
|
|
|
|
lines = [line for line in os.environ.get("UNIT_ENV_LINES", "").splitlines() if line.strip()]
|
|
present = set()
|
|
for line in lines:
|
|
prefix = 'Environment="'
|
|
if line.startswith(prefix):
|
|
key = line[len(prefix):].split("=", 1)[0]
|
|
present.add(key)
|
|
|
|
for item in shlex.split(os.environ.get("EXISTING_ENV", "")):
|
|
key, sep, value = item.partition("=")
|
|
if sep and key in {"AI_WORKSPACE_AUTH_TOKEN", "BRIDGE_AUTH_TOKEN", "BRIDGE_REVIEW_AUTH_TOKEN"} and key not in present:
|
|
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
lines.append(f'Environment="{key}={escaped}"')
|
|
present.add(key)
|
|
|
|
print("\n".join(lines))
|
|
PY
|
|
)"
|
|
|
|
cat >"${USER_SYSTEMD_DIR}/${SERVICE_NAME}" <<UNIT
|
|
[Unit]
|
|
Description=XWorkmate bridge control plane
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=${REMOTE_WORKING_DIR}
|
|
Environment="HOME=/home/${USER}"
|
|
Environment="TERM=xterm-256color"
|
|
Environment="BRIDGE_CONFIG_PATH=${BRIDGE_CONFIG_PATH}"
|
|
${unit_env_lines}
|
|
ExecStart=${REMOTE_BINARY} serve --listen ${SERVICE_LISTEN_ADDR}
|
|
Restart=always
|
|
RestartSec=2
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
UNIT
|
|
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable "${SERVICE_NAME}" >/dev/null
|
|
|
|
if systemctl is-active --quiet "${SYSTEM_SERVICE_NAME}" 2>/dev/null; then
|
|
echo "${SYSTEM_SERVICE_NAME} is still active as a system service; disable it before starting the user service" >&2
|
|
exit 1
|
|
fi
|
|
|
|
systemctl --user restart "${SERVICE_NAME}"
|
|
deadline=$((SECONDS + 20))
|
|
actual_exe=""
|
|
pid=""
|
|
while (( SECONDS < deadline )); do
|
|
pid="$(systemctl --user show -p MainPID --value "${SERVICE_NAME}")"
|
|
if [[ -n "${pid}" && "${pid}" != "0" ]] && [[ "${DEPLOY_NATIVE_SKIP_PROC_CHECK}" == "true" || -e "/proc/${pid}/exe" ]]; then
|
|
actual_exe="$(readlink -f "/proc/${pid}/exe" 2>/dev/null || true)"
|
|
if [[ "${actual_exe}" == "${REMOTE_BINARY}" ]]; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [[ -z "${pid}" || "${pid}" == "0" ]]; then
|
|
echo "${SERVICE_NAME} did not start as a user service" >&2
|
|
exit 1
|
|
fi
|
|
echo "${SERVICE_NAME} is not running ${REMOTE_BINARY}; pid=${pid}; actual=${actual_exe:-unknown}" >&2
|
|
exit 1
|
|
REMOTE
|