From 59cfbcde6ce5aef592adac6acb94798d10064391 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 17 Jul 2026 11:51:43 +0800 Subject: [PATCH] fix: remount goosed after portal runtime swap --- release-goosed-remount-guard.test.mjs | 25 ++++++++ scripts/release-portal-runtime-prod.sh | 80 ++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 release-goosed-remount-guard.test.mjs diff --git a/release-goosed-remount-guard.test.mjs b/release-goosed-remount-guard.test.mjs new file mode 100644 index 0000000..d1ad776 --- /dev/null +++ b/release-goosed-remount-guard.test.mjs @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; + +const releaseScript = fs.readFileSync( + new URL('./scripts/release-portal-runtime-prod.sh', import.meta.url), + 'utf8', +); + +test('103 release remounts goosed after swapping the live directory', () => { + assert.match(releaseScript, /remount_goosed_after_live_swap\(\)/); + assert.match(releaseScript, /docker-compose\.prod\.yml/); + assert.match(releaseScript, /up -d --force-recreate/); + assert.match(releaseScript, /MindSpace\/\.goosed-remount-/); + assert.match(releaseScript, /containers\[@\].*!= 9/); + assert.match(releaseScript, /seq 18006 18014/); + assert.match(releaseScript, /\/opt\/portal\/mindspace-sandbox-mcp\.mjs/); + + const swapIndex = releaseScript.indexOf('mv "${RUNTIME_DIR}" "${APP_DIR}"'); + const remountIndex = releaseScript.indexOf('remount_goosed_after_live_swap', swapIndex); + const portalStartIndex = releaseScript.indexOf('say "启动新的 Portal runtime"'); + assert.ok(swapIndex >= 0, 'live directory swap must exist'); + assert.ok(remountIndex > swapIndex, 'goosed remount must happen after the live swap'); + assert.ok(portalStartIndex > remountIndex, 'Portal must start only after goosed remount succeeds'); +}); diff --git a/scripts/release-portal-runtime-prod.sh b/scripts/release-portal-runtime-prod.sh index 3421b46..e830787 100755 --- a/scripts/release-portal-runtime-prod.sh +++ b/scripts/release-portal-runtime-prod.sh @@ -378,6 +378,84 @@ OLD_LIVE_DIR="${ARCHIVE_DIR}/Memind-source-before-${RELEASE_ID}" BUNDLE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.tar.gz" MANIFEST="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.manifest.txt" SHA_FILE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.sha256" + +remount_goosed_after_live_swap() { + local docker_bin="/opt/homebrew/bin/docker" + local compose_dir="/Users/john/Project/goosed-prod" + local compose_file="${compose_dir}/docker-compose.prod.yml" + local marker_rel="MindSpace/.goosed-remount-${RELEASE_ID}" + local marker_host="${APP_DIR}/${marker_rel}" + local marker_value="${RELEASE_ID}-$(date +%s)" + local ready=0 + + if [[ ! -x "${docker_bin}" || ! -f "${compose_file}" ]]; then + echo "goosed remount failed: docker or ${compose_file} is unavailable" >&2 + return 1 + fi + + mkdir -p "${APP_DIR}/MindSpace" + printf '%s\n' "${marker_value}" > "${marker_host}" + + say "重建 goosed 容器以刷新 Portal/MindSpace bind mount" + if ! ( + cd "${compose_dir}" + "${docker_bin}" compose -f "${compose_file}" up -d --force-recreate + ); then + rm -f "${marker_host}" + return 1 + fi + + for _ in $(seq 1 60); do + local healthy=1 + local containers=() + while IFS= read -r container; do + [[ -n "${container}" ]] && containers+=("${container}") + done < <("${docker_bin}" ps \ + --filter 'label=com.docker.compose.project=goosed-prod' \ + --format '{{.Names}}') + + if ((${#containers[@]} != 9)); then + healthy=0 + fi + + local container + for container in "${containers[@]}"; do + local state + state="$("${docker_bin}" inspect "${container}" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' 2>/dev/null || true)" + if [[ "${state}" != "healthy" ]]; then + healthy=0 + continue + fi + local observed + observed="$("${docker_bin}" exec "${container}" sh -lc "cat '${APP_DIR}/${marker_rel}'" 2>/dev/null || true)" + if [[ "${observed}" != "${marker_value}" ]]; then + healthy=0 + fi + if ! "${docker_bin}" exec "${container}" sh -lc 'test -f /opt/portal/mindspace-sandbox-mcp.mjs' >/dev/null 2>&1; then + healthy=0 + fi + done + + local port + for port in $(seq 18006 18014); do + if [[ "$(curl -skS -m 5 "https://127.0.0.1:${port}/status" 2>/dev/null || true)" != "ok" ]]; then + healthy=0 + fi + done + + if [[ "${healthy}" -eq 1 ]]; then + ready=1 + break + fi + sleep 2 + done + + rm -f "${marker_host}" + if [[ "${ready}" -ne 1 ]]; then + echo "goosed remount failed: containers did not become healthy on the new live directory" >&2 + return 1 + fi +} FULL_BACKUP_TAR="${BACKUP_DIR}/memind-full-${RELEASE_ID}-before.tar.gz" PERSIST_BACKUP_TAR="${BACKUP_DIR}/memind-persisted-${RELEASE_ID}-before.tar.gz" PERSISTED_ITEMS=( @@ -570,6 +648,8 @@ rm -rf "${OLD_LIVE_DIR}" mv "${APP_DIR}" "${OLD_LIVE_DIR}" mv "${RUNTIME_DIR}" "${APP_DIR}" +remount_goosed_after_live_swap + say "更新 LaunchAgent 指向 runtime 启动脚本" cat > "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" < -- 2.52.0