feat: prepare deep search production runtime
Memind CI / Test, build, and release guards (pull_request) Successful in 4m4s
Memind CI / Test, build, and release guards (pull_request) Successful in 4m4s
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publishes only the independently versioned Deep Search runtime to 103.
|
||||
# Portal and memindadm are not modified by this workflow.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HOST="${STUDIO_HOST:-58.38.22.103}"
|
||||
REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}"
|
||||
RUNTIME_BASE="${DEEP_SEARCH_RUNTIME_BASE:-${REMOTE_ROOT}/deep-search-runtime}"
|
||||
INCOMING="${REMOTE_ROOT}/incoming/deep-search-runtime"
|
||||
RELEASE_ID="$(date +%Y%m%d-%H%M%S)-$(git -C "${ROOT}" rev-parse --short HEAD)"
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/deep-search-runtime-release.XXXXXX")"
|
||||
DRY_RUN=0
|
||||
AUTO_YES=0
|
||||
|
||||
cleanup() {
|
||||
rm -rf "${TMP_DIR}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--yes) AUTO_YES=1 ;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
Usage: bash scripts/release-deep-search-runtime-prod.sh [--dry-run] [--yes]
|
||||
|
||||
Required production gates:
|
||||
- current branch is main
|
||||
- HEAD exactly matches origin/main
|
||||
- worktree is clean
|
||||
- CONFIRMED_CI_SHA equals the full current HEAD
|
||||
|
||||
This installs only Deep Search on 103. It does not release Portal or memindadm.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
git -C "${ROOT}" fetch origin main --quiet
|
||||
branch="$(git -C "${ROOT}" branch --show-current)"
|
||||
head_sha="$(git -C "${ROOT}" rev-parse HEAD)"
|
||||
origin_sha="$(git -C "${ROOT}" rev-parse origin/main)"
|
||||
[[ "${branch}" == "main" ]] || {
|
||||
echo "Deep Search production release must run from main" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${head_sha}" == "${origin_sha}" ]] || {
|
||||
echo "main must exactly match origin/main" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -z "$(git -C "${ROOT}" status --porcelain=v1 --untracked-files=all)" ]] || {
|
||||
echo "worktree must be clean" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${CONFIRMED_CI_SHA:-}" == "${head_sha}" ]] || {
|
||||
echo "CONFIRMED_CI_SHA must equal the verified main HEAD: ${head_sha}" >&2
|
||||
exit 1
|
||||
}
|
||||
ALLOW_MAIN_RELEASE=1 bash "${ROOT}/scripts/check-release-ready.sh"
|
||||
|
||||
(
|
||||
cd "${ROOT}"
|
||||
npm run test:deep-search
|
||||
node --test capabilities.test.mjs scripts/deep-search-runtime-package.test.mjs
|
||||
npm run build:deep-search-runtime
|
||||
)
|
||||
|
||||
RUNTIME_DIR="${ROOT}/.runtime/deep-search"
|
||||
for file in \
|
||||
deep-search-server.mjs \
|
||||
scripts/run-deep-search-prod.sh \
|
||||
scripts/install-deep-search-runtime-prod.sh \
|
||||
RUNBOOK.txt; do
|
||||
[[ -f "${RUNTIME_DIR}/${file}" ]] || {
|
||||
echo "missing runtime artifact: ${file}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
MANIFEST="${RUNTIME_DIR}/release-manifest.txt"
|
||||
{
|
||||
echo "release_id=${RELEASE_ID}"
|
||||
echo "git_head=${head_sha}"
|
||||
echo "git_branch=${branch}"
|
||||
echo "created_at=$(date '+%Y-%m-%d %H:%M:%S %z')"
|
||||
echo "artifact=.runtime/deep-search"
|
||||
echo "persistent_env=${RUNTIME_BASE}/shared/.env"
|
||||
echo "persistent_db=${RUNTIME_BASE}/data/research.sqlite"
|
||||
} > "${MANIFEST}"
|
||||
|
||||
ARCHIVE="${TMP_DIR}/deep-search-runtime-${RELEASE_ID}.tar.gz"
|
||||
SHA_FILE="${ARCHIVE}.sha256"
|
||||
tar -C "${RUNTIME_DIR}" -czf "${ARCHIVE}" .
|
||||
(cd "${TMP_DIR}" && shasum -a 256 "$(basename "${ARCHIVE}")" > "$(basename "${SHA_FILE}")")
|
||||
|
||||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||||
shasum -a 256 -c "${SHA_FILE}"
|
||||
echo "Deep Search dry-run artifact verified: ${ARCHIVE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${AUTO_YES}" -ne 1 ]]; then
|
||||
echo "Target: ${HOST}:${RUNTIME_BASE}"
|
||||
echo "Release: ${RELEASE_ID}"
|
||||
read -r -p "Install standalone Deep Search on 103? [y/N] " confirm </dev/tty
|
||||
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
|
||||
fi
|
||||
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "mkdir -p '${INCOMING}' '${RUNTIME_BASE}/releases' '${RUNTIME_BASE}/backups'"
|
||||
scp -q "${ARCHIVE}" "${SHA_FILE}" "${HOST}:${INCOMING}/"
|
||||
|
||||
ssh -o BatchMode=yes "${HOST}" \
|
||||
"RELEASE_ID='${RELEASE_ID}' RUNTIME_BASE='${RUNTIME_BASE}' INCOMING='${INCOMING}' /bin/bash" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
archive="${INCOMING}/deep-search-runtime-${RELEASE_ID}.tar.gz"
|
||||
sha_file="${archive}.sha256"
|
||||
release_dir="${RUNTIME_BASE}/releases/${RELEASE_ID}"
|
||||
backup_dir="${RUNTIME_BASE}/backups/${RELEASE_ID}"
|
||||
current_link="${RUNTIME_BASE}/current"
|
||||
plist="${HOME}/Library/LaunchAgents/cn.tkmind.memind-deep-search.plist"
|
||||
label="cn.tkmind.memind-deep-search"
|
||||
gui="gui/$(id -u)"
|
||||
previous_target="$(readlink "${current_link}" 2>/dev/null || true)"
|
||||
database_path="${RUNTIME_BASE}/data/research.sqlite"
|
||||
|
||||
mkdir -p "${release_dir}" "${backup_dir}"
|
||||
cp "${plist}" "${backup_dir}/launchagent.plist" 2>/dev/null || true
|
||||
if [[ -f "${database_path}" ]]; then
|
||||
sqlite3 "${database_path}" ".timeout 10000" \
|
||||
".backup '${backup_dir}/research.sqlite'" >/dev/null
|
||||
fi
|
||||
cd "${INCOMING}"
|
||||
shasum -a 256 -c "$(basename "${sha_file}")"
|
||||
tar -xzf "${archive}" -C "${release_dir}"
|
||||
|
||||
rollback() {
|
||||
if [[ -n "${previous_target}" && -d "${previous_target}" ]]; then
|
||||
ln -sfn "${previous_target}" "${current_link}"
|
||||
else
|
||||
rm -f "${current_link}"
|
||||
fi
|
||||
if [[ -f "${backup_dir}/launchagent.plist" ]]; then
|
||||
cp "${backup_dir}/launchagent.plist" "${plist}"
|
||||
launchctl bootout "${gui}/${label}" >/dev/null 2>&1 || true
|
||||
launchctl bootstrap "${gui}" "${plist}" >/dev/null 2>&1 || true
|
||||
launchctl kickstart -k "${gui}/${label}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap rollback ERR
|
||||
|
||||
DEEP_SEARCH_RUNTIME_BASE="${RUNTIME_BASE}" \
|
||||
DEEP_SEARCH_RELEASE_DIR="${release_dir}" \
|
||||
bash "${release_dir}/scripts/install-deep-search-runtime-prod.sh"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "${RUNTIME_BASE}/shared/.env"
|
||||
set +a
|
||||
|
||||
curl -fsS --max-time 5 "http://127.0.0.1:20100/health" >/dev/null
|
||||
curl -fsS --max-time 20 \
|
||||
-H "X-Secret-Key: ${TKMIND_DEEP_SEARCH_SECRET}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"OpenAI","limit":3}' \
|
||||
"http://127.0.0.1:20100/v1/search" \
|
||||
| grep -q '"results"'
|
||||
|
||||
docker_bin=""
|
||||
for candidate in /usr/local/bin/docker /opt/homebrew/bin/docker; do
|
||||
if [[ -x "${candidate}" ]]; then
|
||||
docker_bin="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "${docker_bin}" ]]; then
|
||||
goosed_container="$("${docker_bin}" ps --format '{{.Names}}' | grep -E '^goosed-prod-' | head -1)"
|
||||
if [[ -n "${goosed_container}" ]]; then
|
||||
"${docker_bin}" exec "${goosed_container}" sh -lc \
|
||||
'curl -fsS --max-time 5 http://host.docker.internal:20100/health >/dev/null'
|
||||
fi
|
||||
fi
|
||||
|
||||
trap - ERR
|
||||
printf '%s\n' "${RELEASE_ID}" > "${RUNTIME_BASE}/current-release.txt"
|
||||
REMOTE
|
||||
|
||||
echo "Deep Search installed and verified on 103: ${RELEASE_ID}"
|
||||
Reference in New Issue
Block a user