Bug 修复完整工作流
从发现 Bug、创建 Issue、建立修复分支,到测试、提交、PR、CI、Review、合并和收尾的完整可审查修复流程。
[!info] related notes
- 前置笔记: GitHub Flow, GitHub
- 相关 MOC: Git MOC
- 相关资源: GitHub CLI(gh)命令速查, TDD Workflow, Pre-Commit Quality Gate, Git 提交规范
- 合并策略: GitHub Pull Request 的三种合并方式
- AI / Agent 证据型 Bug 专项: AI Evidence-Grounded Bug 的端到端修复流程
Bug 修复完整工作流
目标
把一次 Bug 修复做成一个可复现、可验证、可审查、可追踪、可回滚的完整闭环,而不是“本地改好了就算结束”。
以下以常见的 GitHub Flow / main 主干开发为例。具体仓库如果有自己的分支保护、CI、提交规范,应优先遵守项目规范。
发现 Bug
↓
确认现象与影响范围
↓
GitHub Issue
↓
从最新 main 创建 fix 分支
↓
复现 / 写回归测试
↓
最小修复
↓
本地验证
↓
Commit
↓
Push
↓
Pull Request
↓
CI + Review
↓
Merge main
↓
Issue 自动关闭 + 本地收尾
AI / Agent Evidence-Grounding 类 Bug
如果症状不是普通的 UI/API 错误,而是:
输入证据不支持某个 claim
→ 模型仍生成该 claim
→ 系统还把它保存/授权/展示了
例如:
没有体态证据
→ Assessment 生成“高低肩”
→ BodyState 持久化
不要只沿“页面 → API → DB”做普通排查,更不要直接把结论写成“模型幻觉,改 Prompt”。这类问题应切换到专项流程:AI Evidence-Grounded Bug 的端到端修复流程。
它会继续要求:
Frozen Replay
→ Hard Invariant
→ First Bad Boundary
→ Error Introduction / Escape / Propagation
→ Evidence Consistency Contract
→ Durable Revalidation
→ Replay / Eval / E2E
通用 Git/Issue/PR/CI 闭环仍由本文负责;AI 专项笔记只补充其中的证据归因与运行时 Trust Boundary。
0. 先确认这真的是一个 Bug
在写代码之前,先把问题说清楚:
- 当前实际行为是什么?
- 预期行为是什么?
- 如何稳定复现?
- 影响哪些用户、页面、模块或数据?
- 是实现错误、语义错误、回归,还是需求本身还不明确?
如果连“错误在哪里”都无法稳定描述,先继续排查,不急着进入修复分支。
最小产物
至少写出:
Actual:
...
Expected:
...
Reproduction:
1. ...
2. ...
3. ...
1. 创建 GitHub Issue
Issue 负责描述问题本身,不是提前写死“我要改哪一行代码”。
一个实用的 Bug Issue 至少包含:
## Problem
当前错误行为和影响。
## Reproduction
1. ...
2. ...
3. ...
## Expected behavior
正确行为应该是什么。
## Acceptance criteria
- [ ] 条件 1
- [ ] 条件 2
- [ ] 有回归测试
- [ ] 相关检查通过
使用 GitHub CLI 时可以:
gh issue create
Issue 创建后记下编号,例如:
#123
后面的分支名、PR 和讨论都围绕这个编号建立追踪关系。
在创建分支前再确认一次编号
不要只凭聊天记录或记忆给分支编号。先确认 GitHub 上这个编号对应的对象、标题和状态。
例如:
gh issue view 144
gh pr view 145
如果不确定某个数字到底是 Issue 还是已经存在的 PR,就先查询再命名分支。
这一步看起来很小,但可以避免这种情况:
真正 Bug Issue = #144
误把旧 PR #123 当成 Issue
↓
创建了 fix/123-...
↓
开发完成后才发现编号错误
如果分支还没有推送,直接改名即可:
git branch -m fix/144-short-description
GitHub 中 Issue 和 Pull Request 都使用仓库中的编号体系,因此编号本身只是入口,对象类型、标题和状态必须实际核对。
2. 保护当前工作区
开始新 Bug 前先看当前状态:
git status
git diff
如果当前还有和本 Bug 无关、尚未提交的修改,不要直接带进新分支。
可以选择:
- 先提交原任务;
- 使用独立 worktree;
- 或暂时 stash。
例如:
git stash push -u -m "wip: before issue #123"
目标是让新 Bug 分支从一个清晰、可解释的基线开始。
3. 同步主干
在 GitHub Flow 中,修复分支应从最新主干创建:
git fetch origin
git switch main
git pull --ff-only origin main
检查:
git status
git log -5 --oneline
确认工作区干净,并且本地 main 已经追平远端。
如果仓库不是 GitHub Flow,而是有明确的 release / maintenance / dev 分支,应改用项目规定的基线,不要机械套用
main。
4. 创建 Bug 修复分支
推荐分支名直接带 Issue 编号和简短问题描述:
git switch -c fix/123-short-description
例如:
fix/144-body-state-temporal-actions
fix/456-login-refresh-loop
fix/789-upload-validation
确认:
git branch --show-current
git status
5. 先读代码,建立最小心智模型
不要看到报错就马上改第一处看起来相关的代码。
先回答:
- 用户操作从哪个入口进入?
- 数据从哪里来?
- 哪个模块拥有这条业务规则?
- 是否已经存在正确的实现路径可以复用?
- 这个问题是 UI 显示错误,还是 Domain / API / 数据本身也错了?
常见阅读顺序:
用户现象
↓
页面 / 组件
↓
Hook / Service / API client
↓
后端 Handler / Service
↓
Domain / Repository / DB
目标不是“读完整个项目”,而是找到最小责任边界。
先分清“表现 Bug”和“语义 Bug”
有些 Bug 看起来只是按钮或文案不对,但真正错误可能在领域语义。
例如 BodySense 中:
lifestyle.activity = 久坐为主
如果 UI 给它展示:
后来改善
后来加重
后来恢复
表面上是按钮设计问题,但根因其实是:
症状型 Fact 的 trend / lifecycle 操作被错误套到了“当前生活方式状态”上。
正确修复不能只隐藏按钮,还要确认保存新状态时是否走正确的 current-context transition。
所以一次完整定位通常要回答两层问题:
UI 应该展示什么?
+
Domain 应该发生什么?
6. 先建立一个能复现 Bug 的验证方式
优先把 Bug 固化成一个回归测试:
Bug 存在
↓
测试应该失败(Red)
↓
修改实现
↓
同一个测试通过(Green)
测试的重点是描述业务行为,而不是复制当前实现细节。
先运行目标测试并确认它确实失败。如果测试一开始就是绿的,它就没有证明自己捕获到了这个 Bug。
相关:TDD Workflow。
测试应该逐层扩大
对于跨 UI / API / 数据库的 Bug,可以按下面的层次建立证据:
Component Test
→ Command / Hook Test
→ Typecheck / Lint
→ Playwright E2E
→ 真实 API / DB 状态验证
例如“更新现状”这种状态迁移,仅验证按钮点击并不够。真正需要证明的是:
Browser
↓
React UI
↓
Command
↓
API Client
↓
Go Handler / Service
↓
Repository
↓
PostgreSQL
并且最终读回来仍然符合预期。
7. 做最小正确修复
修改时遵循三个原则:
- 修根因,不只遮住症状。
- 优先复用已有 Domain / API 路径,不创造第二套 source of truth。
- 不要把无关重构顺手塞进 Bug PR。
第一轮修复先追求“行为正确 + 边界清楚”,不是一次性把周围所有代码都重构完。
复用已有正确路径
如果代码库已经有正确的领域入口,应优先复用。
BodySense #144 中,生活方式现状变更已有:
PUT /api/v1/lifestyle
↓
LifestyleService.Update
↓
ApplyCurrentContextPatch
这个路径会:
旧 Fact → inactive / historical
新 Fact → active
新 Fact.supersedes_fact_id → 旧 Fact
revision → current_context.updated
因此 Workbench 应该接入这条路径,而不是继续给 Lifestyle Fact 调 updateFactTemporal(trend=...)。
8. 边改边检查 Diff
频繁查看:
git diff
也可以只看目标文件:
git diff -- path/to/file
持续问自己:
- 这些修改都属于当前 Issue 吗?
- 有没有顺手格式化无关文件?
- 有没有调试日志、临时代码、硬编码?
- 是否出现了超出 Issue 范围的新设计?
开发时真正需要审视的是:
当前分支相对于主干,到底改变了什么?
格式化基础设施不要顺手塞进 Bug PR
如果修 Bug 时发现代码格式化配置本身也需要优化,最好拆成独立工程化任务。
例如:
Bug PR
→ 只修业务行为
chore/dev-formatting
→ Prettier / VS Code / EditorConfig
这样 Review 时不会看到大量与 Bug 无关的格式 diff。
9. 分层运行验证
验证建议从小到大:
目标测试
↓
相关测试集
↓
Typecheck / Lint / Build
↓
必要的集成或 E2E
↓
人工复现原 Bug
一个常见的检查组合是:
# 目标测试
<project-specific test command>
# 相关测试
<project-specific test suite>
# 静态检查
<typecheck command>
<lint command>
最终还要按照最开始的 Reproduction 再手动走一遍,确认真实用户路径已经恢复。
E2E 不只验证“请求发出去了”
对于持久化状态 Bug,E2E 最好继续读取服务器状态并断言语义。
BodySense #144 的 E2E 实际验证了:
初始化:日常活动以久坐为主
↓
浏览器点击“更新现状”
↓
改为:现在工作中会频繁走动
↓
保存
↓
current_revision: 1 → 2
↓
新 Fact ID != 旧 Fact ID
↓
新 Fact.lifecycle_state = active
↓
新 Fact.supersedes_fact_id = 旧 Fact ID
↓
revision.change_type = current_context.updated
↓
reload 后仍显示新状态
这种 E2E 才真正证明:
UI 修复、API 路径和数据库 longitudinal semantics 是同一个闭环。
10. 提交前做一次人工 Review
先看:
git status
git diff
检查:
- Bug 是否真的被修掉?
- 是否保留了回归测试?
- 是否修改了无关文件?
- 是否引入新的状态或数据语义问题?
- 是否有 secrets、token、
.env或临时文件? - 是否需要同步文档?
这一步可以把自己从“实现者视角”切换成“Reviewer 视角”。
同时检查格式和 whitespace
建议至少运行:
git diff --check
没有输出通常说明不存在 trailing whitespace 等明显问题。
如果仓库使用 Prettier,还可以对本次修改文件检查:
pnpm exec prettier --check <changed-files...>
本地保存自动格式化属于开发体验;format:check / git diff --check 属于提交前质量保证,两层都值得保留。
11. 精确 Stage
比起直接:
git add .
更推荐:
git add -p
它会逐块询问:
Stage this hunk [y,n,q,a,d,...]?
也可以在文件很明确时显式列出:
git add path/a path/b path/c
只把真正属于当前 Issue 的修改放进本次提交。
一定要检查 staged diff
git commit 提交的是暂存区,不是你“以为自己改过的全部内容”。
所以提交前再看:
git diff --staged
心智模型:
working tree
↓ git add
staging area
↓ git commit
commit
12. 创建原子 Commit
如果项目使用 Conventional Commits,可以写:
git commit -m "fix(scope): describe the fix"
例如:
fix(web): scope temporal actions by fact semantics
fix(api): reject unsupported assessment evidence
一个好的 Commit 应该能够独立回答:
这个历史节点具体修复了什么?
检查分支相对主干的提交:
git log --oneline origin/main..HEAD
相关:Git 提交规范、Pre-Commit Quality Gate。
13. Push 修复分支
第一次推送:
git push -u origin fix/123-short-description
建立 upstream 后,后续只需要:
git push
-u / --set-upstream 建立的是:
local fix branch
↕ tracking
origin/fix branch
后续 git status -sb 会直接显示本地相对 upstream 的 ahead / behind 状态。
14. 创建 Pull Request
PR 是合并前的审查与验证边界,不只是“把代码放进 main”。
PR 至少应该说明:
## Summary
修了什么,为什么需要修。
## Related Issue
Fixes #123
## Changes
- ...
- ...
## Testing
- [x] 目标回归测试
- [x] 相关测试
- [x] Typecheck / Lint
- [x] Manual testing
使用 CLI:
gh pr create
如果希望 Issue 在 PR 合并后自动关闭,在 PR 描述中写:
Fixes #123
常见自动关闭关键字还包括:
Closes #123
Resolves #123
PR 中的 Testing 要写“证据”,不只是打勾
相比只写:
[x] tests pass
更有价值的是写清:
Web tests: 204 passed
Focused BodyState tests: 8 passed
Lifestyle current-update E2E: passed
Typecheck: passed
Lint: passed
这样 Reviewer 可以迅速知道你验证到了哪一层。
15. 等待 CI,并根据结果继续修改
PR 创建后,CI 会对提交进行自动验证。
可以查看:
gh pr checks
CI 失败时,不需要重新开 PR。
继续在同一分支修改:
# 修改代码
git add -p
git commit -m "fix(scope): address review or CI failure"
git push
原 PR 会自动更新。
先区分代码失败还是 CI 环境失败
不要看到红色就直接改业务代码。
先定位 failure step:
test / lint / typecheck assertion
→ 更可能是代码问题
runner / network / dependency timeout
→ 更可能是 CI 基础设施问题
governance / commit-lint
→ 更可能是 PR metadata 或提交规范问题
修复的是证据指向的根因,而不是“让红灯消失”。
16. Review Pull Request
进入 PR 的 Files changed,把它当成别人提交的代码重新看一遍。
Review 时重点检查:
Issue 描述的问题
↓
PR 是否只解决这个问题?
↓
测试是否真正覆盖原 Bug?
↓
实现是否符合 Domain / Architecture?
↓
有没有隐藏副作用?
如果 Reviewer 提出问题,就继续在原分支追加修改和提交,直到通过。
17. Merge 到主干
只有当以下条件满足时才合并:
- Issue 的 Acceptance Criteria 已满足;
- 目标 Bug 已无法复现;
- 回归测试存在并通过;
- 必要 CI 全绿;
- Review 通过;
- PR diff 没有无关内容。
受保护的 main 不应靠直接 push 绕过 PR。
GitHub 常见的三种 PR 合并方式是:
Create a merge commit
Squash and merge
Rebase and merge
它们对 commit graph、SHA、历史和删除分支时的行为不同,详见 GitHub Pull Request 的三种合并方式。
对于短生命周期 Bugfix,常见选择是:
Squash and merge
因为开发分支可以保留多个 WIP / review commit,而 main 最终只留下一个清晰的业务修复 commit。
如果 PR 描述中有:
Fixes #123
GitHub 会在 PR 合并进默认分支后自动关闭对应 Issue。
18. 合并后的本地收尾
同步主干:
git switch main
git pull --ff-only origin main
先确认最终 merge 结果确实已经进入主干:
git log --oneline -5
再删除已经合并的本地分支:
git branch -d fix/123-short-description
如需要删除远端分支:
git push origin --delete fix/123-short-description
git fetch --prune
如果开始前 stash 了旧工作,再回到原来的任务分支恢复:
git stash list
git stash pop
Squash / Rebase 后 git branch -d 可能出现 warning
这并不一定意味着代码没有进入 main。
例如 feature commit:
303cb509
经过 Squash Merge 后,main 上可能变成全新的:
653aaa5f
此时代码内容已经合并,但 Git 的 commit graph 中:
303cb509 ∉ ancestors(main)
因此执行:
git branch -d fix/...
可能看到:
warning: ... merged to origin/fix/..., but not yet merged to HEAD
这里 Git 判断的是 commit ancestry,不是“代码内容是否等价”。
正确处理顺序是:
1. GitHub PR 确认 MERGED
2. git switch main
3. git pull --ff-only origin main
4. git log 确认主干最终 commit
5. 再清理 local / remote feature branch
不要把能不能 git branch -d/-D 当作 PR 是否真正合并的唯一证据。
19. 做一次 Post-Merge 验证
Bug 修复在 merge 后才真正进入主干,所以最后再确认:
- Issue 已自动关闭;
main的 CI 正常;- 主干版本仍无法复现原 Bug;
- 如果项目有 staging / deployment channel,确认后续发布流程按项目策略继续推进。
不要把“PR merge”与“已经发布到生产环境”混为一谈。
BodySense #144 实战复盘
这次 BodySense 的 Bug 修复完整走了一次上面的闭环,可以作为可复用样例。
1. 问题不是普通 UI Bug,而是 Fact 语义错误
Issue:
#144 fix: scope BodyState temporal actions by fact semantics
错误行为:
lifestyle.activity = 久坐为主
lifestyle.recovery = 无
却和症状 Fact 一样显示:
后来改善
后来加重
后来恢复
这会产生语义不自然的数据:
久坐为主 + trend=improving
无恢复行为 + trend=worsening
最终区分为:
症状 / discomfort
→ trend / lifecycle temporal actions
Lifestyle current state
→ 当前值 replacement / transition
2. 找到已有正确 Domain Path
没有新造后端接口,而是复用:
PUT /api/v1/lifestyle
↓
LifestyleService.Update
↓
ApplyCurrentContextPatch
UI 最终语义:
Lifestyle Fact
[纠正记录] [更新现状]
症状 Fact
[纠正记录] [后来改善] [后来加重] [后来恢复]
其中:
纠正记录
= 之前那条记录本身就是错的
更新现状
= 之前记录当时是对的,现在情况变了
3. 测试从 Component 一直走到真实 PostgreSQL
Focused tests:
BodyStateWorkbench 7 / 7
useBodyStateCommand 1 / 1
Web 全量测试:
41 test files
204 tests passed
静态检查:
Typecheck ✅
ESLint ✅
真实 Playwright E2E:
日常活动以久坐为主
↓
更新现状
↓
现在工作中会频繁走动
↓
保存
↓
revision 1 → 2
↓
新 Fact active
↓
supersedes_fact_id = 旧 Fact ID
↓
current_context.updated
↓
reload 后仍正确
4. 格式化问题单独收口
修复期间发现:
git diff --check
→ trailing whitespace
并且项目缺少完整的 save-time formatter 配置。
处理方式不是把 Prettier 工程化一起塞进 Bug PR,而是:
#144 Bug branch
→ 只格式化本次修改的文件
chore/dev-formatting
→ 独立准备 Prettier / VS Code / EditorConfig
这保证了 Bug PR scope 清晰。
5. 分支编号也在提交前修正
最初分支误命名为:
fix/123-body-state-temporal-actions
核对 GitHub 后发现:
#123 = 已关闭的旧 PR
真正 Bug Issue = #144
因此在推送前改名:
git branch -m fix/144-body-state-temporal-actions
最终使用正确分支名继续提交。
6. Commit → Push → PR
Commit:
303cb509
fix(web): scope temporal actions by fact semantics
Push:
git push -u origin fix/144-body-state-temporal-actions
PR:
#145 fix(web): scope temporal actions by fact semantics
PR body 使用:
Fixes #144
因此 PR 合并后 Issue #144 自动关闭。
7. CI / Branch Protection 是真正的合并门
该仓库 main 要求:
commit-lint
Governance Oracle
Quality Oracle
Database Oracle
Experience Oracle
Delivery Observation
所以本地测试全绿只是“可以开 PR”,不是“可以直接进 main”。
真正过程是:
local green
→ PR
→ required checks green
→ merge
8. 使用 Squash Merge 后 SHA 改变
feature branch commit:
303cb509
PR #145 Squash Merge 后 main commit:
653aaa5f
fix(web): scope temporal actions by fact semantics (#145)
所以:
303cb509 != 653aaa5f
这解释了后面删除分支时的 warning。
详细机制见 GitHub Pull Request 的三种合并方式。
9. 最终状态
Issue #144 CLOSED
PR #145 MERGED
main 653aaa5f
local main = origin/main
Bug branch 可安全清理
这一轮最值得保留的经验不是某个具体按钮,而是:
先把 Bug 固化成业务语义,再沿真实责任链修到 Domain 边界,最后用测试、PR、CI 和 Git 历史把修复证据完整留下来。
完成定义(Definition of Done)
一次完整的 Bug 修复至少应满足:
- Bug 可以稳定描述和复现
- 已有 Issue 记录问题与验收标准
- 修复分支来自正确基线
- 分支名中的 Issue 编号已经实际核对
- 有回归测试或等价的稳定验证方式
- 实现针对根因且没有无关改动
- 本地相关测试、静态检查和人工复现通过
-
git diff --check/ formatter 检查通过 - Commit 清晰且原子
- PR 关联 Issue
- CI / Review 通过
- PR 合并到正确主干
- Issue 关闭
- 本地分支和工作区完成收尾
- 必要时完成合并后的环境验证
最短记忆模型
Issue
→ Branch
→ Reproduce
→ Test Red
→ Fix
→ Test Green
→ Diff Review
→ Format Check
→ Stage Review
→ Commit
→ Push
→ PR
→ CI / Review
→ Merge
→ Close Issue
→ Cleanup
核心不是“Git 命令都敲对”,而是让每一次 Bug 修复都留下完整的问题记录、代码历史、验证证据和审查轨迹。
后续补充位置
这篇笔记保留以下位置,方便针对具体项目继续补充:
- 其他类型 Bug 的复现截图与日志;
- 不同项目的代码责任链分析;
- 更多回归测试与 E2E 模式;
- 不同仓库的 CI 检查项;
- PR Review 失败案例;
- Merge 后 staging / production 验证过程。