Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 046704816f | |||
| 02f2dc6b17 |
@@ -16,6 +16,13 @@ Portal 的首个生产动作必须是用户级灰度,不能直接整包替换
|
|||||||
`MEMIND_RUNTIME_ROLE=candidate` 会禁止重复启动 MindSpace、Plaza、提醒、订阅续期和
|
`MEMIND_RUNTIME_ROLE=candidate` 会禁止重复启动 MindSpace、Plaza、提醒、订阅续期和
|
||||||
Agent 恢复扫描等单例后台任务。
|
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 匹配。
|
- `john`:按数据库中的唯一用户名或用户 ID 匹配。
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
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, /nginx -t/);
|
||||||
assert.match(source, /CANARY_PROXY_PORT=18082/);
|
assert.match(source, /CANARY_PROXY_PORT=18082/);
|
||||||
assert.match(source, /CANARY_TUNNEL_REMOTE_PORT=19082/);
|
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_USERNAMES="john"/);
|
||||||
assert.match(source, /CANARY_WECHAT_USER_IDS="wx_ul610et8"/);
|
assert.match(source, /CANARY_WECHAT_USER_IDS="wx_ul610et8"/);
|
||||||
assert.match(source, /route-probe\?\$\{probe_query\}/);
|
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/);
|
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 () => {
|
test('canary release and rollback remote shells remain syntactically valid', async () => {
|
||||||
const releaseSource = await fs.readFile(CANARY_RELEASE, 'utf8');
|
const releaseSource = await fs.readFile(CANARY_RELEASE, 'utf8');
|
||||||
const rollbackSource = await fs.readFile(CANARY_ROLLBACK, 'utf8');
|
const rollbackSource = await fs.readFile(CANARY_ROLLBACK, 'utf8');
|
||||||
|
|||||||
@@ -261,7 +261,9 @@ candidate_healthy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
edge_ssh() {
|
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() {
|
write_canary_tunnel_plist() {
|
||||||
@@ -586,7 +588,9 @@ done
|
|||||||
[[ "${counts:-unknown unknown}" == "0 0" ]]
|
[[ "${counts:-unknown unknown}" == "0 0" ]]
|
||||||
|
|
||||||
say "Switch the committed 105 nginx upstreams to the isolated canary tunnel"
|
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
|
set -euo pipefail
|
||||||
for config in '${EDGE_MOBILE_CONFIG}' '${EDGE_WECHAT_CONFIG}'; do
|
for config in '${EDGE_MOBILE_CONFIG}' '${EDGE_WECHAT_CONFIG}'; do
|
||||||
before_count="\$(grep -cF 'proxy_pass http://58.38.22.103:8081;' "\${config}")"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd "${ROOT}"
|
||||||
|
|
||||||
set -a
|
set -a
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "${STABLE_ROOT}/.env"
|
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 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_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}"
|
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
|
unset TKMIND_API_TARGET_1
|
||||||
|
|
||||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||||
|
|||||||
Reference in New Issue
Block a user