docs: add portal-release skill for package/deploy/verify/rollback

Codifies the 103 production release workflow used for the
0701bug003 deploy: doc-mandated pre-release tests, build + dry-run,
release-portal-runtime-prod.sh, full post-deploy verification
(health, /auth/login, all 4 goosed ports, log keyword scan, real
API-driven page-create smoke test), and both a preferred rollback
(redeploy previous good commit) and an emergency manual rollback path.

Passwords are taken from env vars (JOHN_PASSWORD/H5_ACCESS_PASSWORD),
matching the existing scripts/fruit-theme-john-e2e.mjs convention —
never hardcoded in the skill file.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
john
2026-07-01 15:21:55 +08:00
parent 27960a3762
commit 7f0b216775
+170
View File
@@ -0,0 +1,170 @@
---
name: portal-release
description: Package, deploy, verify, and roll back the Memind Portal runtime on 103 production. Use for any "打包/发布/上线/回滚/发布验证" request targeting 103.
---
# Portal 生产发布 / 验证 / 回滚
103(`john@58.38.22.103`)是唯一生产 Portal 主机。这个 skill 是打包、发布、验证、回滚的**唯一入口**——不要现场发明新流程,不要直接 SSH 到 103 手改代码或手写文件到 `MindSpace/`
## 0. 发布前置检查(必须)
```bash
git status --short
```
**worktree 必须干净。** 如果这个仓库有并发运行的其他 agent/session(本仓库经常有),工作区里可能混着别人未提交的改动:
- 先辨认清楚哪些文件是"这次要发布的改动",哪些是别人正在写的无关改动
- **不要**未经确认就自己 stash 别人的未提交内容——这是需要用户明确同意的动作,先问,拿到同意后再 `git stash push -u -m "<说明>" -- <具体文件...>`
- 发布完成后必须 `git stash pop` 把对方的改动还回去,不要留着不还
确认当前分支和要发布的 commit 就是打算上线的那个(`git log --oneline -3`)。
## 1. 发布前测试(必须,对应 docs/发包必看.md #1.5
```bash
node --test db.test.mjs capabilities.test.mjs llm-providers.test.mjs wechat-mp.test.mjs
```
全绿之后才能进入打包发布,不要跳过。
## 2. 打包 + 发布(唯一入口)
先 dry-run 确认构建产物正常(不会碰生产):
```bash
bash scripts/release-portal-runtime-prod.sh --skip-tests --dry-run
```
确认无误后正式发布(`--skip-tests` 是因为第 1 步已经手动跑过;这里不再重复跑脚本内置的窄范围测试):
```bash
bash scripts/release-portal-runtime-prod.sh --skip-tests --yes
```
这个脚本会自动完成:
1. 本机构建 `.runtime/portal`
2. 上传到 103
3. **103 备份当前 `/Users/john/Project/Memind` 全目录**`full_backup`
4. **103 单独备份持久目录**`.env``MindSpace/``data/``users/``.tailscale/``public/plaza-covers/``logs/``persist_backup`
5. 原子切换 live 目录(旧源码树移入 `archived_source`,不是删除)
6. 更新 LaunchAgent,健康检查 `http://127.0.0.1:8081/api/status` 200
7. 脚本内置 `trap rollback ERR`:如果切换过程中途失败,会自动尝试把旧目录移回来并重启旧服务——但**这只覆盖脚本执行期间的失败**,脚本成功退出之后发现问题需要走第 4 节手动回滚
**记录脚本输出里的这几行,回滚和报告都要用**
```
release_id=...
archived_source=/Users/john/Project/archives/Memind-source-before-<release_id>
full_backup=/Users/john/Project/backups/memind/memind-full-<release_id>-before.tar.gz
persist_backup=/Users/john/Project/backups/memind/memind-persisted-<release_id>-before.tar.gz
```
## 3. 发布后验证(必须,不能只看 /api/status
### 3.1 健康检查 + auth/login(本机执行)
```bash
curl -k -i https://m.tkmind.cn/auth/login \
-H 'content-type: application/json' \
--data '{"username":"john","password":"wrong-password"}' | head -20
```
必须是 **401** + `用户名或密码错误`。如果是 `503` + `未配置用户数据库或访问密码`,说明用户系统 bootstrap 失败——不是简单健康检查能发现的问题,必须当场处理,不能算发布成功。
### 3.2 Portal + 四个 goosed 实例(SSH 103
```bash
ssh john@58.38.22.103 '
printf "portal="; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8081/api/status
for p in 18006 18007 18008 18009; do
printf "goosed_${p}="
curl -k -s -o /dev/null -w "%{http_code}\n" https://127.0.0.1:${p}/status
done
pid=$(pgrep -f "node .*server.mjs" | head -1)
echo "portal_pid=${pid}"
ps eww -p "$pid" | tr " " "\n" | awk -F= "/^(DATABASE_URL|H5_PORT|TKMIND_API_TARGETS|GOOSED_MCP_NODE_PATH|GOOSED_MCP_SERVER_PATH)=/ {print \$1\"=SET\"}"
'
```
必须全部是 `200`,五个 env var 全部 `SET`
### 3.3 日志关键词检查(SSH 103
```bash
ssh john@58.38.22.103 '
tail -200 ~/Library/Logs/memind-portal.log |
egrep "User auth bootstrap failed|ER_PARSE_ERROR|Cannot find package|Failed to add extension|No such file|Unknown extension|Provider not set|not configured" || echo "no error keywords found"
'
```
新启动之后不能再出现这些关键词。
### 3.4 真实业务路径验证(必须,测试账号 `john`)
只看健康接口不算数,必须走一次真实的"生成页面 → 发布 → 能访问"链路。**禁止直接 SSH 到 103 手写文件到 `MindSpace/` 目录充当验证**——那是绕过应用本身的路径,不代表真实用户会走的流程,而且是对生产文件系统的越权写入。只能通过应用自己的 API。
密码通过环境变量传入,**不要把明文密码写进这个文件或提交到仓库**(沿用 `scripts/fruit-theme-john-e2e.mjs` 里的既有约定:`JOHN_PASSWORD` / `H5_ACCESS_PASSWORD`,向用户当场要,或从 `.env` 之外的安全渠道取):
```bash
CJ=/tmp/portal-release-verify-cookies.txt
# 1. 登录(JOHN_PASSWORD 从环境变量取,不要硬编码)
curl -k -s -c "$CJ" -X POST https://m.tkmind.cn/auth/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"john\",\"password\":\"${JOHN_PASSWORD}\"}"
# 必须 authenticated:true
# 2. 通过真实 API 创建一个页面(验证 mindspace pages 管线本身没坏)
BODY=$(node -e "console.log(JSON.stringify({title:'发布验证页',summary:'release verify',content:'<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>发布验证页</title><meta name=\"mindspace-cover\" content=\'{\"tag\":\"报告\"}\'></head><body>ok</body></html>',content_format:'html',page_type:'html',category_code:'draft'}))")
curl -k -s -X POST https://m.tkmind.cn/api/mindspace/v1/pages \
-H "Content-Type: application/json" -b "$CJ" -c "$CJ" -d "$BODY"
# 必须 201hasThumbnail:true
# 3. 只读验证公网静态页服务没坏:用一个已知存在的已发布页面,不要新写文件
curl -k -s -o /dev/null -w '%{http_code}\n' \
"https://m.tkmind.cn/MindSpace/<已知已发布用户ID>/public/<已知文件名>.html"
# 必须 200
```
第 2 步创建的测试页面记录会留在生产库里(`draft`/`private`,风险很低)。**不要自己调用 DELETE 清理**——删除生产数据记录需要用户明确同意,验证完之后把 page id 报给用户,让用户决定是否清理。
## 4. 手动回滚(脚本成功退出之后才发现问题时用)
**优先方式:重新发布上一个已知good的 commit**,而不是手动倒腾备份目录——这样能完整复用第 2 节的持久化保留逻辑,不会把回滚期间用户新写的 `MindSpace`/`data`/`users` 数据搞丢:
```bash
git log --oneline -5 # 找到上一个已验证通过的 commit
git checkout <上一个好的 commit 或分支>
bash scripts/release-portal-runtime-prod.sh --skip-tests --yes
# 然后重新走第 3 节完整验证
```
**紧急方式**(本地仓库回不去、必须直接用远端备份时):
```bash
ssh john@58.38.22.103 '
launchctl bootout gui/$(id -u)/cn.tkmind.memind-portal 2>/dev/null || true
lsof -tiTCP:8081 -sTCP:LISTEN | xargs kill 2>/dev/null || true
mv /Users/john/Project/Memind /Users/john/Project/Memind-bad-release-$(date +%s)
mv <archived_source 路径> /Users/john/Project/Memind
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/cn.tkmind.memind-portal.plist
launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal
'
```
⚠️ `archived_source` 里的 `MindSpace`/`data`/`users` 是**发布那一刻**的快照。如果坏版本上线后已经有用户写入了新数据,直接整体换回去会丢失这段时间的新数据——先确认这一点,必要时把坏版本目录里当前的 `MindSpace`/`data`/`users` 复制出来,回滚后再合并回去,而不是整体覆盖。回滚后必须重新走第 3 节验证。
## 5. 发布完成后的最小回报格式(必须,不能只说"已发布成功")
- release id / git head / git branch
- full backup 路径
- persisted backup 路径
- Portal 健康结果(200/非200
- `/auth/login` 结果(401 + 正确消息,还是别的)
- 18006-18009 健康结果
- 日志关键词检查结果
- 第 3.4 节真实业务路径验证结果(含遗留的测试 page id,交给用户决定是否清理)
- 是否发现并处理了问题;如果发布后发现回滚,说明触发原因