Compare commits

..

4 Commits

Author SHA1 Message Date
john 4e6db07123 fix: verify MindSpace service replacement
Memind CI / Test, build, and release guards (pull_request) Successful in 2m26s
2026-07-21 18:55:23 +08:00
tkmind fe4b8c0da9 merge: preserve MindSpace runtime data during release
Memind CI / Test, build, and release guards (push) Successful in 2m30s
CI run 1063 passed. Required release safety fix for the image_make production canary.
2026-07-21 10:41:21 +00:00
john fa777fa195 fix: preserve MindSpace runtime data during release
Memind CI / Test, build, and release guards (pull_request) Successful in 2m31s
2026-07-21 18:37:49 +08:00
tkmind b63c906c23 merge: complete reviewed image generation delivery
Memind CI / Test, build, and release guards (push) Successful in 2m57s
Merge validated 0717-dev image generation delivery changes. No 103 runtime artifact or production deployment.
2026-07-20 12:09:06 +00:00
2 changed files with 91 additions and 5 deletions
+44
View File
@@ -6,6 +6,10 @@ const releaseScript = fs.readFileSync(
new URL('./scripts/release-portal-runtime-prod.sh', import.meta.url),
'utf8',
);
const mindSpaceReleaseScript = fs.readFileSync(
new URL('./scripts/release-mindspace-service-prod.sh', import.meta.url),
'utf8',
);
test('103 release remounts goosed after swapping the live directory', () => {
assert.match(releaseScript, /remount_goosed_after_live_swap\(\)/);
@@ -23,3 +27,43 @@ test('103 release remounts goosed after swapping the live directory', () => {
assert.ok(remountIndex > swapIndex, 'goosed remount must happen after the live swap');
assert.ok(portalStartIndex > remountIndex, 'Portal must start only after goosed remount succeeds');
});
test('MindSpace release preserves runtime data and returns it during rollback', () => {
assert.match(mindSpaceReleaseScript, /PERSISTED_DATA_MOVED=0/);
assert.match(mindSpaceReleaseScript, /PERSISTED_USERS_MOVED=0/);
assert.match(mindSpaceReleaseScript, /mv "\$\{OLD_APP_ARCHIVE\}\/data" "\$\{APP_DIR\}\/data"/);
assert.match(mindSpaceReleaseScript, /mv "\$\{OLD_APP_ARCHIVE\}\/users" "\$\{APP_DIR\}\/users"/);
assert.match(mindSpaceReleaseScript, /mv "\$\{APP_DIR\}\/data" "\$\{OLD_APP_ARCHIVE\}\/data"/);
assert.match(mindSpaceReleaseScript, /mv "\$\{APP_DIR\}\/users" "\$\{OLD_APP_ARCHIVE\}\/users"/);
});
test('MindSpace release waits for the old listener and verifies the new launchd job', () => {
const bootoutIndex = mindSpaceReleaseScript.indexOf(
'launchctl bootout "${LAUNCHD_GUI}/${SERVICE_LABEL}"',
);
const listenerWaitIndex = mindSpaceReleaseScript.indexOf('lsof -nP -iTCP:8082', bootoutIndex);
const swapIndex = mindSpaceReleaseScript.indexOf('mv "${NEW_DIR}" "${APP_DIR}"');
const bootstrapIndex = mindSpaceReleaseScript.indexOf(
'launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}"',
swapIndex,
);
const portalRestartIndex = mindSpaceReleaseScript.indexOf(
'launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}"',
bootstrapIndex,
);
const finalServiceCheckIndex = mindSpaceReleaseScript.indexOf('service_is_running', portalRestartIndex);
assert.ok(bootoutIndex >= 0, 'old MindSpace launchd job must be stopped');
assert.ok(listenerWaitIndex > bootoutIndex, 'release must wait for the old 8082 listener to exit');
assert.ok(swapIndex > listenerWaitIndex, 'runtime swap must happen after the old listener exits');
assert.ok(bootstrapIndex > swapIndex, 'new launchd job must start after the runtime swap');
assert.doesNotMatch(
mindSpaceReleaseScript.slice(bootstrapIndex, bootstrapIndex + 120),
/\|\| true/,
'launchctl bootstrap failures must not be ignored',
);
assert.ok(
finalServiceCheckIndex > portalRestartIndex,
'MindSpace launchd state must be rechecked after Portal restarts',
);
});
+47 -5
View File
@@ -196,11 +196,17 @@ SERVICE_STARTED=0
PORTAL_TOUCHED=0
APP_MOVED=0
HAD_OLD_APP=0
PERSISTED_DATA_MOVED=0
PERSISTED_USERS_MOVED=0
say() {
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
}
service_is_running() {
launchctl print "${LAUNCHD_GUI}/${SERVICE_LABEL}" 2>/dev/null | grep -q 'state = running'
}
rollback() {
set +e
say "rollback: restoring previous state"
@@ -217,6 +223,16 @@ rollback() {
rm -f "${SERVICE_PLIST}"
fi
if [[ "${APP_MOVED}" == "1" ]]; then
if [[ "${HAD_OLD_APP}" == "1" ]]; then
if [[ "${PERSISTED_DATA_MOVED}" == "1" && ( -e "${APP_DIR}/data" || -L "${APP_DIR}/data" ) ]]; then
rm -rf "${OLD_APP_ARCHIVE}/data"
mv "${APP_DIR}/data" "${OLD_APP_ARCHIVE}/data"
fi
if [[ "${PERSISTED_USERS_MOVED}" == "1" && ( -e "${APP_DIR}/users" || -L "${APP_DIR}/users" ) ]]; then
rm -rf "${OLD_APP_ARCHIVE}/users"
mv "${APP_DIR}/users" "${OLD_APP_ARCHIVE}/users"
fi
fi
rm -rf "${APP_DIR}"
if [[ "${HAD_OLD_APP}" == "1" && -d "${OLD_APP_ARCHIVE}" ]]; then
mv "${OLD_APP_ARCHIVE}" "${APP_DIR}"
@@ -302,6 +318,16 @@ EOF
say "切换 MindSpace service live 目录"
launchctl bootout "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
for _ in $(seq 1 60); do
if ! lsof -nP -iTCP:8082 -sTCP:LISTEN >/dev/null 2>&1; then
break
fi
sleep 1
done
if lsof -nP -iTCP:8082 -sTCP:LISTEN >/dev/null 2>&1; then
echo "旧 MindSpace service 未在超时内退出,拒绝切换 runtime" >&2
exit 1
fi
rm -rf "${OLD_APP_ARCHIVE}"
if [[ -d "${APP_DIR}" ]]; then
mv "${APP_DIR}" "${OLD_APP_ARCHIVE}"
@@ -309,21 +335,34 @@ fi
mv "${NEW_DIR}" "${APP_DIR}"
APP_MOVED=1
say "继承 MindSpace 持久目录"
if [[ "${HAD_OLD_APP}" == "1" && ( -e "${OLD_APP_ARCHIVE}/data" || -L "${OLD_APP_ARCHIVE}/data" ) ]]; then
rm -rf "${APP_DIR}/data"
mv "${OLD_APP_ARCHIVE}/data" "${APP_DIR}/data"
PERSISTED_DATA_MOVED=1
fi
if [[ "${HAD_OLD_APP}" == "1" && ( -e "${OLD_APP_ARCHIVE}/users" || -L "${OLD_APP_ARCHIVE}/users" ) ]]; then
rm -rf "${APP_DIR}/users"
mv "${OLD_APP_ARCHIVE}/users" "${APP_DIR}/users"
PERSISTED_USERS_MOVED=1
fi
say "启动 MindSpace service"
mkdir -p "${APP_DIR}/data/mindspace" "${APP_DIR}/users"
launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}" >/dev/null 2>&1 || true
launchctl enable "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
launchctl kickstart -k "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}" >/dev/null
SERVICE_STARTED=1
launchctl enable "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null
launchctl kickstart -k "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null
for _ in $(seq 1 30); do
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)"
if [[ "${code}" == "200" ]]; then
if [[ "${code}" == "200" ]] && service_is_running; then
break
fi
sleep 1
done
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)" == "200" ]]
service_is_running
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/mindspace/v1/contract || true)" == "200" ]]
set_env() {
@@ -371,6 +410,9 @@ for _ in $(seq 1 30); do
sleep 1
done
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/api/status || true)" == "200" ]]
sleep 3
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)" == "200" ]]
service_is_running
trap - ERR
@@ -379,7 +421,7 @@ printf 'release_id=%s\n' "${RELEASE_ID}"
printf 'service_root=%s\n' "${APP_DIR}"
printf 'service_backup=%s\n' "${APP_BACKUP}"
printf 'env_backup=%s\n' "${ENV_BACKUP}"
printf 'service_health=%s\n' "$(curl -s http://127.0.0.1:8082/health)"
printf 'service_health=%s\n' "$(curl -fsS http://127.0.0.1:8082/health)"
REMOTE
say "发布完成"