tools-sync: 增加 ls-remote 同步闸门,并记下 gitea 空 pack 故障的排查法
这次同步事故的教训不是服务器坏了,而是坏了没人知道:pull --rebase 一失败脚本 就退出,push 从没走到,3 个 commit(含整个 bmem skill)在本机躺了 8 天,报告里 却算作"已知故障、待恢复"。而 ls-remote 这 8 天一直是好的。 - sync_repos.ps1 增 Assert-RemoteInSync:push 后(及 -DryRun 时)用 ls-remote 比对 origin/<branch> 的实际 sha,不一致就 FAIL 并报出滞留/落后的 commit 数。 远端分支不存在时单独报错,避免那两个计数被静默地按 HEAD 算出误导性数字。 - SKILL.md 阶段 1 写明闸门是硬性的:服务端故障是原因不是结果,不得报成已同步。 - SKILL.md 新增 bad pack header / early EOF 的排查阶梯(三步各排掉一层),并记下 根因:全局 gitconfig 被注入 [uploadpack] packObjectsHook,把 pack-objects 换成 了空转脚本。附带提醒 HOME 是 /data/gitea/home 而非 /data/git,以及这是一条 完整 RCE 链、要连 hook 与 /tmp 载荷一起清。 - bmem/SKILL.md 的"已知边界"改为已修复,避免继续把普通的没 pull 误判成服务端故障。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tMFjKGgxdmxJ28dyXdx1u
This commit is contained in:
+5
-3
@@ -277,6 +277,8 @@ bmem delete --yes --sync # 确认执行并同步给其它机器
|
|||||||
| 注销项目 | `bmem delete`(预览)/ `bmem delete --yes --sync`(执行) |
|
| 注销项目 | `bmem delete`(预览)/ `bmem delete --yes --sync`(执行) |
|
||||||
| 帮助 | `bmem help` |
|
| 帮助 | `bmem help` |
|
||||||
|
|
||||||
已知边界:当前 gitea 的 pack 服务故障,**push 正常但 clone/fetch 失败**,所以第二台机器
|
曾经的边界,**2026-09-02 已修复**:gitea 的 pack 服务坏了约一周(push 正常、clone/fetch
|
||||||
暂时拉不到 Markdown 层(log 层走 PostgreSQL 不受影响,Notes 全文在中心库里)。
|
报 `bad pack header`),Markdown 层因此在机器之间停摆。根因是 gitea 的全局 gitconfig 被注入
|
||||||
看到 `bmem machines` 报"本机的 bmem.md 落后了"时,先想到这条。
|
了 `[uploadpack] packObjectsHook`,把 `git pack-objects` 换成了一个不产出 pack 的脚本;
|
||||||
|
排查与处置写在 tools-sync skill 的阶段 1。现在 `bmem sync` 的 git 部分应当正常。
|
||||||
|
再看到 `bmem machines` 报"本机的 bmem.md 落后了",就按普通的没 pull 处理,不要再归因于服务端。
|
||||||
|
|||||||
+26
-1
@@ -48,7 +48,14 @@ powershell -File <skill目录>\scripts\sync_repos.ps1 -RepoUrl <origin_url>
|
|||||||
```
|
```
|
||||||
|
|
||||||
脚本对主副本做的事:`git add -A` → 有变更则 commit(消息格式
|
脚本对主副本做的事:`git add -A` → 有变更则 commit(消息格式
|
||||||
`sync: <机器名> <日期> <一句话摘要>`)→ `git pull --rebase` → `git push`。
|
`sync: <机器名> <日期> <一句话摘要>`)→ `git pull --rebase` → `git push` →
|
||||||
|
**`ls-remote` 闸门**:比对 `origin/<branch>` 的实际 sha 与本地是否一致,不一致就
|
||||||
|
`FAIL` 并报出有几个 commit 滞留本机。
|
||||||
|
|
||||||
|
这个闸门是硬性的,不要绕过。**服务端故障是原因,不是结果**:传输坏了、commit
|
||||||
|
只躺在本地,这一轮同步就是失败的,不能在报告里写成"已同步(远端故障,待恢复)"。
|
||||||
|
2026-09-02 就是这么丢了 8 天——本地 3 个 commit(含一整个新 skill)以为存下了,
|
||||||
|
其实哪台机器都看不到;而 `ls-remote` 那 8 天一直是好的,这一句比对随时能发现。
|
||||||
|
|
||||||
人工介入的情形:
|
人工介入的情形:
|
||||||
|
|
||||||
@@ -63,6 +70,23 @@ powershell -File <skill目录>\scripts\sync_repos.ps1 -RepoUrl <origin_url>
|
|||||||
写入,所以是可证同一份历史,不是近似复制。重建后
|
写入,所以是可证同一份历史,不是近似复制。重建后
|
||||||
`git update-ref refs/remotes/origin/main <sha>` + `git rebase <sha>`,本地历史就归位了,
|
`git update-ref refs/remotes/origin/main <sha>` + `git rebase <sha>`,本地历史就归位了,
|
||||||
服务恢复后一条 `git push` 即可。**任何情况下都不要 force push**(会丢掉别的机器的 commit)。
|
服务恢复后一条 `git push` 即可。**任何情况下都不要 force push**(会丢掉别的机器的 commit)。
|
||||||
|
- **clone/fetch 报 `bad pack header` / `early EOF`,但 `git ls-remote` 和 Gitea 网页/API 正常**:
|
||||||
|
服务端把 pack 生成外包给了一个不产出内容的程序。**2026-09-02 实际发生过一次**,
|
||||||
|
症状是 `POST /git-upload-pack` 记 200 OK、耗时也正常,响应体却只有 `0008NAK`。
|
||||||
|
别去猜版本或网络,按这个阶梯两三步就能定位(每一步都排掉一层):
|
||||||
|
1. 容器/宿主机内直连 gitea 端口 clone —— 还失败就与 frp、反向代理无关;
|
||||||
|
2. `git upload-pack --stateless-rpc <repo裸路径> < 一个手写的 want/done 请求 | wc -c`
|
||||||
|
—— 出得来几 MB 就说明 git 本身没病,**注意必须带 `--stateless-rpc`**,
|
||||||
|
用 `clone file://` 走的不是 HTTP 那条码路,测了不算数;
|
||||||
|
3. 在 gitea 和 git 之间插一个 wrapper(把 `/usr/bin/git` 换成脚本,`tee` 下 argv/env/
|
||||||
|
stdin/stdout)跑一次 clone。**上次就是这一步定的案:upload-pack 只吐了 17 字节、
|
||||||
|
stderr 全空**——不是 gitea 丢包,是 git 自己被指使着不干活。
|
||||||
|
根因在 gitea 的全局 gitconfig(`$HOME/.gitconfig`,docker 下 HOME=`/data/gitea/home`,
|
||||||
|
**不是** `/data/git`)里被注入了 `[uploadpack] packObjectsHook`。该配置会把
|
||||||
|
`git pack-objects` 整个替换掉,指向的脚本不产出 pack 就得到空包。
|
||||||
|
处置:备份该文件取证 → 移走 → 重启 gitea(它启动时会重建一份干净的)→
|
||||||
|
在容器内和公网各 clone 一次验证。同时检查 hook 脚本有没有被追加执行语句、
|
||||||
|
`/tmp` 有没有配套载荷——这是一条完整的 RCE 链,不是配置写错。
|
||||||
- **push 被拒(认证)**:检查凭据管理器条目;新机器上用
|
- **push 被拒(认证)**:检查凭据管理器条目;新机器上用
|
||||||
`git config --global credential.helper manager` 后重试一次,让 credential
|
`git config --global credential.helper manager` 后重试一次,让 credential
|
||||||
helper 弹窗或从 skills_env.md 读凭据拼一次性 URL 完成首次认证。
|
helper 弹窗或从 skills_env.md 读凭据拼一次性 URL 完成首次认证。
|
||||||
@@ -237,6 +261,7 @@ setupAgenticToolkit("configure", Agents=["claude-code","codex"], ...
|
|||||||
# tools-sync 同步报告 <日期>
|
# tools-sync 同步报告 <日期>
|
||||||
## skills 同步
|
## skills 同步
|
||||||
- ~\.claude\skills: <pull/push 结果,commit 摘要>
|
- ~\.claude\skills: <pull/push 结果,commit 摘要>
|
||||||
|
- 远端校验: origin/main = <sha>,与本地一致 / **不一致:N 个 commit 滞留本机**
|
||||||
- ~\.codex\skills: <pull 结果,注册差集>
|
- ~\.codex\skills: <pull 结果,注册差集>
|
||||||
## 硬编码审计
|
## 硬编码审计
|
||||||
- 本次修复: <清单>
|
- 本次修复: <清单>
|
||||||
|
|||||||
@@ -56,16 +56,56 @@ if ($st.Out) {
|
|||||||
Write-Host "working tree clean"
|
Write-Host "working tree clean"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $DryRun) {
|
# The sync is only "done" when the remote branch actually points at our commit.
|
||||||
|
# ls-remote asks for refs only, so it keeps working even when the server's pack
|
||||||
|
# service is broken -- which is exactly the outage this gate exists to survive.
|
||||||
|
function Assert-RemoteInSync {
|
||||||
|
param([string]$Dir, [string]$Branch)
|
||||||
|
$local = Invoke-Git $Dir @("rev-parse", $Branch)
|
||||||
|
if ($local.Code -ne 0) { Fail "cannot resolve local $Branch." }
|
||||||
|
$ls = Invoke-Git $Dir @("ls-remote", "origin", "refs/heads/$Branch")
|
||||||
|
if ($ls.Code -ne 0) {
|
||||||
|
Write-Host $ls.Out
|
||||||
|
Fail "ls-remote failed: cannot verify the push landed. Treat this run as NOT synced."
|
||||||
|
}
|
||||||
|
$remote = ($ls.Out -split "\s+")[0]
|
||||||
|
if ($remote -ne $local.Out) {
|
||||||
|
Write-Host " local $Branch = $($local.Out)"
|
||||||
|
if (-not $remote) {
|
||||||
|
# Empty ref: the counts below would silently be measured against HEAD.
|
||||||
|
Fail "origin has no refs/heads/$Branch. Nothing on this machine is backed up by the server."
|
||||||
|
}
|
||||||
|
Write-Host " remote $Branch = $remote"
|
||||||
|
$ahead = Invoke-Git $Dir @("rev-list", "--count", "$remote..$($local.Out)")
|
||||||
|
if ($ahead.Code -eq 0 -and [int]$ahead.Out -gt 0) {
|
||||||
|
Write-Host " $($ahead.Out) commit(s) STRANDED on this machine - they exist nowhere else."
|
||||||
|
}
|
||||||
|
$behind = Invoke-Git $Dir @("rev-list", "--count", "$($local.Out)..$remote")
|
||||||
|
if ($behind.Code -eq 0 -and [int]$behind.Out -gt 0) {
|
||||||
|
Write-Host " $($behind.Out) commit(s) from other machines not merged here."
|
||||||
|
}
|
||||||
|
Fail "primary is NOT in sync with origin/$Branch. A server-side outage is a reason, never an outcome: do not report this run as synced."
|
||||||
|
}
|
||||||
|
Write-Host "verified: origin/$Branch = $remote (this machine's work is on the server)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
# Read-only, but still worth running: silent drift is what this gate is for.
|
||||||
|
Assert-RemoteInSync $ClaudeSkills $Branch
|
||||||
|
} else {
|
||||||
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
|
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
|
||||||
if ($r.Code -ne 0) {
|
if ($r.Code -ne 0) {
|
||||||
Write-Host $r.Out
|
Write-Host $r.Out
|
||||||
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run."
|
$stranded = Invoke-Git $ClaudeSkills @("rev-list", "--count", "origin/$Branch..$Branch")
|
||||||
|
if ($stranded.Code -eq 0 -and [int]$stranded.Out -gt 0) {
|
||||||
|
Write-Host "NOTE: $($stranded.Out) local commit(s) cannot reach the server until this is fixed."
|
||||||
|
}
|
||||||
|
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run. If the transport itself is broken (see skill Phase 1), fix the server -- do not leave commits stranded."
|
||||||
}
|
}
|
||||||
Write-Host $r.Out
|
Write-Host $r.Out
|
||||||
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
|
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
|
||||||
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
|
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
|
||||||
Write-Host "primary in sync with origin/$Branch"
|
Assert-RemoteInSync $ClaudeSkills $Branch
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Phase 2: junction reconciliation (~\.codex\skills) ----------
|
# ---------- Phase 2: junction reconciliation (~\.codex\skills) ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user