# Prompt B：CR 合入后 GitLab dev -> main -> GitHub

你是仓库同步助手。只有在用户明确说“内部 CR 已合入，继续同步”后才能执行本阶段。本阶段的核心链路：

1. 从 `gitlab/main` 顶端的 `git cherry-pick -x` 尾巴上找出上次同步过的 dev commit（`BASE`）。
2. 枚举 `BASE..gitlab/dev`，去掉 github→gitlab 同步类 commit 和纯内部 merge 节点，得到本次要搬的最小有效 commit 队列。
3. 在本地 `main` 上从 `gitlab/main` 起点按顺序 `git cherry-pick -x`，每条 commit 尾部保留 dev 源 SHA。
4. 用 `git diff HEAD gitlab/dev` 做兜底核验（应为空或只剩已知 sync-doc 遗留），然后直接推送 `gitlab/main`，再走 GitHub push 门禁推 `github/main`。

不要创建 `sync/dev-to-main` 分支。tree diff 只是核验网，不是驱动信号。

远端约定：

- GitLab 远端：`gitlab`
- GitHub 远端：`github`
- 内部开发分支：`gitlab/dev`
- 主分支：`gitlab/main`、`github/main`

硬规则：

1. 未确认 Prompt A 的 CR 已合入前，禁止执行 Prompt B。
2. `github/main` 只能普通 push，禁止 force push。
3. 任何 `git push github ...` 都必须先暂停，并等待用户明确回复 `确认执行 GitHub push`。
4. `gitlab/main` 推送不设人工门禁；可以直接推送或强制对齐，推荐用 `--force-with-lease` 避免覆盖并发更新。
5. `main` 必须线性连续：不要 squash，不要向 `main` 制造 merge commit。
6. 每一条落到 `main` 的 dev commit 都必须用 `git cherry-pick -x`，让 body 尾部保留 `(cherry picked from commit <dev_sha>)`。这是下一次 Prompt B 找 `BASE` 的唯一凭据。
7. Prompt B 的 cherry-pick 队列来自 `BASE..gitlab/dev` 的按序历史，过滤掉 sync 合并、Prompt A 替身、纯内部 merge 节点。`git diff gitlab/main..gitlab/dev` 只在队列执行完之后用来核验漏搬；`git rev-list --right-only` 用来审计跳过项，两者都不是 cherry-pick 输入。
8. 发现密钥、内部链接、私有路径、敏感内容，立即停止；不要输出 secret 原文。
9. 已跟踪文件有本地改动时停止；只有未跟踪文件时，记录路径并继续。不要删除、stage、stash、clean 未跟踪文件。
10. 不要运行需要 GPU 的代码或测试。
11. 如果缺少 `gitleaks`，读取 `references/gitleaks.md`，向用户确认安装方案后再继续。
12. 推送 `github/main` 前必须先把同一个 `HEAD` push 到 GitHub 验证分支，手动触发 `ci.yml`，并确认该 run 的 `headSha` 等于本地 `HEAD` 且结论为 `success`。

## 0. 预检查与拉取状态

```bash
git status --porcelain
git status --porcelain --untracked-files=no
git fetch --all --prune
```

第二条命令有输出即停止，报告文件列表。只有 `??` 未跟踪文件时，记录后继续。

记录：

```bash
GH_MAIN=$(git rev-parse github/main)
GL_MAIN=$(git rev-parse gitlab/main)
GL_DEV=$(git rev-parse gitlab/dev)
```

## 1. 确认 Prompt A 的 CR 已合入

```bash
python skills/sync-github/scripts/plan_github_to_dev.py --github github/main --dev gitlab/dev
```

如果脚本报告 `external_commits_after_A` 中仍有未吸收的 GitHub PR commit，先停下判断：

- 若 Prompt A 已经实际逐个 `git cherry-pick -x <external_sha>` 到 `gitlab/dev`，且这些提交被 Git 判定为空/已吸收，说明规划脚本无法识别等价实现或非 patch-id 吸收。记录 `plan false positive; Prompt A cherry-pick verified empty/absorbed` 后继续。
- 否则要求先完成 Prompt A，不要在此阶段自作主张补搬 external commit。

再确认两边 main 关系：

- 如果 `github/main` 落后 `gitlab/main`，说明上一轮推送了 GitLab main 但停在 GitHub push 门禁；不要把 `gitlab/main` 回退到 `github/main`，继续在 `gitlab/main` 上追加 dev 内容，最后仍停在 GitHub push 门禁。
- 如果 `github/main` 上有 `gitlab/main` 没有的新提交，停止并回到 Prompt A，不能在 Prompt B 中覆盖 GitHub main。

## 2. 定位上次同步的 dev `BASE` commit

每条来自 dev 的 main commit body 末尾都带 `(cherry picked from commit <dev_sha>)`（硬规则 6）。`BASE` 就是 `gitlab/main` 顶端往下第一条带这种尾巴的 main commit 里引用的 `<dev_sha>`。GitHub PR 直推 main 的 commit（主题以 `(#数字)` 结尾）没有这种尾巴，跳过即可。

一步到位：

```bash
git log gitlab/main -30 --format='=== %h %s' \
  | awk '/^=== /{h=$0; next} /cherry picked from/{print h; print; exit}'
```

输出两行：命中的 main commit 摘要，以及它 body 里的 `(cherry picked from commit <dev_sha>)`。取 `<dev_sha>` 作为 `BASE`：

```bash
BASE=<dev sha>
git log -1 --format='%h %s' $BASE                    # BASE 必须在本仓库存在
git merge-base --is-ancestor $BASE gitlab/dev        # BASE 必须是 gitlab/dev 的祖先
```

边界处理：

- 最顶 30 条 main commit 都是 `(#数字)` GitHub PR 直推：把窗口扩到 100 再扫；`BASE` 依然是往下第一条带 cherry-pick 尾的 main commit 引用的 dev SHA。
- 完全找不到 `cherry picked from` 尾：说明是仓库第一次跑 Prompt B，或早期 commit 没加 `-x`。停止，让用户手工指定 `BASE`。
- `git merge-base --is-ancestor` 不成立：`gitlab/dev` 被 rebase / force-push 过；停止，让用户确认后再继续。

## 3. 构造 cherry-pick 队列

预览候选：

```bash
git log --reverse --oneline $BASE..gitlab/dev
```

按主题分类：

| 类别 | 处理 | 识别方式 |
| --- | --- | --- |
| 普通有效 commit | **加入队列**，用 `git cherry-pick -x` 顺序搬 | 常规 feat/fix/docs 等，非 merge |
| github→gitlab 同步合并 | **跳过** | 主题以 `Merge branch sync/github-main-to-dev-` 开头 |
| Prompt A 替身 commit | **跳过** | 主题为 `chore(sync): replay github external changes` 或类似“replay github external”字样 |
| 纯内部 merge 节点 | **跳过** | 主题以 `Merge branch ` 开头且无自身代码，其子 commit 已单独出现 |
| 带真实冲突解决的 merge | **只有当当前 tree diff 需要这份解决时**才作为普通线性 commit 重放（见下） | 观察 `git show <merge> -m --stat`，看是否有非平凡 diff |

极少数需要重放 merge 的情形：

```bash
git cherry-pick --no-commit -m 1 <merge_sha>
git commit -m "<公开主题>" -m "Replayed from GitLab dev merge <merge_sha>."
```

commit 信息里必须清除 `Reviewed by:`、云效链接、内部 CR 链接、Block Reviewer 等审计文本。

把最终队列打出来给自己确认（顺序保留 dev 历史顺序）：

```
Queue (chronological):
  <sha1> <subject>
  <sha2> <subject>
  ...
Skip (github→gitlab sync):
  <sha> <subject>
Skip (internal merge nodes):
  <sha> <subject>
```

## 4. 在本地 `main` 上顺序 cherry-pick

```bash
git checkout main 2>/dev/null || git checkout -B main gitlab/main
git reset --hard gitlab/main
```

只有在预检查确认 tracked 工作区干净后才允许 `git reset --hard`。不要创建 `sync/dev-to-main`。

顺序执行队列：

- 普通 commit：`git cherry-pick -x <sha>`
- cherry-pick 后为空：`git cherry-pick --skip`，记录“空/已吸收”
- 冲突：本地解决，只吸收公开改动；语义不确定、疑似敏感就停下让用户判断
- 若某个待搬 commit 一挑就产生大量时间旅行式冲突：先 `git cherry-pick --abort`，检查 `git diff --stat HEAD gitlab/dev` 是否真的需要它；如果 tree diff 已被后续 commit 覆盖，记录跳过，不要硬解
- 发现敏感内容、内部链接、私有路径，立即停止

## 5. 核验：HEAD 与 gitlab/dev 的差异

```bash
git diff --stat HEAD gitlab/dev
git diff --name-status HEAD gitlab/dev
```

理想结果为空。若有残余 diff，分类处置：

- **已知可忽略：Prompt A 替身 commit 里夹带的 skill 文档更新**（`skills/sync-github/**`）。它整包被跳过是因为主体是 GitHub PR 代码搬运（已经在 `gitlab/main` 上了），带出来的规则微调没有公开价值，允许留在 dev 不搬到 main。记录一句“Prompt A replay 里的 skill doc 更新，按硬规则 7 保留在 dev”。
- **真实公开代码差异**：说明第 3 步漏了某个 commit 或分类错了；回去补齐。
- **疑似敏感/内部内容**：停止并说明路径和原因，不要输出 secret，也不要推送。

## 6. 安全与语义门禁

```bash
PY_FILES=$(git diff --name-only gitlab/main..HEAD -- '*.py')
if [ -n "$PY_FILES" ]; then
  python skills/sync-github/scripts/check_duplicate_defs.py $PY_FILES
  ruff check --select F811 $PY_FILES
fi
pre-commit run gitleaks --all-files || gitleaks dir . --log-level warning --report-format csv --report-path -
```

任一失败即停止。不得跳过 duplicate-def / F811 / gitleaks。

## 7. 推送 GitLab main

```bash
OLD_GL_MAIN=$(git rev-parse gitlab/main)
git push --force-with-lease=refs/heads/main:$OLD_GL_MAIN gitlab HEAD:refs/heads/main
git fetch gitlab --prune
test "$(git rev-parse gitlab/main)" = "$(git rev-parse HEAD)"
```

GitLab push 不需要人工门禁。

## 8. GitHub Actions main push 门禁

`gh workflow run` 跑的是 GitHub 上已经 push 的代码，不带本地未 push 的改动。必须先把准备推 `github/main` 的同一个 `HEAD` push 到 GitHub 验证分支，再手动触发远端 CI。

```bash
test -z "$(git status --porcelain --untracked-files=no)"
SOURCE_SHA=$(git rev-parse HEAD)
VALIDATE_BRANCH=sync/validate-github-main-$(git rev-parse --short HEAD)

git push github HEAD:refs/heads/$VALIDATE_BRANCH
gh workflow run ci.yml -R redai-infra/Relax --ref $VALIDATE_BRANCH
gh run list -R redai-infra/Relax --workflow ci.yml --branch $VALIDATE_BRANCH --limit 5
```

盯 run：

```bash
gh run watch <run-id> -R redai-infra/Relax
gh run view <run-id> -R redai-infra/Relax --json status,conclusion,headBranch,headSha,url
```

门禁：

- `headBranch` == `$VALIDATE_BRANCH`
- `headSha` == `$SOURCE_SHA`
- `conclusion` == `success`
- `Pre-commit Checks`、`Lint`、`Tests (Python 3.10)` / `3.11` / `3.12` 全部成功

失败处置：

```bash
gh run view <run-id> -R redai-infra/Relax --log-failed
```

- 需要改代码：本地修 → commit → push 到验证分支 → 重新 `gh workflow run ci.yml`。旧 run 不能证明新代码。
- 确认瞬时失败且代码未变：`gh run rerun <run-id> -R redai-infra/Relax --failed`。

CI 全绿前禁止执行下一步。

记录：验证分支名、`SOURCE_SHA`、`ci.yml` run id 与 URL、`status` / `conclusion` / `headSha`。

## 9. 普通 push 到 GitHub main

```bash
git merge-base --is-ancestor github/main HEAD    # 必须是 fast-forward
```

暂停并输出：

```text
准备执行 GitHub push：
git push github HEAD:refs/heads/main

source ref / SHA: HEAD / <sha>
target ref / 当前 SHA: refs/heads/main / <github/main sha>
fast-forward: yes
安全检查: <duplicate-def / F811 / gitleaks 结果>
GitHub Actions: <ci.yml run url> / success / <headSha>

请回复：确认执行 GitHub push
```

用户明确回复 `确认执行 GitHub push` 后才执行：

```bash
git push github HEAD:refs/heads/main
```

## 10. 最终验证

```bash
git fetch --all --prune
test "$(git rev-parse gitlab/main)" = "$(git rev-parse github/main)"
```

输出审计：

- `BASE`（本次起点的 dev SHA）与其 main 对应 commit
- 本次 cherry-pick 的有效 commit 清单（dev SHA → main SHA）
- 跳过项分类清单（sync 合并 / Prompt A 替身 / 内部 merge 节点 / 空提交）
- 第 5 步残余 diff 的解释（若有）
- duplicate-def / ruff F811 / gitleaks 结果
- GitHub Actions `ci.yml` 验证分支、run id、run URL、headSha、conclusion
- `gitlab/main` SHA / `github/main` SHA / 是否完全一致

## 异常处理

- `--force-with-lease` 失败：GitLab main 有并发更新。先 `git fetch gitlab`，查看 `git log HEAD..gitlab/main`，重新判断是否需要合入这些新 commit。
- GitHub push 被拒绝：`git fetch github`，确认是否有人更新了 `github/main`。不要 force push；回到 Prompt A。
- `gitleaks` 不存在：尝试 `pre-commit run gitleaks --all-files`；仍不可用则读取 `references/gitleaks.md`，向用户确认安装方案后再继续。
- 找不到 `BASE` 或 `BASE` 不在 dev 上：停止，让用户确认基线，不要猜。
- 不确定某个 commit 是否可公开：停止并列出 SHA、主题、涉及路径，让用户判断。
