feat(guard): 部署一致性守卫 deploy_guard.py + 知微运维纪律 + 健康JSON移出跟踪
- deploy_guard.py (15min cron): 代码漂移自动回滚(仅未提交改动)+ session-work可快进时自动merge部署+幂等重链+cron引用完整性, 状态落盘JSON, 有动作即XMPP报备 - docs/zhiwei-ops-discipline.md: 知微纪律——禁止直接编辑被跟踪代码/ 禁stale提交/禁直调gateway批量LLM; 自愈白名单(rerun/restart/ sync/switch_key)本不改代码, 与纪律不矛盾; 紧急热修走git流程, 提交到session-work后guard 15min自动部署 - dev-spec 红线#6 补充部署守卫机制 - static/mofin_health.json 移出git跟踪(运行时产物, 常驻dirty 会废掉漂移检测)
This commit is contained in:
@@ -28,3 +28,5 @@ data/stock_analysis.db
|
||||
gateway/logs/
|
||||
gateway/temp/
|
||||
index.html
|
||||
|
||||
static/mofin_health.json
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy_guard.py — 部署一致性守卫(每 15 分钟 cron,全天候)
|
||||
|
||||
原则:实际部署的脚本 = MoFin git 最新版代码。
|
||||
|
||||
五件事:
|
||||
1. 代码漂移检测:git 工作区中【被跟踪的代码文件】出现未提交改动
|
||||
→ 自动 git checkout 回滚 + 重链硬链接 + XMPP 告警
|
||||
(只回滚未提交的工作区改动;git 提交永远安全)
|
||||
2. 上游自动部署:session-work 分支领先 master 且可快进
|
||||
→ 自动 merge + 重链 + 必要时重启 dashboard + 告警
|
||||
3. 硬链接一致性:幂等执行 sync_profile_scripts.sh
|
||||
4. cron 引用完整性:jobs.json 引用的脚本必须存在
|
||||
5. 状态落盘:gateway/logs/deploy_guard_status.json + deploy_guard.log
|
||||
(有动作/有问题才发 XMPP,沉默即正常)
|
||||
"""
|
||||
import json, os, subprocess, sys, glob
|
||||
from datetime import datetime
|
||||
|
||||
REPO = "/home/hmo/MoFin"
|
||||
CANONICAL = f"{REPO}/deploy/profile-scripts"
|
||||
PROFILE_DIRS = [
|
||||
"/home/hmo/.hermes/profiles/position-analyst/scripts",
|
||||
"/home/hmo/.hermes/profiles/default/scripts",
|
||||
]
|
||||
LOG = f"{REPO}/gateway/logs/deploy_guard.log"
|
||||
STATUS_JSON = f"{REPO}/gateway/logs/deploy_guard_status.json"
|
||||
SYNC_SCRIPT = f"{CANONICAL}/sync_profile_scripts.sh"
|
||||
|
||||
# 守卫范围:被 git 跟踪的【代码】路径(运行时产物 data/ logs/ static/*.json 不在其列)
|
||||
CODE_PATHS = [
|
||||
"deploy/profile-scripts", "deploy/bot", "prompt_manager",
|
||||
"server.py", "mofin_db.py", "strategy_lifecycle.py", "xmpp_logger.py",
|
||||
"mo_config.py", "scripts/mo_data.py",
|
||||
"static/index.html", "static/mofin_health.html",
|
||||
]
|
||||
|
||||
actions = [] # 采取了的动作(需告警)
|
||||
problems = [] # 发现的问题(需告警)
|
||||
|
||||
|
||||
def log(msg):
|
||||
line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
|
||||
print(line, flush=True)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(LOG), exist_ok=True)
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def xmpp(msg):
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(
|
||||
"http://127.0.0.1:5805/",
|
||||
data=json.dumps({"body": msg, "to": "hmo@yoin.fun", "type": "chat"}).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
urllib.request.urlopen(req, timeout=5)
|
||||
except Exception as e:
|
||||
log(f"XMPP发送失败: {e}")
|
||||
|
||||
|
||||
def git(*args, check=True):
|
||||
r = subprocess.run(["git", "-C", REPO] + list(args),
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if check and r.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args)}: {r.stderr.strip()[:200]}")
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
# ── 1. 代码漂移检测与回滚 ──
|
||||
def check_code_drift():
|
||||
out = git("status", "--porcelain", "--", *CODE_PATHS)
|
||||
if not out:
|
||||
return
|
||||
modified, untracked = [], []
|
||||
for line in out.splitlines():
|
||||
status, path = line[:2], line[3:].strip()
|
||||
if status == "??":
|
||||
untracked.append(path)
|
||||
else:
|
||||
modified.append(path)
|
||||
if modified:
|
||||
diff_stat = git("diff", "--stat", "--", *modified, check=False)
|
||||
log(f"DRIFT: 检测到 {len(modified)} 个代码文件未入库改动: {modified}")
|
||||
for f in modified:
|
||||
git("checkout", "--", f)
|
||||
subprocess.run(["bash", SYNC_SCRIPT], capture_output=True, timeout=120)
|
||||
actions.append(f"回滚未入库代码改动 {len(modified)} 个: {', '.join(modified[:5])}"
|
||||
+ (f" 等(共{len(modified)}个)" if len(modified) > 5 else ""))
|
||||
log(f"DRIFT_REVERTED: {modified}\n{diff_stat[:500]}")
|
||||
if untracked:
|
||||
# 未跟踪的新文件:不删(可能是在途工作),只告警
|
||||
problems.append(f"权威目录出现未跟踪文件(需人工确认): {', '.join(untracked[:5])}")
|
||||
log(f"UNTRACKED: {untracked}")
|
||||
|
||||
|
||||
# ── 2. session-work → master 自动部署 ──
|
||||
def check_upstream_deploy():
|
||||
try:
|
||||
git("rev-parse", "--verify", "session-work")
|
||||
except Exception:
|
||||
return # 无此分支
|
||||
ahead = git("rev-list", "--count", "master..session-work", check=False)
|
||||
if not ahead.isdigit() or int(ahead) == 0:
|
||||
return
|
||||
# 仅当可快进(master 是 session-work 祖先)才自动合并
|
||||
r = subprocess.run(["git", "-C", REPO, "merge-base", "--is-ancestor", "master", "session-work"])
|
||||
if r.returncode != 0:
|
||||
problems.append(f"session-work 领先 master {ahead} 个提交但存在分叉,需人工合并")
|
||||
log(f"UPSTREAM_DIVERGED: ahead={ahead}")
|
||||
return
|
||||
log(f"UPSTREAM: session-work 领先 {ahead} 个提交,自动合并部署")
|
||||
merge_out = git("merge", "session-work", "--no-edit")
|
||||
sync_out = subprocess.run(["bash", SYNC_SCRIPT], capture_output=True, text=True, timeout=120)
|
||||
# 触及 dashboard 代码则重启
|
||||
touched = git("diff", "--name-only", f"HEAD~{ahead}", "HEAD", check=False)
|
||||
restarted = ""
|
||||
if any(t.startswith(("server.py", "static/")) for t in touched.splitlines()):
|
||||
rr = subprocess.run(["sudo", "-n", "systemctl", "restart", "mofin-dashboard"],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
restarted = " + dashboard已重启" if rr.returncode == 0 else " (dashboard重启失败,需人工)"
|
||||
actions.append(f"自动部署 session-work→master ({ahead}个提交){restarted}")
|
||||
log(f"UPSTREAM_DEPLOYED: {merge_out[:200]} | sync: {sync_out.stdout.strip()[-100:]}")
|
||||
|
||||
|
||||
# ── 3. 硬链接一致性 ──
|
||||
def check_hardlinks():
|
||||
r = subprocess.run(["bash", SYNC_SCRIPT], capture_output=True, text=True, timeout=120)
|
||||
tail = (r.stdout or "").strip().splitlines()
|
||||
if tail:
|
||||
log(f"SYNC: {tail[-1]}")
|
||||
if r.returncode != 0:
|
||||
problems.append(f"sync_profile_scripts.sh 执行失败: {(r.stderr or '')[:150]}")
|
||||
|
||||
|
||||
# ── 4. cron 引用完整性 ──
|
||||
def check_cron_refs():
|
||||
missing = []
|
||||
for pj in glob.glob("/home/hmo/.hermes/profiles/*/cron/jobs.json"):
|
||||
profile = pj.split("/")[-3]
|
||||
try:
|
||||
with open(pj, encoding="utf-8") as f:
|
||||
jobs = json.load(f)
|
||||
jobs = jobs if isinstance(jobs, list) else jobs.get("jobs", [])
|
||||
except Exception:
|
||||
continue
|
||||
for j in jobs:
|
||||
if not j.get("enabled", True):
|
||||
continue
|
||||
script = j.get("script") or ""
|
||||
if not script:
|
||||
continue
|
||||
base = os.path.basename(script)
|
||||
if not any(os.path.exists(os.path.join(d, base)) for d in PROFILE_DIRS + [CANONICAL]):
|
||||
missing.append(f"[{profile}]{j.get('name','?')}→{base}")
|
||||
if missing:
|
||||
problems.append(f"cron引用缺失脚本 {len(missing)} 个: {', '.join(missing[:5])}")
|
||||
log(f"CRON_MISSING: {missing}")
|
||||
|
||||
|
||||
def main():
|
||||
log("── deploy_guard 开始 ──")
|
||||
for fn in (check_code_drift, check_upstream_deploy, check_hardlinks, check_cron_refs):
|
||||
try:
|
||||
fn()
|
||||
except Exception as e:
|
||||
problems.append(f"{fn.__name__} 异常: {str(e)[:150]}")
|
||||
log(f"ERROR in {fn.__name__}: {e}")
|
||||
|
||||
status = {
|
||||
"ts": datetime.now().isoformat(timespec="seconds"),
|
||||
"ok": not problems,
|
||||
"actions": actions,
|
||||
"problems": problems,
|
||||
}
|
||||
try:
|
||||
with open(STATUS_JSON, "w", encoding="utf-8") as f:
|
||||
json.dump(status, f, ensure_ascii=False, indent=1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if actions or problems:
|
||||
msg = "🛡️ 部署守卫\n"
|
||||
for a in actions:
|
||||
msg += f"✅ {a}\n"
|
||||
for p in problems:
|
||||
msg += f"⚠️ {p}\n"
|
||||
xmpp(msg.strip())
|
||||
log(f"── 结束: actions={len(actions)} problems={len(problems)} ──")
|
||||
return 0 if not problems else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -18,6 +18,7 @@
|
||||
- **git hooks**(`.git/hooks/post-merge` + `post-checkout`):git 操作后自动重链
|
||||
- **手动兜底**:`bash deploy/profile-scripts/sync_profile_scripts.sh`(改完文件随手跑)
|
||||
- 同步日志:`gateway/logs/link_sync.log`;断链检测同时是 L2 卫生审计的检查项
|
||||
- **部署守卫**(`deploy_guard.py`,每 15 分钟全天候):①被跟踪代码文件出现未提交改动 → 自动回滚+重链+告警(git 提交不受影响)②`session-work` 领先 master 且可快进 → 自动 merge+重链+(触及 dashboard 时)重启服务 ③幂等重链 ④cron 引用完整性检查。状态落盘 `gateway/logs/deploy_guard_status.json`,有动作即 XMPP 报备。**禁止直接编辑 246 上任何被跟踪的代码文件**——所有改动必须经 git(详见 `docs/zhiwei-ops-discipline.md`)
|
||||
7. **数据路径必须绝对** — 引用数据文件/数据库时,必须写绝对路径并指向权威位置(`/home/hmo/MoFin/data/`)。**禁止**用 `Path(__file__).parent / "data"` 这类相对解析——同一个模块被硬链接到不同位置时会解析出不同的数据库(2026-07-20 三库事件的根因)
|
||||
8. **备份/遗留物禁止留在生产数据目录** — `.bak`、`decisions_backup_*`、迁移残留 JSON、废弃 DB,必须在迁移/变更完成时移到 `archive/`。生产数据目录(`MoFin/data` = `web-dashboard/data`)只放活文件。监控脚本扫描生产目录时,遗留物就是未来的假警报
|
||||
9. **死模块必须收尸** — 宣布模块废弃时,必须在同一轮操作中完成收尸六步:①杀进程 ②stop+disable systemd 服务 ③删 cron job ④归档脚本到 `archive/` ⑤归档数据文件 ⑥从期望矩阵/监控中移除。只说"已废弃"不收尸 = 没废弃(小果 bot 以 root 白跑 8 天 2.5GB 的教训)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 知微运维纪律(2026-07-21 生效)
|
||||
|
||||
> 由小小莫(Sisyphus)与老爸共同制定。核心原则:**实际部署的脚本 = MoFin git 最新版代码**。
|
||||
> 违反本纪律的改动会被 `deploy_guard.py`(每 15 分钟)自动回滚并告警。
|
||||
|
||||
---
|
||||
|
||||
## 一、绝对禁止
|
||||
|
||||
1. **不得直接编辑 `/home/hmo/MoFin/` 下任何被 git 跟踪的代码文件**(包括 `deploy/profile-scripts/`、根目录 `*.py`、`server.py`、`static/*.html`、`prompt_manager/`)。部署守卫每 15 分钟扫描,**未提交的工作区改动一律自动 `git checkout` 回滚**,并通过 XMPP 告警
|
||||
2. **不得直接编辑 `/home/hmo/.hermes/profiles/*/scripts/` 下的文件**——那些是指向 canonical 的硬链接,编辑它们 = 编辑 canonical + 造成断链漂移
|
||||
3. **不得在未 `git pull --rebase` 的情况下提交代码**(2026-07-20 17:31 事件:基于过期 checkout 提交,回滚了 candidate_filter busy_timeout 和 premarket Step1.5 两处修复)
|
||||
4. **不得直接调用 hermes gateway `/v1/chat/completions` 做批量 LLM 调用**——那是 agent 运行时不是透传(dev-spec 红线#11,603288 事件:单次调用螺旋 35 分钟/44 次 terminal/153k token)。批量调用一律用 `llm_client.call_llm()`(OCG 直连)
|
||||
|
||||
## 二、自愈系统如何在纪律下运作(不矛盾)
|
||||
|
||||
L3 自愈(self_repair.py)的动作白名单**本来就不改代码**,guard 不会干预:
|
||||
|
||||
| 白名单动作 | 性质 | guard 态度 |
|
||||
|---|---|---|
|
||||
| `rerun_script` | 重跑脚本 | ✅ 允许 |
|
||||
| `restart_service` | 重启 systemd 服务 | ✅ 允许 |
|
||||
| `sync_links` | 重链硬链接 | ✅ 允许(与 guard 同向) |
|
||||
| `switch_llm_key` | 切换 LLM key(改 hermes config,非 MoFin repo) | ✅ 允许 |
|
||||
| `none`(只报告) | 报备 | ✅ 允许 |
|
||||
|
||||
**key 切换、服务重启、任务重跑、硬链修复——自愈能做的一切都不需要碰代码。**
|
||||
|
||||
## 三、当判断"需要改代码"时
|
||||
|
||||
### 路径 A(首选):kanban 提单给小小莫
|
||||
描述问题+证据+建议方案,小小莫处理。适用于一切非紧急情况。
|
||||
|
||||
### 路径 B(紧急热修):走 git 正规流程
|
||||
```
|
||||
cd /home/hmo/MoFin
|
||||
git pull --rebase # 必须第一步!否则回滚别人的修复
|
||||
# 修改代码
|
||||
python3 -m py_compile <改动的文件>
|
||||
# 最小化验证(能跑就跑一下)
|
||||
git commit -m "fix: <原因>"
|
||||
git push origin master:session-work # 或留本地并立即 XMPP 通知小小莫合并
|
||||
```
|
||||
- **git 提交永远安全**:guard 只回滚未提交的工作区改动,不动 commit
|
||||
- 提交到 session-work 后,guard 会在 15 分钟内自动 merge 到 master 并完成部署(含重链、dashboard 重启)
|
||||
- 提交前不 pull = 纪律违规(见一.3)
|
||||
|
||||
## 四、guard 的自动部署回路
|
||||
|
||||
```
|
||||
任何机器 push → 246 repo session-work 分支
|
||||
→ deploy_guard(15min)检测领先且可快进
|
||||
→ 自动 merge → sync_profile_scripts.sh 重链
|
||||
→ 触及 server.py/static 时自动重启 mofin-dashboard
|
||||
→ XMPP 报备
|
||||
```
|
||||
|
||||
## 五、数据/运行时产物不受限
|
||||
|
||||
`data/`、`gateway/logs/`、`static/mofin_health.json` 等运行时产物不在守卫范围,正常读写无碍。
|
||||
|
||||
---
|
||||
|
||||
*依据:dev-spec 十条红线 + 2026-07-20 stale 提交事件 + 2026-07-21 gateway agent 螺旋事件*
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user