fix: verify configured Goose targets during Portal release
Memind CI / Test, build, and release guards (push) Has been cancelled

This commit is contained in:
john
2026-10-19 13:55:57 +08:00
parent 07e9244fbd
commit afc8cbc03c
3 changed files with 93 additions and 3 deletions
@@ -63,3 +63,11 @@
发布授权已经收到;剩余阻断是工作区整理与 Git 写权限,并非再次索要发布批准。解除后须按现有规则继续提交、同步主线、CI、整包与生产 Gate,而不是单文件上传线上源码。当前不能保证次日自动生成已经使用修复后的提取器。
后续用户明确允许 Git 写入,正常 `git add` 已成功。图片代码与测试将单独提交;其余未提交内容需按 portal-release 流程先取得临时 stash 的单独同意,发布后恢复。发布前基础数据库、能力、provider 与微信测试已通过。
## 2026-10-19 发布回滚与配置修复
图片修复已通过 CI 并进入 main `07e9244`。快速发布 `20261019-134634-07e9244` 切换后自动回滚,当前 live manifest 仍为 `20261009-102835-05b440c`,失败 runtime 保留在 `archives/Memind-failed-after-20261019-134634-07e9244`。
发现发布脚本 native remount 和健康检查仍写死 18006–18014 九个实例,而生产 `TKMIND_API_TARGETS` 仅配置 18006、18007,两者 HTTP 200,18008 已停用。脚本改为从 live `.env` 读取配置,按实际目标重启和检查;空配置、非法地址和端口仍阻断,不跳过不可用的已配置目标。Docker 路径的健康探测同样使用实际目标。未通过增加已停用实例或跳过健康检查解决。
新增六项离线回归,覆盖双实例、引号与 localhost、空配置、非法地址、非法端口和已配置实例不健康的阻断。修复后需重新通过 CI 和完整 main runtime 发布,再核对 live manifest。
+57
View File
@@ -0,0 +1,57 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const release = readFileSync(new URL('./scripts/release-portal-runtime-prod.sh', import.meta.url), 'utf8');
const start = release.indexOf('remount_goosed_after_live_swap() {');
const end = release.indexOf('\n mkdir -p "${APP_DIR}"', release.indexOf('\n return 0\n fi', start));
const nativeFunction = release.slice(start, end)
.replace('if [[ ! -x "${docker_bin}" || ! -f "${compose_file}" ]]; then', 'if true; then') + '\n}';
function run(targets, unavailable = '') {
const root = mkdtempSync(join(tmpdir(), 'portal-targets-'));
try {
writeFileSync(join(root, '.env'), `TKMIND_API_TARGETS=${targets}\n`);
writeFileSync(join(root, 'mindspace-sandbox-mcp.mjs'), '');
const result = spawnSync('/bin/bash', ['-c', `
set -euo pipefail
APP_DIR="$1"; RELEASE_ID=test; unavailable="$2"
say() { :; }
launchctl() { printf 'restart=%s\\n' "$*" >> "$APP_DIR/restarts"; }
sleep() { :; }
curl() { case "$*" in *"$unavailable"*) return 1;; esac; printf ok; }
# An empty unavailable marker must not match every request.
if [[ -z "$2" ]]; then curl() { printf ok; }; fi
${nativeFunction}
remount_goosed_after_live_swap
`, 'test', root, unavailable], { encoding: 'utf8' });
try { result.stdout += readFileSync(join(root, 'restarts'), 'utf8'); } catch {}
return result;
} finally { rmSync(root, { recursive: true, force: true }); }
}
test('native remount restarts only the two configured targets', () => {
const result = run('https://127.0.0.1:18006,https://127.0.0.1:18007');
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /native-18006/);
assert.match(result.stdout, /native-18007/);
assert.doesNotMatch(result.stdout, /native-18008/);
});
test('quoted targets and localhost are supported', () => {
assert.equal(run('"https://127.0.0.1:18006,https://localhost:18007"').status, 0);
});
for (const targets of ['', 'https://example.com:18006', 'https://127.0.0.1:99999']) {
test(`invalid configured targets fail closed: ${targets || '(empty)'}`, () => {
const result = run(targets);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /goosed remount failed/);
assert.doesNotMatch(result.stdout, /restart=/);
});
}
test('an unavailable configured target still blocks remount', () => {
const result = run('https://127.0.0.1:18006,https://127.0.0.1:18007', '18007');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /did not become healthy/);
});
+28 -3
View File
@@ -497,6 +497,31 @@ remount_goosed_after_live_swap() {
local marker_host="${APP_DIR}/${marker_rel}"
local marker_value="${RELEASE_ID}-$(date +%s)"
local ready=0
local targets_line target port
local target_urls=() native_ports=()
targets_line="$(sed -n 's/^TKMIND_API_TARGETS=//p' "${APP_DIR}/.env" | tail -1 | tr -d "\"'\r")"
if [[ -z "${targets_line}" ]]; then
echo "goosed remount failed: TKMIND_API_TARGETS is empty" >&2
return 1
fi
IFS=',' read -r -a target_urls <<< "${targets_line}"
for target in "${target_urls[@]}"; do
target="$(printf '%s' "${target}" | tr -d '[:space:]')"
if [[ ! "${target}" =~ ^https://(127\.0\.0\.1|localhost):([0-9]+)/*$ ]]; then
echo "goosed remount failed: invalid or unsupported configured target: ${target}" >&2
return 1
fi
port="${BASH_REMATCH[2]}"
if (( 10#${port} < 1 || 10#${port} > 65535 )); then
echo "goosed remount failed: configured target port out of range" >&2
return 1
fi
native_ports+=("${port}")
done
if [[ "${#native_ports[@]}" -eq 0 ]]; then
echo "goosed remount failed: TKMIND_API_TARGETS is empty" >&2
return 1
fi
if [[ ! -x "${docker_bin}" || ! -f "${compose_file}" ]]; then
say "重启 native goosed 以刷新 Portal/MindSpace bind mount"
@@ -505,7 +530,7 @@ remount_goosed_after_live_swap() {
local gui="gui/$(id -u)"
local port
for port in $(seq 18006 18014); do
for port in "${native_ports[@]}"; do
launchctl kickstart -k "${gui}/cn.tkmind.goosed-native-${port}" >/dev/null 2>&1 || true
done
@@ -517,7 +542,7 @@ remount_goosed_after_live_swap() {
if [[ ! -f "${APP_DIR}/mindspace-sandbox-mcp.mjs" ]]; then
healthy=0
fi
for port in $(seq 18006 18014); do
for port in "${native_ports[@]}"; do
if [[ "$(curl -skS -m 5 "https://127.0.0.1:${port}/status" 2>/dev/null || true)" != "ok" ]]; then
healthy=0
fi
@@ -590,7 +615,7 @@ remount_goosed_after_live_swap() {
done
local port
for port in $(seq 18006 18014); do
for port in "${native_ports[@]}"; do
if [[ "$(curl -skS -m 5 "https://127.0.0.1:${port}/status" 2>/dev/null || true)" != "ok" ]]; then
healthy=0
fi