feat: add guarded portal canary release
Memind CI / Test, build, and release guards (push) Failing after 2m14s
Memind CI / Test, build, and release guards (push) Failing after 2m14s
This commit is contained in:
@@ -136,6 +136,26 @@ async function bundleServer() {
|
||||
await run(esbuildBin, args);
|
||||
}
|
||||
|
||||
async function bundleCanaryProxy() {
|
||||
if (!(await exists(esbuildBin))) {
|
||||
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
||||
}
|
||||
console.log('==> 打包用户级灰度路由器');
|
||||
const args = [
|
||||
'memind-canary-proxy.mjs',
|
||||
'--bundle',
|
||||
'--platform=node',
|
||||
'--format=esm',
|
||||
`--target=${runtimeNodeTarget}`,
|
||||
'--outfile=.runtime/portal/memind-canary-proxy.mjs',
|
||||
'--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
|
||||
];
|
||||
for (const pkg of externalPackages) {
|
||||
args.push(`--external:${pkg}`);
|
||||
}
|
||||
await run(esbuildBin, args);
|
||||
}
|
||||
|
||||
async function bundleSandboxMcp() {
|
||||
if (!(await exists(esbuildBin))) {
|
||||
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
||||
@@ -386,6 +406,21 @@ async function writeMetadata() {
|
||||
} else {
|
||||
throw new Error(`缺少 Portal 启动脚本: ${prodStartScript}`);
|
||||
}
|
||||
for (const scriptName of [
|
||||
'run-memind-portal-candidate.sh',
|
||||
'run-memind-canary-proxy-prod.sh',
|
||||
]) {
|
||||
const source = path.join(root, 'scripts', scriptName);
|
||||
if (!(await exists(source))) {
|
||||
throw new Error(`缺少灰度运行脚本: ${source}`);
|
||||
}
|
||||
await fs.copyFile(source, path.join(runtimeScriptsDir, scriptName));
|
||||
await fs.chmod(path.join(runtimeScriptsDir, scriptName), 0o755);
|
||||
}
|
||||
await fs.copyFile(
|
||||
path.join(root, 'scripts', 'goosed-canary.compose.yml'),
|
||||
path.join(runtimeScriptsDir, 'goosed-canary.compose.yml'),
|
||||
);
|
||||
|
||||
await fs.copyFile(
|
||||
path.join(root, 'scripts', 'load-env.mjs'),
|
||||
@@ -491,6 +526,7 @@ async function writeMetadata() {
|
||||
' tkmind-search-mcp.mjs (esbuild bundle; required when MindSearch is enabled)',
|
||||
' tkmind-excel-mcp.mjs (esbuild bundle; required when Excel Analyst is enabled)',
|
||||
' wechat-mp.bundle.mjs (esbuild bundle; hot-swappable WeChat MP module)',
|
||||
' memind-canary-proxy.mjs (immutable-identity stable/candidate router)',
|
||||
'',
|
||||
'Post-deploy validation (scripts/check-mindspace-public-links.mjs):',
|
||||
' Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.',
|
||||
@@ -516,6 +552,7 @@ async function writeMetadata() {
|
||||
' Current public path: m.tkmind.cn -> 105 nginx -> 103 Portal :8081',
|
||||
' No separate public test domain remains; use explicit local/test overrides only when needed.',
|
||||
' Production H5 and WeChat links should use m.tkmind.cn unless a test rollout explicitly overrides H5_PUBLIC_BASE_URL.',
|
||||
' Canary layout: stable :8081, candidate :18081, identity router :18080, candidate goosed :18015.',
|
||||
'',
|
||||
'Streaming runtime operations:',
|
||||
' node scripts/check-stream-runtime.mjs',
|
||||
@@ -560,6 +597,7 @@ async function main() {
|
||||
await buildFrontend();
|
||||
await copyRuntimeAssets();
|
||||
await bundleServer();
|
||||
await bundleCanaryProxy();
|
||||
await bundleWechatMp();
|
||||
await bundleSandboxMcp();
|
||||
await bundleMindSearchMcp();
|
||||
@@ -571,6 +609,8 @@ async function main() {
|
||||
await copyMindspacePublicLinkTools();
|
||||
await writeMetadata();
|
||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
|
||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-candidate.sh'), 0o755);
|
||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-canary-proxy-prod.sh'), 0o755);
|
||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'), 0o755);
|
||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'), 0o755);
|
||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), 0o755);
|
||||
|
||||
@@ -53,9 +53,16 @@ case "${branch}" in
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "$(git -C "${ROOT}" status --porcelain=v1 --untracked-files=all)" ]]; then
|
||||
# The runtime builder intentionally strips this legacy tracked content from the
|
||||
# source-free artifact. The complete Gate applies the same narrow exception.
|
||||
relevant_status="$(
|
||||
git -C "${ROOT}" status --porcelain=v1 --untracked-files=all -- \
|
||||
. \
|
||||
':(exclude).runtime/portal/public/plaza-covers/**'
|
||||
)"
|
||||
if [[ -n "${relevant_status}" ]]; then
|
||||
echo "Release check failed: worktree has uncommitted or untracked changes." >&2
|
||||
git -C "${ROOT}" status --short --untracked-files=all >&2
|
||||
printf '%s\n' "${relevant_status}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
services:
|
||||
goosed-canary:
|
||||
image: tkmind/goosed:prod-${GOOSED_TAG:-latest}
|
||||
container_name: goosed-prod-canary
|
||||
restart: unless-stopped
|
||||
env_file: ${GOOSED_PROD_ENV_FILE:-/Users/john/Project/goosed-prod/.env}
|
||||
environment:
|
||||
GOOSE_PORT: 18006
|
||||
GOOSE_AIDER_BIN: /usr/local/bin/aider
|
||||
GOOSE_OPENHANDS_BIN: /usr/local/bin/openhands
|
||||
GOOSE_OPENHANDS_RUNNER: host
|
||||
GOOSE_OPENHANDS_TIMEOUT_SECS: "900"
|
||||
ports:
|
||||
- "18015:18006"
|
||||
volumes:
|
||||
- ${CANDIDATE_RUNTIME_DIR:?}:/opt/portal:ro
|
||||
- ${GOOSED_RUNTIME_TOOLS_DIR:?}:/opt/tools:ro
|
||||
- ${GOOSED_RUNTIME_TOOLS_DIR:?}/bin/aider:/usr/local/bin/aider:ro
|
||||
- ${GOOSED_RUNTIME_TOOLS_DIR:?}/bin/openhands:/usr/local/bin/openhands:ro
|
||||
- ${GOOSED_RUNTIME_TOOLS_DIR:?}/openhands-data:/opt/tools/openhands-data
|
||||
- ${GOOSED_HARNESS_DIR:?}:/home/goose/.codex/harness
|
||||
- ${MINDSPACE_ROOT:?}:${MINDSPACE_ROOT:?}
|
||||
- ${MINDSPACE_STORAGE_ROOT:?}:${MINDSPACE_STORAGE_ROOT:?}
|
||||
- goosed-canary-data:/home/goose/.local/share/goose
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-kfsS", "https://127.0.0.1:18006/status"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
networks:
|
||||
- goosed-net
|
||||
|
||||
volumes:
|
||||
goosed-canary-data:
|
||||
|
||||
networks:
|
||||
goosed-net:
|
||||
driver: bridge
|
||||
Executable
+575
@@ -0,0 +1,575 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HOST="${STUDIO_HOST:-58.38.22.103}"
|
||||
REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}"
|
||||
STABLE_DIR="${REMOTE_ROOT}/Memind"
|
||||
CANDIDATE_BASE="${REMOTE_ROOT}/Memind-candidates"
|
||||
INCOMING_DIR="${REMOTE_ROOT}/incoming/memind-portal-canary"
|
||||
BACKUP_DIR="${REMOTE_ROOT}/backups/memind"
|
||||
GOOSED_DIR="${REMOTE_ROOT}/goosed-prod"
|
||||
RUNTIME_ROOT="${ROOT}/.runtime/portal"
|
||||
PORTAL_CANDIDATE_LABEL="cn.tkmind.memind-portal-candidate"
|
||||
CANARY_PROXY_LABEL="cn.tkmind.memind-canary-proxy"
|
||||
PORTAL_TUNNEL_LABEL="cn.tkmind.memind-portal-tunnel"
|
||||
MEMIND_PORTAL_TUNNEL_HOST="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}"
|
||||
MEMIND_PORTAL_TUNNEL_REMOTE_PORT="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}"
|
||||
CANARY_USERNAMES="john"
|
||||
CANARY_WECHAT_USER_IDS="wx_ul610et8"
|
||||
RELEASE_TS="$(date +%Y%m%d-%H%M%S)"
|
||||
FULL_SHA="$(git -C "${ROOT}" rev-parse HEAD)"
|
||||
SHORT_SHA="${FULL_SHA:0:7}"
|
||||
RELEASE_ID="${RELEASE_TS}-${SHORT_SHA}"
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/memind-portal-canary-release.XXXXXX")"
|
||||
BUNDLE_PATH="${TMP_DIR}/memind-portal-canary-${RELEASE_ID}.tar.gz"
|
||||
MANIFEST_PATH="${TMP_DIR}/memind-portal-canary-${RELEASE_ID}.manifest.txt"
|
||||
SHA_PATH="${TMP_DIR}/memind-portal-canary-${RELEASE_ID}.sha256"
|
||||
DRY_RUN=0
|
||||
AUTO_YES=0
|
||||
|
||||
cleanup() {
|
||||
rm -rf "${TMP_DIR}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/release-portal-canary-prod.sh [--dry-run] [--yes]
|
||||
|
||||
Deploys a candidate Portal beside the stable 103 runtime:
|
||||
stable Portal 127.0.0.1:8081
|
||||
canary router 127.0.0.1:18080
|
||||
candidate Portal 127.0.0.1:18081
|
||||
candidate goosed 127.0.0.1:18015
|
||||
|
||||
The stable runtime is not replaced. The 105 reverse tunnel moves to the canary
|
||||
router only after backups, candidate health, immutable identity checks, and
|
||||
automatic fallback checks pass.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--yes|-y) AUTO_YES=1 ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
for bypass in \
|
||||
ALLOW_PORTAL_RELEASE_SCOPE_BYPASS \
|
||||
ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES \
|
||||
MEMIND_RELEASE_SKIP_GATE; do
|
||||
if [[ "${!bypass:-0}" == "1" ]]; then
|
||||
echo "Production canary release forbids ${bypass}." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
say() {
|
||||
printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "Missing command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
need_cmd ssh
|
||||
need_cmd scp
|
||||
need_cmd tar
|
||||
need_cmd shasum
|
||||
|
||||
branch="$(git -C "${ROOT}" branch --show-current)"
|
||||
if [[ "${branch}" != "main" ]]; then
|
||||
echo "Canary production release requires main, got: ${branch:-detached}" >&2
|
||||
exit 1
|
||||
fi
|
||||
git -C "${ROOT}" fetch origin --prune
|
||||
if [[ "$(git -C "${ROOT}" rev-parse HEAD)" != "$(git -C "${ROOT}" rev-parse origin/main)" ]]; then
|
||||
echo "Canary production release requires HEAD to equal origin/main." >&2
|
||||
exit 1
|
||||
fi
|
||||
ALLOW_MAIN_RELEASE=1 bash "${ROOT}/scripts/check-release-ready.sh" --skip-fetch
|
||||
|
||||
say "Run release source guards"
|
||||
(
|
||||
cd "${ROOT}"
|
||||
npm test -- --test-name-pattern='publish|space|billing' >/dev/null
|
||||
npm run verify:mindspace-publish-guards >/dev/null
|
||||
npm run verify:mindspace-page-sync-guards >/dev/null
|
||||
npm run verify:h5-session-patches >/dev/null
|
||||
npm run verify:page-data >/dev/null
|
||||
npm run check:mindspace-public-links >/dev/null
|
||||
)
|
||||
|
||||
required_runtime_paths=(
|
||||
server.mjs
|
||||
memind-canary-proxy.mjs
|
||||
wechat-mp.bundle.mjs
|
||||
mindspace-sandbox-mcp.mjs
|
||||
tkmind-search-mcp.mjs
|
||||
tkmind-excel-mcp.mjs
|
||||
dist
|
||||
package.json
|
||||
scripts/run-memind-portal-candidate.sh
|
||||
scripts/run-memind-canary-proxy-prod.sh
|
||||
scripts/goosed-canary.compose.yml
|
||||
scripts/memind-portal-tunnel.sh
|
||||
)
|
||||
for relative in "${required_runtime_paths[@]}"; do
|
||||
[[ -e "${RUNTIME_ROOT}/${relative}" ]] || {
|
||||
echo "Candidate runtime is missing ${relative}; rebuild and rerun the complete Gate." >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
say "Verify the complete Gate report against the exact candidate artifact"
|
||||
node "${ROOT}/scripts/verify-release-gate-report.mjs" --artifact "${RUNTIME_ROOT}"
|
||||
|
||||
say "Package the verified candidate artifact"
|
||||
tar -czf "${BUNDLE_PATH}" -C "${RUNTIME_ROOT}" .
|
||||
bundle_sha="$(shasum -a 256 "${BUNDLE_PATH}" | awk '{print $1}')"
|
||||
printf '%s %s\n' "${bundle_sha}" "$(basename "${BUNDLE_PATH}")" > "${SHA_PATH}"
|
||||
{
|
||||
echo "release_id=${RELEASE_ID}"
|
||||
echo "created_at=$(date '+%Y-%m-%d %H:%M:%S %z')"
|
||||
echo "git_head=${FULL_SHA}"
|
||||
echo "git_branch=${branch}"
|
||||
echo "artifact_tree=.runtime/portal"
|
||||
echo "artifact_bundle_sha256=${bundle_sha}"
|
||||
echo "routing=stable:8081,proxy:18080,candidate:18081,goosed-canary:18015"
|
||||
echo "canary_usernames=${CANARY_USERNAMES}"
|
||||
echo "canary_wechat_user_ids=${CANARY_WECHAT_USER_IDS}"
|
||||
echo "manual_environment_changes=none"
|
||||
} > "${MANIFEST_PATH}"
|
||||
|
||||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||||
say "Dry-run complete; no 103 connection or production write occurred"
|
||||
ls -lh "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say "Run 103 read-only preflight"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" \
|
||||
"test -d '${STABLE_DIR}' \
|
||||
&& test -f '${STABLE_DIR}/.env' \
|
||||
&& test -f '${GOOSED_DIR}/docker-compose.prod.yml' \
|
||||
&& curl -fsS http://127.0.0.1:8081/api/status >/dev/null \
|
||||
&& /opt/homebrew/bin/docker inspect goosed-prod-1 >/dev/null \
|
||||
&& test \"\$(df -Pk '${REMOTE_ROOT}' | awk 'NR==2 {print \$4}')\" -gt 10485760"
|
||||
|
||||
if [[ "${AUTO_YES}" -ne 1 ]]; then
|
||||
say "Production canary confirmation"
|
||||
echo "Target host: ${HOST}"
|
||||
echo "Release: ${RELEASE_ID}"
|
||||
echo "Commit: ${FULL_SHA}"
|
||||
echo "Stable remains on 8081; selected immutable identities route to candidate 18081."
|
||||
read -r -p "Continue with the 103 canary release? [y/N] " confirm </dev/tty
|
||||
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
|
||||
fi
|
||||
|
||||
say "Upload the verified candidate bundle"
|
||||
ssh -o BatchMode=yes "${HOST}" "mkdir -p '${INCOMING_DIR}' '${BACKUP_DIR}' '${CANDIDATE_BASE}'"
|
||||
scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR}/"
|
||||
|
||||
say "Install the isolated candidate and identity router on 103"
|
||||
ssh -o BatchMode=yes "${HOST}" \
|
||||
"RELEASE_ID='${RELEASE_ID}' \
|
||||
STABLE_DIR='${STABLE_DIR}' \
|
||||
CANDIDATE_BASE='${CANDIDATE_BASE}' \
|
||||
INCOMING_DIR='${INCOMING_DIR}' \
|
||||
BACKUP_DIR='${BACKUP_DIR}' \
|
||||
GOOSED_DIR='${GOOSED_DIR}' \
|
||||
PORTAL_CANDIDATE_LABEL='${PORTAL_CANDIDATE_LABEL}' \
|
||||
CANARY_PROXY_LABEL='${CANARY_PROXY_LABEL}' \
|
||||
PORTAL_TUNNEL_LABEL='${PORTAL_TUNNEL_LABEL}' \
|
||||
MEMIND_PORTAL_TUNNEL_HOST='${MEMIND_PORTAL_TUNNEL_HOST}' \
|
||||
MEMIND_PORTAL_TUNNEL_REMOTE_PORT='${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}' \
|
||||
CANARY_USERNAMES='${CANARY_USERNAMES}' \
|
||||
CANARY_WECHAT_USER_IDS='${CANARY_WECHAT_USER_IDS}' \
|
||||
/bin/bash" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
|
||||
LAUNCHD_GUI="gui/$(id -u)"
|
||||
CANDIDATE_DIR="${CANDIDATE_BASE}/${RELEASE_ID}"
|
||||
BUNDLE="${INCOMING_DIR}/memind-portal-canary-${RELEASE_ID}.tar.gz"
|
||||
MANIFEST="${INCOMING_DIR}/memind-portal-canary-${RELEASE_ID}.manifest.txt"
|
||||
SHA_FILE="${INCOMING_DIR}/memind-portal-canary-${RELEASE_ID}.sha256"
|
||||
FULL_BACKUP_TAR="${BACKUP_DIR}/memind-full-${RELEASE_ID}-before-canary.tar.gz"
|
||||
PERSIST_BACKUP_TAR="${BACKUP_DIR}/memind-persisted-${RELEASE_ID}-before-canary.tar.gz"
|
||||
TUNNEL_PLIST="${HOME}/Library/LaunchAgents/${PORTAL_TUNNEL_LABEL}.plist"
|
||||
TUNNEL_PLIST_BACKUP="${BACKUP_DIR}/${PORTAL_TUNNEL_LABEL}-${RELEASE_ID}.plist"
|
||||
CANDIDATE_PLIST="${HOME}/Library/LaunchAgents/${PORTAL_CANDIDATE_LABEL}.plist"
|
||||
PROXY_PLIST="${HOME}/Library/LaunchAgents/${CANARY_PROXY_LABEL}.plist"
|
||||
SECRET_FILE="${HOME}/.config/memind/canary-router.secret"
|
||||
DOCKER_BIN="/opt/homebrew/bin/docker"
|
||||
GOOSED_COMPOSE="${GOOSED_DIR}/docker-compose.prod.yml"
|
||||
|
||||
say() {
|
||||
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
|
||||
}
|
||||
|
||||
candidate_healthy() {
|
||||
local headers code
|
||||
headers="$(mktemp)"
|
||||
code="$(
|
||||
curl -sS -D "${headers}" -o /dev/null -w '%{http_code}' \
|
||||
http://127.0.0.1:18081/api/status 2>/dev/null || true
|
||||
)"
|
||||
if [[ "${code}" == "200" ]] \
|
||||
&& grep -qi '^x-memind-runtime-role: candidate' "${headers}"; then
|
||||
rm -f "${headers}"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${headers}"
|
||||
return 1
|
||||
}
|
||||
|
||||
write_tunnel_plist() {
|
||||
local local_port="$1"
|
||||
cat > "${TUNNEL_PLIST}" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>${PORTAL_TUNNEL_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array><string>${STABLE_DIR}/scripts/memind-portal-tunnel.sh</string></array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>MEMIND_PORTAL_TUNNEL_HOST</key><string>${MEMIND_PORTAL_TUNNEL_HOST}</string>
|
||||
<key>MEMIND_PORTAL_TUNNEL_LOCAL_PORT</key><string>${local_port}</string>
|
||||
<key>MEMIND_PORTAL_TUNNEL_REMOTE_PORT</key><string>${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>${HOME}/Library/Logs/memind-portal-tunnel.log</string>
|
||||
<key>StandardErrorPath</key><string>${HOME}/Library/Logs/memind-portal-tunnel.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_TUNNEL_LABEL}" >/dev/null 2>&1 || true
|
||||
launchctl bootstrap "${LAUNCHD_GUI}" "${TUNNEL_PLIST}" >/dev/null
|
||||
launchctl enable "${LAUNCHD_GUI}/${PORTAL_TUNNEL_LABEL}" >/dev/null 2>&1 || true
|
||||
launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_TUNNEL_LABEL}" >/dev/null
|
||||
}
|
||||
|
||||
stop_candidate_services() {
|
||||
launchctl bootout "${LAUNCHD_GUI}/${CANARY_PROXY_LABEL}" >/dev/null 2>&1 || true
|
||||
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null 2>&1 || true
|
||||
"${DOCKER_BIN}" rm -f goosed-prod-canary >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
rollback() {
|
||||
local status=$?
|
||||
trap - ERR
|
||||
say "Canary rollout failed; restoring stable-only tunnel"
|
||||
rm -f "${STABLE_DIR}/.release-drain"
|
||||
write_tunnel_plist 8081 >/dev/null 2>&1 || true
|
||||
stop_candidate_services
|
||||
exit "${status}"
|
||||
}
|
||||
trap rollback ERR
|
||||
|
||||
[[ -f "${BUNDLE}" && -f "${MANIFEST}" && -f "${SHA_FILE}" ]]
|
||||
cd "${INCOMING_DIR}"
|
||||
shasum -a 256 -c "$(basename "${SHA_FILE}")"
|
||||
|
||||
say "Create and verify the full stable backup"
|
||||
COPYFILE_DISABLE=1 tar --exclude='Memind/.tailscale/*.sock' \
|
||||
-czf "${FULL_BACKUP_TAR}" -C "$(dirname "${STABLE_DIR}")" "$(basename "${STABLE_DIR}")"
|
||||
shasum -a 256 "${FULL_BACKUP_TAR}" > "${FULL_BACKUP_TAR}.sha256"
|
||||
shasum -a 256 -c "${FULL_BACKUP_TAR}.sha256"
|
||||
gzip -t "${FULL_BACKUP_TAR}"
|
||||
tar -tzf "${FULL_BACKUP_TAR}" | grep '^Memind/.env$' >/dev/null
|
||||
|
||||
say "Create and verify the persisted-data backup"
|
||||
persist_tmp="$(mktemp -d "${TMPDIR:-/tmp}/memind-canary-persist.XXXXXX")"
|
||||
for item in .env MindSpace data users .tailscale public/plaza-covers logs; do
|
||||
if [[ -e "${STABLE_DIR}/${item}" ]]; then
|
||||
mkdir -p "${persist_tmp}/$(dirname "${item}")"
|
||||
if [[ "${item}" == ".tailscale" ]]; then
|
||||
mkdir -p "${persist_tmp}/${item}"
|
||||
(
|
||||
cd "${STABLE_DIR}/${item}"
|
||||
COPYFILE_DISABLE=1 tar --exclude='*.sock' -cf - .
|
||||
) | (
|
||||
cd "${persist_tmp}/${item}"
|
||||
COPYFILE_DISABLE=1 tar -xf -
|
||||
)
|
||||
else
|
||||
cp -a "${STABLE_DIR}/${item}" "${persist_tmp}/$(dirname "${item}")/"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
COPYFILE_DISABLE=1 tar -czf "${PERSIST_BACKUP_TAR}" -C "${persist_tmp}" .
|
||||
rm -rf "${persist_tmp}"
|
||||
shasum -a 256 "${PERSIST_BACKUP_TAR}" > "${PERSIST_BACKUP_TAR}.sha256"
|
||||
shasum -a 256 -c "${PERSIST_BACKUP_TAR}.sha256"
|
||||
gzip -t "${PERSIST_BACKUP_TAR}"
|
||||
tar -tzf "${PERSIST_BACKUP_TAR}" | grep -E '^(\./)?\.env$' >/dev/null
|
||||
|
||||
if [[ -f "${TUNNEL_PLIST}" ]]; then
|
||||
cp "${TUNNEL_PLIST}" "${TUNNEL_PLIST_BACKUP}"
|
||||
fi
|
||||
|
||||
say "Extract the source-free candidate runtime"
|
||||
rm -rf "${CANDIDATE_DIR}"
|
||||
mkdir -p "${CANDIDATE_DIR}"
|
||||
tar -xzf "${BUNDLE}" -C "${CANDIDATE_DIR}"
|
||||
cp "${MANIFEST}" "${CANDIDATE_DIR}/.release-manifest.txt"
|
||||
chmod 755 \
|
||||
"${CANDIDATE_DIR}/scripts/run-memind-portal-candidate.sh" \
|
||||
"${CANDIDATE_DIR}/scripts/run-memind-canary-proxy-prod.sh"
|
||||
|
||||
say "Stop an older canary without touching stable Portal 8081"
|
||||
if curl -fsS http://127.0.0.1:18080/api/status >/dev/null 2>&1; then
|
||||
write_tunnel_plist 8081
|
||||
sleep 2
|
||||
fi
|
||||
stop_candidate_services
|
||||
|
||||
say "Start an isolated goosed candidate on 18015"
|
||||
(
|
||||
cd "${GOOSED_DIR}"
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
export CANDIDATE_RUNTIME_DIR="${CANDIDATE_DIR}"
|
||||
"${DOCKER_BIN}" compose -p goosed-prod \
|
||||
-f "${GOOSED_COMPOSE}" \
|
||||
-f "${CANDIDATE_DIR}/scripts/goosed-canary.compose.yml" \
|
||||
up -d --no-deps goosed-canary
|
||||
)
|
||||
for _ in $(seq 1 60); do
|
||||
if [[ "$(curl -skS -m 5 https://127.0.0.1:18015/status 2>/dev/null || true)" == "ok" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
[[ "$(curl -skS -m 5 https://127.0.0.1:18015/status 2>/dev/null || true)" == "ok" ]]
|
||||
"${DOCKER_BIN}" exec goosed-prod-canary \
|
||||
sh -lc 'test -x /usr/local/bin/node && test -f /opt/portal/mindspace-sandbox-mcp.mjs'
|
||||
|
||||
say "Start the passive candidate Portal on 18081"
|
||||
cat > "${CANDIDATE_PLIST}" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>${PORTAL_CANDIDATE_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array><string>${CANDIDATE_DIR}/scripts/run-memind-portal-candidate.sh</string></array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>MEMIND_CANARY_STABLE_ROOT</key><string>${STABLE_DIR}</string>
|
||||
<key>MEMIND_CANARY_RELEASE_ID</key><string>${RELEASE_ID}</string>
|
||||
<key>MEMIND_CANARY_CANDIDATE_PORT</key><string>18081</string>
|
||||
<key>MEMIND_CANARY_GOOSED_URL</key><string>https://127.0.0.1:18015</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>ThrottleInterval</key><integer>5</integer>
|
||||
<key>StandardOutPath</key><string>${HOME}/Library/Logs/memind-portal-candidate.log</string>
|
||||
<key>StandardErrorPath</key><string>${HOME}/Library/Logs/memind-portal-candidate.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
launchctl bootstrap "${LAUNCHD_GUI}" "${CANDIDATE_PLIST}" >/dev/null
|
||||
launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null
|
||||
for _ in $(seq 1 60); do
|
||||
candidate_healthy && break
|
||||
sleep 2
|
||||
done
|
||||
candidate_healthy
|
||||
|
||||
say "Validate immutable canary selectors against the production identity database"
|
||||
mkdir -p "$(dirname "${SECRET_FILE}")"
|
||||
if [[ ! -s "${SECRET_FILE}" ]]; then
|
||||
umask 077
|
||||
/usr/bin/openssl rand -hex 32 > "${SECRET_FILE}"
|
||||
fi
|
||||
chmod 600 "${SECRET_FILE}"
|
||||
(
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${STABLE_DIR}/.env"
|
||||
set +a
|
||||
export MEMIND_PORTAL_H5_ROOT="${STABLE_DIR}"
|
||||
export MEMIND_RELEASE_CANARY_USERNAMES="${CANARY_USERNAMES}"
|
||||
export MEMIND_RELEASE_CANARY_WECHAT_USER_IDS="${CANARY_WECHAT_USER_IDS}"
|
||||
/opt/homebrew/opt/node@24/bin/node "${CANDIDATE_DIR}/memind-canary-proxy.mjs" --check
|
||||
)
|
||||
|
||||
say "Start the fail-closed identity router on 18080"
|
||||
cat > "${PROXY_PLIST}" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>${CANARY_PROXY_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array><string>${CANDIDATE_DIR}/scripts/run-memind-canary-proxy-prod.sh</string></array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>MEMIND_CANARY_STABLE_ROOT</key><string>${STABLE_DIR}</string>
|
||||
<key>MEMIND_CANARY_SECRET_FILE</key><string>${SECRET_FILE}</string>
|
||||
<key>MEMIND_RELEASE_CANARY_USERNAMES</key><string>${CANARY_USERNAMES}</string>
|
||||
<key>MEMIND_RELEASE_CANARY_WECHAT_USER_IDS</key><string>${CANARY_WECHAT_USER_IDS}</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>ThrottleInterval</key><integer>5</integer>
|
||||
<key>StandardOutPath</key><string>${HOME}/Library/Logs/memind-canary-proxy.log</string>
|
||||
<key>StandardErrorPath</key><string>${HOME}/Library/Logs/memind-canary-proxy.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
launchctl bootstrap "${LAUNCHD_GUI}" "${PROXY_PLIST}" >/dev/null
|
||||
launchctl kickstart -k "${LAUNCHD_GUI}/${CANARY_PROXY_LABEL}" >/dev/null
|
||||
sleep 2
|
||||
diagnostic_secret="$(tr -d '\r\n' < "${SECRET_FILE}")"
|
||||
diagnostic="$(
|
||||
curl -fsS \
|
||||
-H "X-Memind-Canary-Secret: ${diagnostic_secret}" \
|
||||
http://127.0.0.1:18080/__memind_canary/health
|
||||
)"
|
||||
printf '%s' "${diagnostic}" | /opt/homebrew/opt/node@24/bin/node --input-type=module -e '
|
||||
let input = "";
|
||||
process.stdin.on("data", (chunk) => { input += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
const status = JSON.parse(input);
|
||||
if (!status.ok || !status.stable || !status.candidate) process.exit(1);
|
||||
if (status.policy.usernames < 1 || status.policy.wechatUserIds < 1) process.exit(1);
|
||||
});
|
||||
'
|
||||
for probe_query in 'username=john' 'wechat_user_id=wx_ul610et8'; do
|
||||
route_probe="$(
|
||||
curl -fsS \
|
||||
-H "X-Memind-Canary-Secret: ${diagnostic_secret}" \
|
||||
"http://127.0.0.1:18080/__memind_canary/route-probe?${probe_query}"
|
||||
)"
|
||||
printf '%s' "${route_probe}" | /opt/homebrew/opt/node@24/bin/node --input-type=module -e '
|
||||
let input = "";
|
||||
process.stdin.on("data", (chunk) => { input += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
const status = JSON.parse(input);
|
||||
if (status.configuredTarget !== "candidate") process.exit(1);
|
||||
if (status.candidate !== true || status.route !== "candidate") process.exit(1);
|
||||
});
|
||||
'
|
||||
done
|
||||
|
||||
say "Drain active Agent runs for the bounded tunnel switch"
|
||||
touch "${STABLE_DIR}/.release-drain"
|
||||
deadline=$(( $(date +%s) + 120 ))
|
||||
while (( $(date +%s) < deadline )); do
|
||||
queue="$(
|
||||
/opt/homebrew/opt/node@24/bin/node \
|
||||
"${STABLE_DIR}/scripts/agent-run-worker.mjs" --status 2>/dev/null || true
|
||||
)"
|
||||
counts="$(
|
||||
printf '%s' "${queue}" | /opt/homebrew/opt/node@24/bin/node --input-type=module -e '
|
||||
let input = "";
|
||||
process.stdin.on("data", (chunk) => { input += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
const queue = JSON.parse(input).queue || {};
|
||||
console.log(`${Number(queue.inFlight || 0)} ${Number(queue.pendingDispatches || 0)}`);
|
||||
} catch {
|
||||
console.log("unknown unknown");
|
||||
}
|
||||
});
|
||||
'
|
||||
)"
|
||||
if [[ "${counts}" == "0 0" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
[[ "${counts:-unknown unknown}" == "0 0" ]]
|
||||
|
||||
say "Switch only the reverse tunnel from stable 8081 to identity router 18080"
|
||||
write_tunnel_plist 18080
|
||||
sleep 2
|
||||
remote_code="$(
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${MEMIND_PORTAL_TUNNEL_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}/api/status" \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
[[ "${remote_code}" == "200" ]]
|
||||
rm -f "${STABLE_DIR}/.release-drain"
|
||||
|
||||
say "Verify candidate failure automatically falls back to stable"
|
||||
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
if ! curl -fsS http://127.0.0.1:18081/api/status >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fallback_probe="$(
|
||||
curl -fsS \
|
||||
-H "X-Memind-Canary-Secret: ${diagnostic_secret}" \
|
||||
'http://127.0.0.1:18080/__memind_canary/route-probe?username=john'
|
||||
)"
|
||||
printf '%s' "${fallback_probe}" | /opt/homebrew/opt/node@24/bin/node --input-type=module -e '
|
||||
let input = "";
|
||||
process.stdin.on("data", (chunk) => { input += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
const status = JSON.parse(input);
|
||||
if (status.configuredTarget !== "candidate") process.exit(1);
|
||||
if (status.candidate !== false || status.route !== "stable") process.exit(1);
|
||||
});
|
||||
'
|
||||
launchctl bootstrap "${LAUNCHD_GUI}" "${CANDIDATE_PLIST}" >/dev/null
|
||||
launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null
|
||||
for _ in $(seq 1 60); do
|
||||
candidate_healthy && break
|
||||
sleep 2
|
||||
done
|
||||
candidate_healthy
|
||||
final_diagnostic="$(
|
||||
curl -fsS \
|
||||
-H "X-Memind-Canary-Secret: ${diagnostic_secret}" \
|
||||
http://127.0.0.1:18080/__memind_canary/health
|
||||
)"
|
||||
printf '%s' "${final_diagnostic}" | /opt/homebrew/opt/node@24/bin/node --input-type=module -e '
|
||||
let input = "";
|
||||
process.stdin.on("data", (chunk) => { input += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
const status = JSON.parse(input);
|
||||
if (!status.ok || !status.stable || !status.candidate) process.exit(1);
|
||||
});
|
||||
'
|
||||
|
||||
trap - ERR
|
||||
say "103 production canary is active"
|
||||
printf 'release_id=%s\n' "${RELEASE_ID}"
|
||||
printf 'candidate_dir=%s\n' "${CANDIDATE_DIR}"
|
||||
printf 'full_backup=%s\n' "${FULL_BACKUP_TAR}"
|
||||
printf 'persist_backup=%s\n' "${PERSIST_BACKUP_TAR}"
|
||||
printf 'stable_health=http://127.0.0.1:8081/api/status\n'
|
||||
printf 'proxy_health=http://127.0.0.1:18080/__memind_canary/health\n'
|
||||
printf 'candidate_health=http://127.0.0.1:18081/api/status\n'
|
||||
printf 'candidate_goosed=https://127.0.0.1:18015/status\n'
|
||||
REMOTE_SCRIPT
|
||||
|
||||
say "103 canary release completed"
|
||||
printf 'release_id=%s\n' "${RELEASE_ID}"
|
||||
printf 'git_head=%s\n' "${FULL_SHA}"
|
||||
@@ -84,8 +84,9 @@ if [[ "${ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES:-0}" == "1" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" -eq 0 ]]; then
|
||||
echo "当前脚本执行整包替换,不是用户级灰度入口;在候选 runtime + 灰度代理上线前禁止生产发布。" >&2
|
||||
echo "请先使用 docs/release-canary-103.md 规定的灰度流程,并由守门员重新授权。" >&2
|
||||
echo "当前脚本执行 8081 整包晋升,不是用户级灰度入口。" >&2
|
||||
echo "在同一候选完成 103 灰度验收且晋升证据校验落地前,禁止非 dry-run。" >&2
|
||||
echo "首次生产动作请使用 scripts/release-portal-canary-prod.sh。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -165,7 +166,7 @@ fi
|
||||
|
||||
verify_runtime_artifact() {
|
||||
local missing=0
|
||||
for required in server.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs tkmind-search-mcp.mjs tkmind-excel-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
|
||||
for required in server.mjs memind-canary-proxy.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs tkmind-search-mcp.mjs tkmind-excel-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/run-memind-portal-candidate.sh scripts/run-memind-canary-proxy-prod.sh scripts/goosed-canary.compose.yml scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
|
||||
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
|
||||
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
|
||||
missing=1
|
||||
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${STUDIO_HOST:-58.38.22.103}"
|
||||
STABLE_DIR="${STUDIO_REMOTE_ROOT:-/Users/john/Project}/Memind"
|
||||
PORTAL_CANDIDATE_LABEL="cn.tkmind.memind-portal-candidate"
|
||||
CANARY_PROXY_LABEL="cn.tkmind.memind-canary-proxy"
|
||||
PORTAL_TUNNEL_LABEL="cn.tkmind.memind-portal-tunnel"
|
||||
MEMIND_PORTAL_TUNNEL_HOST="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}"
|
||||
MEMIND_PORTAL_TUNNEL_REMOTE_PORT="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}"
|
||||
AUTO_YES=0
|
||||
|
||||
if [[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]]; then
|
||||
AUTO_YES=1
|
||||
elif [[ $# -gt 0 ]]; then
|
||||
echo "Usage: bash scripts/rollback-portal-canary-prod.sh [--yes]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${AUTO_YES}" -ne 1 ]]; then
|
||||
echo "This restores the 105 tunnel to stable Portal 8081 and stops only canary services."
|
||||
read -r -p "Continue with 103 canary rollback? [y/N] " confirm </dev/tty
|
||||
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
|
||||
fi
|
||||
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" \
|
||||
"STABLE_DIR='${STABLE_DIR}' \
|
||||
PORTAL_CANDIDATE_LABEL='${PORTAL_CANDIDATE_LABEL}' \
|
||||
CANARY_PROXY_LABEL='${CANARY_PROXY_LABEL}' \
|
||||
PORTAL_TUNNEL_LABEL='${PORTAL_TUNNEL_LABEL}' \
|
||||
MEMIND_PORTAL_TUNNEL_HOST='${MEMIND_PORTAL_TUNNEL_HOST}' \
|
||||
MEMIND_PORTAL_TUNNEL_REMOTE_PORT='${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}' \
|
||||
/bin/bash" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
LAUNCHD_GUI="gui/$(id -u)"
|
||||
TUNNEL_PLIST="${HOME}/Library/LaunchAgents/${PORTAL_TUNNEL_LABEL}.plist"
|
||||
|
||||
cat > "${TUNNEL_PLIST}" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>${PORTAL_TUNNEL_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array><string>${STABLE_DIR}/scripts/memind-portal-tunnel.sh</string></array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>MEMIND_PORTAL_TUNNEL_HOST</key><string>${MEMIND_PORTAL_TUNNEL_HOST}</string>
|
||||
<key>MEMIND_PORTAL_TUNNEL_LOCAL_PORT</key><string>8081</string>
|
||||
<key>MEMIND_PORTAL_TUNNEL_REMOTE_PORT</key><string>${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>${HOME}/Library/Logs/memind-portal-tunnel.log</string>
|
||||
<key>StandardErrorPath</key><string>${HOME}/Library/Logs/memind-portal-tunnel.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_TUNNEL_LABEL}" >/dev/null 2>&1 || true
|
||||
launchctl bootstrap "${LAUNCHD_GUI}" "${TUNNEL_PLIST}" >/dev/null
|
||||
launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_TUNNEL_LABEL}" >/dev/null
|
||||
launchctl bootout "${LAUNCHD_GUI}/${CANARY_PROXY_LABEL}" >/dev/null 2>&1 || true
|
||||
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null 2>&1 || true
|
||||
/opt/homebrew/bin/docker rm -f goosed-prod-canary >/dev/null 2>&1 || true
|
||||
rm -f "${STABLE_DIR}/.release-drain"
|
||||
|
||||
curl -fsS http://127.0.0.1:8081/api/status >/dev/null
|
||||
sleep 2
|
||||
remote_code="$(
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${MEMIND_PORTAL_TUNNEL_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}/api/status" \
|
||||
2>/dev/null || true
|
||||
)"
|
||||
[[ "${remote_code}" == "200" ]]
|
||||
printf 'stable_port=8081\n'
|
||||
printf 'remote_tunnel_status=%s\n' "${remote_code}"
|
||||
REMOTE
|
||||
|
||||
echo "103 canary rollback completed; stable Portal remains active on 8081."
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STABLE_ROOT="${MEMIND_CANARY_STABLE_ROOT:-/Users/john/Project/Memind}"
|
||||
SECRET_FILE="${MEMIND_CANARY_SECRET_FILE:-${HOME}/.config/memind/canary-router.secret}"
|
||||
|
||||
if [[ ! -d "${STABLE_ROOT}" || ! -f "${STABLE_ROOT}/.env" ]]; then
|
||||
echo "[canary-proxy] stable persistent root is unavailable: ${STABLE_ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -r "${SECRET_FILE}" ]]; then
|
||||
echo "[canary-proxy] diagnostic secret file is unavailable: ${SECRET_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${STABLE_ROOT}/.env"
|
||||
set +a
|
||||
|
||||
export NODE_ENV=production
|
||||
export MEMIND_PORTAL_H5_ROOT="${STABLE_ROOT}"
|
||||
export MEMIND_CANARY_DIAGNOSTIC_SECRET="$(tr -d '\r\n' < "${SECRET_FILE}")"
|
||||
export MEMIND_CANARY_PROXY_HOST=127.0.0.1
|
||||
export MEMIND_CANARY_PROXY_PORT="${MEMIND_CANARY_PROXY_PORT:-18080}"
|
||||
export MEMIND_CANARY_STABLE_URL="${MEMIND_CANARY_STABLE_URL:-http://127.0.0.1:8081}"
|
||||
export MEMIND_CANARY_CANDIDATE_URL="${MEMIND_CANARY_CANDIDATE_URL:-http://127.0.0.1:18081}"
|
||||
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
if [[ ! -x "${NODE_BIN}" ]]; then
|
||||
NODE_BIN="$(command -v node)"
|
||||
fi
|
||||
|
||||
exec "${NODE_BIN}" "${ROOT}/memind-canary-proxy.mjs"
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STABLE_ROOT="${MEMIND_CANARY_STABLE_ROOT:-/Users/john/Project/Memind}"
|
||||
|
||||
if [[ ! -d "${STABLE_ROOT}" || ! -f "${STABLE_ROOT}/.env" ]]; then
|
||||
echo "[portal-candidate] stable persistent root is unavailable: ${STABLE_ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${STABLE_ROOT}/.env"
|
||||
set +a
|
||||
|
||||
export NODE_ENV=production
|
||||
export MEMIND_PORTAL_H5_ROOT="${STABLE_ROOT}"
|
||||
export MEMIND_RUNTIME_ROLE=candidate
|
||||
export MEMIND_CANARY_PASSIVE_RUNTIME=1
|
||||
export MEMIND_RUNTIME_BUILD_ID="${MEMIND_CANARY_RELEASE_ID:?MEMIND_CANARY_RELEASE_ID is required}"
|
||||
export MEMIND_AGENT_RUN_WORKER_ID="portal-candidate-${MEMIND_RUNTIME_BUILD_ID}"
|
||||
export MEMIND_WORKSPACE_MAINTENANCE=0
|
||||
export H5_REMINDER_WORKER_ENABLED=0
|
||||
export H5_PORT="${MEMIND_CANARY_CANDIDATE_PORT:-18081}"
|
||||
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}"
|
||||
unset TKMIND_API_TARGET_1
|
||||
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
if [[ ! -x "${NODE_BIN}" ]]; then
|
||||
NODE_BIN="$(command -v node)"
|
||||
fi
|
||||
|
||||
port="${H5_PORT}"
|
||||
if lsof -tiTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
echo "[portal-candidate] port ${port} is already in use; refusing to replace it" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "${NODE_BIN}" "${ROOT}/server.mjs"
|
||||
Reference in New Issue
Block a user