Merge pull request 'fix: remount goosed after portal runtime swap' (#11) from codex/fix-goosed-remount into main
Memind CI / Test, build, and release guards (push) Failing after 1m23s

Ensure goosed containers remount the new Portal/MindSpace live directory after each atomic runtime swap.
This commit was merged in pull request #11.
This commit is contained in:
2026-07-17 04:07:21 +00:00
2 changed files with 105 additions and 0 deletions
+25
View File
@@ -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');
});
+80
View File
@@ -378,6 +378,84 @@ OLD_LIVE_DIR="${ARCHIVE_DIR}/Memind-source-before-${RELEASE_ID}"
BUNDLE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.tar.gz" BUNDLE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.tar.gz"
MANIFEST="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.manifest.txt" MANIFEST="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.manifest.txt"
SHA_FILE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.sha256" 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" FULL_BACKUP_TAR="${BACKUP_DIR}/memind-full-${RELEASE_ID}-before.tar.gz"
PERSIST_BACKUP_TAR="${BACKUP_DIR}/memind-persisted-${RELEASE_ID}-before.tar.gz" PERSIST_BACKUP_TAR="${BACKUP_DIR}/memind-persisted-${RELEASE_ID}-before.tar.gz"
PERSISTED_ITEMS=( PERSISTED_ITEMS=(
@@ -570,6 +648,8 @@ rm -rf "${OLD_LIVE_DIR}"
mv "${APP_DIR}" "${OLD_LIVE_DIR}" mv "${APP_DIR}" "${OLD_LIVE_DIR}"
mv "${RUNTIME_DIR}" "${APP_DIR}" mv "${RUNTIME_DIR}" "${APP_DIR}"
remount_goosed_after_live_swap
say "更新 LaunchAgent 指向 runtime 启动脚本" say "更新 LaunchAgent 指向 runtime 启动脚本"
cat > "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" <<EOF cat > "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>