Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 046704816f | |||
| 02f2dc6b17 |
@@ -16,6 +16,13 @@ Portal 的首个生产动作必须是用户级灰度,不能直接整包替换
|
||||
`MEMIND_RUNTIME_ROLE=candidate` 会禁止重复启动 MindSpace、Plaza、提醒、订阅续期和
|
||||
Agent 恢复扫描等单例后台任务。
|
||||
|
||||
候选 Portal 必须从候选 runtime 目录启动,使相对模块只解析到本次候选 artifact。
|
||||
候选 goosed 在 Docker 内执行扩展,MCP Node 和入口路径必须固定为
|
||||
`/usr/local/bin/node` 与 `/opt/portal/mindspace-sandbox-mcp.mjs`;禁止继承稳定
|
||||
`.env` 中仅宿主机可见的 `/Users/...` 路径。若日志出现
|
||||
`Failed to add extension` 和宿主机绝对路径的 `MODULE_NOT_FOUND`,应先核对候选
|
||||
进程环境与 `/opt/portal` 容器挂载,禁止把宿主机文件复制进容器临时修补。
|
||||
|
||||
## 身份和回落规则
|
||||
|
||||
- `john`:按数据库中的唯一用户名或用户 ID 匹配。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
@@ -139,6 +140,15 @@ test('production canary keeps stable 8081 live and switches only after verified
|
||||
assert.match(source, /nginx -t/);
|
||||
assert.match(source, /CANARY_PROXY_PORT=18082/);
|
||||
assert.match(source, /CANARY_TUNNEL_REMOTE_PORT=19082/);
|
||||
assert.match(
|
||||
source,
|
||||
/edge_ssh\(\) \{\s*#.*\n\s*#.*\n\s*ssh -n -o BatchMode=yes/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/ssh -o BatchMode=yes -o ConnectTimeout=10 "\$\{EDGE_HOST\}" \/bin\/bash <<EDGE_SWITCH/,
|
||||
);
|
||||
assert.doesNotMatch(source, /edge_ssh \/bin\/bash <<EDGE_SWITCH/);
|
||||
assert.match(source, /CANARY_USERNAMES="john"/);
|
||||
assert.match(source, /CANARY_WECHAT_USER_IDS="wx_ul610et8"/);
|
||||
assert.match(source, /route-probe\?\$\{probe_query\}/);
|
||||
@@ -154,6 +164,60 @@ test('production canary keeps stable 8081 live and switches only after verified
|
||||
assert.doesNotMatch(source, /bootout.*cn\.tkmind\.memind-portal/);
|
||||
});
|
||||
|
||||
test('candidate runner overrides stable host MCP paths with container-visible paths', async (t) => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-canary-runner-'));
|
||||
t.after(() => fs.rm(tempRoot, { recursive: true, force: true }));
|
||||
|
||||
const stableRoot = path.join(tempRoot, 'stable');
|
||||
const fakeNode = path.join(tempRoot, 'node');
|
||||
await fs.mkdir(stableRoot);
|
||||
await fs.writeFile(
|
||||
path.join(stableRoot, '.env'),
|
||||
[
|
||||
'GOOSED_MCP_NODE_PATH=/opt/homebrew/opt/node@24/bin/node',
|
||||
'GOOSED_MCP_SERVER_PATH=/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
await fs.writeFile(
|
||||
fakeNode,
|
||||
[
|
||||
'#!/usr/bin/env bash',
|
||||
'printf "cwd=%s\\n" "$PWD"',
|
||||
'printf "mcp_node=%s\\n" "$GOOSED_MCP_NODE_PATH"',
|
||||
'printf "mcp_server=%s\\n" "$GOOSED_MCP_SERVER_PATH"',
|
||||
'printf "entrypoint=%s\\n" "$1"',
|
||||
'',
|
||||
].join('\n'),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
'bash',
|
||||
[path.join(ROOT, 'scripts', 'run-memind-portal-candidate.sh')],
|
||||
{
|
||||
cwd: '/',
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
MEMIND_CANARY_STABLE_ROOT: stableRoot,
|
||||
MEMIND_CANARY_RELEASE_ID: 'test-release',
|
||||
MEMIND_CANARY_CANDIDATE_PORT: '65534',
|
||||
NODE_BIN: fakeNode,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, new RegExp(`^cwd=${ROOT}$`, 'm'));
|
||||
assert.match(result.stdout, /^mcp_node=\/usr\/local\/bin\/node$/m);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/^mcp_server=\/opt\/portal\/mindspace-sandbox-mcp\.mjs$/m,
|
||||
);
|
||||
assert.match(result.stdout, new RegExp(`^entrypoint=${ROOT}/server\\.mjs$`, 'm'));
|
||||
});
|
||||
|
||||
test('canary release and rollback remote shells remain syntactically valid', async () => {
|
||||
const releaseSource = await fs.readFile(CANARY_RELEASE, 'utf8');
|
||||
const rollbackSource = await fs.readFile(CANARY_ROLLBACK, 'utf8');
|
||||
|
||||
@@ -261,7 +261,9 @@ candidate_healthy() {
|
||||
}
|
||||
|
||||
edge_ssh() {
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${EDGE_HOST}" "$@"
|
||||
# This remote release script itself arrives on stdin. Ordinary nested SSH
|
||||
# commands must not consume the unparsed remainder of that script.
|
||||
ssh -n -o BatchMode=yes -o ConnectTimeout=10 "${EDGE_HOST}" "$@"
|
||||
}
|
||||
|
||||
write_canary_tunnel_plist() {
|
||||
@@ -586,7 +588,9 @@ done
|
||||
[[ "${counts:-unknown unknown}" == "0 0" ]]
|
||||
|
||||
say "Switch the committed 105 nginx upstreams to the isolated canary tunnel"
|
||||
edge_ssh /bin/bash <<EDGE_SWITCH
|
||||
# Unlike ordinary edge_ssh calls, this command intentionally receives only the
|
||||
# bounded EDGE_SWITCH heredoc on stdin.
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${EDGE_HOST}" /bin/bash <<EDGE_SWITCH
|
||||
set -euo pipefail
|
||||
for config in '${EDGE_MOBILE_CONFIG}' '${EDGE_WECHAT_CONFIG}'; do
|
||||
before_count="\$(grep -cF 'proxy_pass http://58.38.22.103:8081;' "\${config}")"
|
||||
|
||||
@@ -9,6 +9,8 @@ if [[ ! -d "${STABLE_ROOT}" || ! -f "${STABLE_ROOT}/.env" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${STABLE_ROOT}/.env"
|
||||
@@ -27,6 +29,10 @@ export H5_HOST=127.0.0.1
|
||||
export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}"
|
||||
export TKMIND_API_TARGETS="${MEMIND_CANARY_GOOSED_URL:-https://127.0.0.1:18015}"
|
||||
export TKMIND_API_TARGET="${MEMIND_CANARY_GOOSED_URL:-https://127.0.0.1:18015}"
|
||||
# Extensions are spawned inside goosed-canary, where the candidate artifact is
|
||||
# mounted at /opt/portal. Never inherit host-only MCP paths from the stable .env.
|
||||
export GOOSED_MCP_NODE_PATH=/usr/local/bin/node
|
||||
export GOOSED_MCP_SERVER_PATH=/opt/portal/mindspace-sandbox-mcp.mjs
|
||||
unset TKMIND_API_TARGET_1
|
||||
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
|
||||
Reference in New Issue
Block a user