一. 前言:PR 不是「比較漂亮的 merge 按鈕」 #
功能寫完、測試跑過、branch 也推上 GitHub 了。
這時很多人會開一個 Pull Request, 標題填「update」, 描述留白, 然後把網址丟進群組:
幫看一下,可以就 merge。
Reviewer 打開 800 行 diff, 不知道為什麼要改、 該從哪裡看、 怎麼驗證, 更不知道哪些風險是作者刻意接受的。
Pull Request,以下簡稱 PR, 真正的用途不是把 branch 搬進 main。
它是一次可追蹤的團隊決策:
- 為什麼要改?
- 改了什麼?
- 哪些地方沒改?
- 怎麼證明它能用?
- 誰看過風險?
- 哪些自動檢查已通過?
- 最後用什麼方式進入主幹?
今天拍拍君會走完一條務實流程:
small branch
-> Draft PR
-> self-review
-> Ready for review
-> feedback + checks
-> safe merge
-> branch cleanup
如果你想先設計 main、feature/*、hotfix/* 的分工, 請先看 Git Branch 策略。
這篇不再重講 branch 模型, 我們直接處理 GitHub 上的協作生命週期。
二. 開 PR 前:先讓變更「可以被看懂」 #
PR 品質通常在按下 Create pull request 以前就決定了一半。
先確認 branch 只做一件事:
git switch -c feature/export-report
做完修改後,先看狀態與差異:
git status --short
git diff
git diff --staged
再用有意義的 commit 留下脈絡:
git add src/report.py tests/test_report.py
git commit -m "feat: add CSV report export"
git push -u origin feature/export-report
拍拍君會在開 PR 前問四件事:
- 這個 PR 能不能用一句話說完?
- 是否混入格式化、重命名或無關重構?
- 測試是否跟著行為一起變更?
- Reviewer 能否在合理時間內理解風險?
如果一句話說不完, 通常不是描述能力突然離家出走, 而是 PR 真的塞太多東西。
把資料庫 migration、UI 改版、依賴升級、重構拆開, review 速度通常會比「一次全送」更快。
小 PR 不是潔癖, 而是降低理解成本與回滾半徑。
三. Draft PR:提早共享,但不要假裝已經完成 #
功能還沒完成時, 可以先開 Draft PR:
gh pr create \
--draft \
--base main \
--title "feat: export reports as CSV" \
--body-file .github/pull_request_template.md
Draft 很適合這些情境:
- 想先確認設計方向
- CI 需要 PR 事件才會執行
- 變更跨越多個模組,想讓大家提早看到
- 想留下進度,但還不希望正式要求 review
- 需要一個穩定網址討論實作
但 Draft 不是永久停車場。
GitHub 的 Draft PR 不能被合併; 而且在 Draft 階段, 不會自動要求 Code Owners review。
因此描述裡最好明確寫出目前狀態:
## Status
- [x] Core export path
- [x] Unit tests
- [ ] Large-file benchmark
- [ ] Documentation
## Feedback wanted now
- CSV escaping strategy
- Public function name
這樣 reviewer 知道現在該看架構, 不是抓你尚未補完的逗號。
完成後再切換為 Ready for review:
gh pr ready
如果新回饋帶來大幅修改, 也可以暫時轉回 Draft:
gh pr ready --undo
狀態是一種溝通, 不是裝飾用的灰色標籤。
四. PR 描述:替 reviewer 準備一張地圖 #
好描述不需要寫成論文, 但要讓沒參與開發的人回答:
這個變更為什麼合理,而且我要怎麼驗證?
拍拍君常用這個模板:
## Summary
- Add CSV export for filtered reports
- Preserve the active column order
- Stream rows instead of buffering the whole file
## Why
Support needs an offline report for customer follow-up.
## Testing
- `uv run pytest tests/test_report.py`
- Manual export with commas, quotes, and Unicode names
## Risk / rollback
- Export endpoint only; no database schema change
- Roll back by reverting this PR
## Screenshots
<!-- Add before/after images when UI changes -->
Summary 說變更, Why 說理由, Testing 說證據, Risk / rollback 說出事時怎麼辦。
若 PR 解決某個 issue, 可以在描述加入:
Closes #128
當 PR 合併後, GitHub 會自動關閉被支援關鍵字引用的 issue。
但不要只寫 Closes #128 就下班。
Issue 記錄需求, PR 還是要記錄實作選擇與驗證方式。
五. 先 Self-review:不要把低成本錯誤外包給同事 #
送出正式 review request 前, 作者應該先看一次 GitHub 的 Files changed。
本機也可以先檢查:
git fetch origin
git diff --stat origin/main...HEAD
git diff origin/main...HEAD
注意這裡用三個點:
origin/main...HEAD
它從共同祖先比較到目前 branch, 通常更接近 PR 想呈現的變更。
Self-review 時,依序找:
- debug print 與暫存檔
- 不必要的 generated file
- 無關 whitespace 或 formatter 噪音
- 被誤提交的 secret
- 沒有更新的測試與文件
- 命名和錯誤訊息是否清楚
- migration 是否可回復
- 描述與實際 diff 是否一致
如果看到自己都看不懂的區塊, 不要期待 reviewer 靠心電感應理解。
先整理 commit 或補註解, 再把 PR 標成 ready。
六. Request review:把問題交給正確的人 #
PR Ready 後, 可以在 GitHub 側邊欄指定 reviewer, 也可以用 CLI:
gh pr edit --add-reviewer pypy-reviewer
Organization 若有 team:
gh pr edit --add-reviewer my-org/backend-team
請 reviewer 時, 不要只丟網址。
給一段很短的 context:
這個 PR 把 report export 改成串流輸出。
想請你特別看 encoding 與錯誤處理;
API schema 沒變,CI 已全綠。
Review 的三種正式結論是:
Comment:留下意見,但不表達 approve 或阻擋Approve:目前變更可以合併Request changes:需要處理阻擋問題後再合併
Reviewer 可以用 CLI 提交結果:
gh pr review 128 --approve
或要求修改:
gh pr review 128 \
--request-changes \
--body "Please add a test for quoted Unicode fields."
重點不是按哪個按鈕, 而是團隊對「阻擋」有共同語言。
拍拍君推薦 comment 前綴:
blocking: 合併前必須處理
question: 想確認設計理由
suggestion: 建議改善,但不阻擋
nit: 小地方,可留待後續
praise: 這段做得好,請保留
如此作者不用猜每句話背後的嚴重程度。
七. 回應 Review:修程式,也要收斂對話 #
收到 feedback 後, 先理解問題, 不要用「可是我本機能跑」當護身符。
若意見合理, 修改後推新 commit:
git add src/report.py tests/test_report.py
git commit -m "test: cover quoted Unicode fields"
git push
同一條 branch 的新 commit 會自動出現在 PR, 相關 checks 也會重新執行。
回覆時說明你做了什麼:
已補上逗號、雙引號和 Unicode 欄位測試,
並把 encoder 統一改成 utf-8-sig。
如果不同意, 也可以提出具體理由:
這裡保留 generator,因為 production report 可能超過 2 GB。
我補了註解與 benchmark,避免未來被誤改成 list。
技術 review 不是投票比人氣, 理由與證據比「我比較習慣」有用。
處理完成後, 記得 resolve conversation。
有些 repository 會要求所有對話 resolved 才能 merge。
若 push 後需要同一位 reviewer 再看一次, 可以重新 request review, 不要默默假設舊 approval 永遠有效。
八. Checks:綠勾不是全部,但紅叉一定要看 #
PR 頁面的 Checks 可能來自:
- unit tests
- integration tests
- lint 與 format
- type checking
- security scanning
- build
- preview deployment
用 CLI 查看全部 checks:
gh pr checks 128
只看 required checks:
gh pr checks 128 --required
等待 checks 完成:
gh pr checks 128 --watch --fail-fast
gh pr checks 在 checks 尚未完成時可回傳 exit code 8。
因此寫 script 時, 不要把「pending」誤判成普通測試失敗。
想學怎麼建立 Python CI, 可以看 uv + GitHub Actions; 這篇只討論 checks 如何成為 PR 的合併門檻。
Branch protection 或 ruleset 可以要求:
- 一定要透過 PR
- 指定數量的 approval
- Code Owner approval
- required status checks 通過
- branch 必須跟 base 同步
- 所有 review conversation resolved
- 最新可 review push 由另一人 approve
- 必須走 merge queue
Required checks 不是「CI 有跑就好」。
只有被設定為 required 的 check, 才會成為平台強制的合併條件。
而 strict status checks 要求 branch 先跟 base branch 同步; 這能降低舊結果在新主幹上失效的風險, 代價是繁忙 repository 可能不斷重跑 CI。
高流量 repository 可以考慮 merge queue, 讓候選變更在最新 base 與佇列前項組合上再次驗證。
九. 合併前最後檢查:不要被綠色按鈕催眠 #
按 Merge 前, 拍拍君會跑一份短 checklist:
[ ] PR 仍然只處理原本宣稱的範圍
[ ] 最新 diff 已被 reviewer 看過
[ ] blocking comments 已處理
[ ] conversations 已 resolve
[ ] required checks 已通過
[ ] base branch 狀態符合 repository 規則
[ ] migration / deploy 順序清楚
[ ] rollback 方法可執行
[ ] merge title 能在 changelog 中看懂
特別注意「approval 之後又 push」的情況。
Repository 可以設定:
- 新 commit 使舊 approval 失效
- 最新一次可 review push 必須由其他人 approve
這兩種規則都在避免: review 完又偷偷塞入未審查變更。
如果最後一刻修改行為, 就應該讓 checks 與 reviewer 重新確認。
十. 選擇 Merge 方法:以 repository 規則為準 #
GitHub 常見三種方式:
| 方法 | 結果 | 適合情境 |
|---|---|---|
| Merge commit | 保留 branch commits,新增 merge commit | 每個 commit 都有意義,想保留分支脈絡 |
| Squash and merge | 將 PR 壓成 base 上的一個 commit | 一個 PR 就是一個邏輯變更 |
| Rebase and merge | 個別 commit 線性接到 base | commits 已整理好,團隊重視線性歷史 |
更完整的歷史圖與 branch 策略, 已在 Git Branch 策略 說明。
對多數小型產品團隊, 拍拍君偏好 Squash and merge:
- PR 可以保留完整討論
- branch 上允許小步 fixup commit
main上每個 PR 對應一個清楚 commit- revert 一個功能比較直覺
但長期 branch 或精心設計的 commit series, 可能更適合 merge commit 或 rebase merge。
真正重要的是 repository 一致, 不要每個人每天憑心情抽卡。
十一. 安全 Merge:Auto-merge 與 Head SHA #
Checks 還在跑時, 可以設定 auto-merge:
gh pr merge 128 --squash --auto --delete-branch
當 required reviews 與 required checks 都滿足後, GitHub 才會自動合併。
Auto-merge 適合「條件已定義,只是在等機器完成」, 不適合拿來跳過尚未釐清的設計問題。
若你在自動化中想確保合併的是自己剛檢查的版本, 先取得 head SHA:
head_sha=$(gh pr view 128 --json headRefOid --jq .headRefOid)
再要求 SHA 必須一致:
gh pr merge 128 \
--squash \
--match-head-commit "$head_sha" \
--delete-branch
若有人在兩個步驟間 push 新 commit, 合併會被拒絕, 而不是把你沒檢查過的版本送進 main。
請避免把 --admin 當成「紅燈很煩」按鈕。
它會使用管理者權限繞過未滿足的要求; 只有明確、有紀錄的緊急流程才該使用。
十二. 合併後:刪 Branch、同步本機、確認結果 #
PR 合併不是流程結束, 還要把現場收乾淨。
若 merge 時沒用 --delete-branch, 可以在 GitHub 點 Delete branch。
Repository 管理者也能開啟:
Settings
-> General
-> Pull Requests
-> Automatically delete head branches
Branch protection 或 ruleset 仍可能阻止某些 branch 自動刪除。
本機則同步主幹並清理 remote-tracking refs:
git switch main
git pull --ff-only
git fetch --prune
git branch -d feature/export-report
使用 -d 而不是直接 -D, 讓 Git 先檢查 branch 是否已安全合併。
若採 squash merge, 本機 Git 有時不把原 branch 視為「已 merge」, 因為 base 上是新的 squash commit。
這時先在 GitHub 確認 PR 已合併、 重要 commit 已可追溯, 再決定是否使用:
git branch -D feature/export-report
如果你用 worktree 做本機 review, 可以參考 Git worktree 實戰 清理額外工作目錄。
最後不要只看 Merge 成功訊息。
也要確認:
main的 CI 是否成功- deployment 是否成功
- feature flag 是否維持預期狀態
- migration 是否依正確順序執行
- monitoring 是否出現新錯誤
PR 合併成功, 不等於產品變更已安全抵達使用者。
十四. 常見失敗:PR 卡住時先看哪裡? #
1. PR 顯示 Draft,不能 Merge #
先確認工作真的完成, 再執行:
gh pr ready
2. Checks 全綠,仍然不能 Merge #
可能還缺:
- required approval
- Code Owner approval
- unresolved conversation
- branch update
- deployment requirement
- merge queue
綠勾只代表 checks, 不代表所有 repository rules 都已滿足。
3. Push 新 Commit 後 Approval 消失 #
Repository 可能啟用 stale approval dismissal。
請 reviewer 重新看最新 diff, 不要要求管理者直接繞過。
4. PR 越 Review 越大 #
Review 時順便加新功能, 最後會讓已看過的範圍不斷漂移。
把額外需求開 issue 或 follow-up PR, 讓目前 PR 保持可完成。
5. CI 一直因 Base Branch 更新而重跑 #
若 repository 很繁忙, strict up-to-date requirement 可能造成排隊與重跑。
這不是叫你關掉測試, 而是評估 merge queue 是否更適合。
6. Merge 後找不到原本的 Commit SHA #
Squash merge 與 rebase merge 都可能產生新的 commit SHA。
以 PR 編號、merge commit 或 squash commit 作為追蹤入口, 不要假設 feature branch 的 SHA 永遠原樣留在 main。
結語:讓每次 Merge 都有足夠證據 #
一個好的 Pull Request, 不是行數特別少、 emoji 特別多、 或 reviewer 特別快按 Approve。
它應該讓團隊清楚知道:
- 變更目標是什麼
- 實際 diff 是否符合目標
- 人類 review 過哪些風險
- 自動 checks 驗證了什麼
- 合併方式如何影響歷史
- 出事時如何回復
先用 Draft 提早共享, Ready 前 self-review, 把 PR 描述寫成 reviewer 的地圖, 讓 required checks 與 ruleset 守住底線, 最後用一致策略安全合併並清理 branch。
這套流程看起來多幾步, 實際上是在省掉事後追問、重看、救火與通靈。
下次準備按 Create pull request 時, 先別只想「怎麼 merge」。
先問:
我是否已經給團隊足夠證據,可以放心做這個決定?