快轉到主要內容
  1. 教學文章/

GitHub Pull Request 實戰:Draft、Review、Checks 與安全合併

·9 分鐘· loading · loading · ·
GitHub Pull Request Code Review Branch Protection Ci Git
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
版本控制: Git - 本文屬於一個選集。
§ 14: 本文

featured

一. 前言: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

如果你想先設計 mainfeature/*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 前問四件事:

  1. 這個 PR 能不能用一句話說完?
  2. 是否混入格式化、重命名或無關重構?
  3. 測試是否跟著行為一起變更?
  4. 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」。

先問:

我是否已經給團隊足夠證據,可以放心做這個決定?

延伸閱讀
#

版本控制: Git - 本文屬於一個選集。
§ 14: 本文

相關文章

Git rerere 實戰:記住衝突解法,讓 Rebase 與 Merge 不再重做
·8 分鐘· loading · loading
Git Rerere Merge-Conflict Rebase Merge Version-Control Developer-Tools
Git reflog 救援實戰:找回 reset、rebase 後消失的 Commit
·9 分鐘· loading · loading
Git Reflog Recovery Reset Rebase Commit Version-Control
Git worktree 實戰:同時開多個分支、平行測試與安全清理完全攻略
·11 分鐘· loading · loading
Git Worktree Branch Workflow Version-Control
Git bisect 實戰:快速定位壞 commit 與除錯流程完全攻略
·9 分鐘· loading · loading
Git Bisect Debugging Regression Version-Control
Git cherry-pick 實戰:精準搬運 commit、修補 hotfix 與分支同步
·11 分鐘· loading · loading
Git Cherry-Pick Hotfix Commit Version-Control
Git stash 實戰:暫存工作現場、切換任務與 patch 管理完全攻略
·11 分鐘· loading · loading
Git Git-Stash Patch Workflow Version-Control