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:
@@ -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"
|
||||
# 必须 201,hasThumbnail: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,交给用户决定是否清理)
|
||||
- 是否发现并处理了问题;如果发布后发现回滚,说明触发原因
|
||||
Reference in New Issue
Block a user