← [ BACK_TO_NOTES ]

Bug 修复完整工作流

从发现 Bug、创建 Issue、建立修复分支,到测试、提交、PR、CI、Review、合并和收尾的完整可审查修复流程。

#howto #growing #dev #git #github #github

[!info] related notes

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. 做最小正确修复

修改时遵循三个原则:

  1. 修根因,不只遮住症状。
  2. 优先复用已有 Domain / API 路径,不创造第二套 source of truth。
  3. 不要把无关重构顺手塞进 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 验证过程。
创建于 2026/8/31 更新于 2026/9/1
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本