gh stack sync pushed a stack branch's merge commit directly onto the trunk branch (`main`)
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
調査の方向性
gh stack sync の push ステップと、そのエントリーポイントである Fast-forward trunk および Cascade rebase から始め、文書化されている 4 ブランチの stack シナリオを再現します。完了の条件は、track されている stack ブランチだけが push され、最下位のブランチが merge commit を作成せずに実際の rebase を使用し、trunk の refspec または push の不一致が成功を報告するのではなく、明示的に失敗することです。
索引モデルが issue の本文から書いたものです。
説明
Summary
Running gh stack sync on an existing 4-branch stack resulted in origin/main being advanced to a commit that merges a stack branch's feature commit directly into trunk — outside of any PR merge. The affected PR was never marked merged by GitHub (mergedAt: null), yet its code was live on main. sync's own output reported full success at every step and gave no indication that a trunk branch had been mutated.
Environment
gh-stackv0.1.0gh2.96.0- git 2.50.1 (Apple Git-155)
- macOS 15.7.4, arm64
Stack layout at the time
main (trunk)
└─ feature-layer-1 (PR #101, base: main)
└─ feature-layer-2 (PR #102, base: feature-layer-1)
└─ feature-layer-3 (PR #103, base: feature-layer-2)
└─ feature-layer-4 (PR #104, base: feature-layer-3)
The stack had already been created with gh stack init + gh stack submit --auto. Between submit and this incident, main had advanced (unrelated PRs merged), and one of the stack branches had also been manually rebased with plain git (outside gh stack) to resolve an unrelated conflict. Also the first stacked pr was approved.
Reproduction
- With the stack above checked out locally (on the topmost branch), run:
gh stack sync - Observed output (abbreviated, all steps reported success):
✓ Fetched latest main from origin ✓ Trunk main fast-forwarded to <sha-main-new> ✓ Fast-forwarded feature-layer-2 to <sha-l2-new> Rebasing stack ... ✓ Rebased feature-layer-1 onto main ✓ Rebased feature-layer-2 onto feature-layer-1 ✓ Rebased feature-layer-3 onto feature-layer-2 ✓ Rebased feature-layer-4 onto feature-layer-3 Pushing 4 branches to origin... ✓ Pushed 4 branches Syncing PRs ... ✓ PR #101 (feature-layer-1) — Open ✓ PR #102 (feature-layer-2) — Open ✓ PR #103 (feature-layer-3) — Open ✓ PR #104 (feature-layer-4) — Open ✓ Stack on GitHub is up to date with 4 PRs (stack #501) ✓ Stack synced Stacked on main (<sha-main-new>) - After this ran,
origin/main's tip (verified viagit ls-remote origin refs/heads/mainand the GitHub APIGET /repos/{owner}/{repo}/branches/main) was a commit that did not exist before this command ran.
Evidence that this was a sync-internal push, not a manual one
git reflog show main(local trunk branch) shows only local fast-forwards (branch: Reset to <sha-main-new>...) — it was never locally at the commit that ended up onorigin/main.git reflog show feature-layer-1(the bottom-of-stack branch, whose base is trunk) shows:
Note the entry type:<sha-A> feature-layer-1@{0}: commit (merge): Merge branch 'main' of https://github.com/.../<repo> into feature-layer-1 <sha-B> feature-layer-1@{1}: commit: feat: <the branch's own feature commit> <sha-C> feature-layer-1@{2}: branch: Created from origin/maincommit (merge), not a rebase. This is despite the CLI's own printed log for this exact step reading"✓ Rebased feature-layer-1 onto main".sha-Aabove is byte-identical toorigin/main's new tip aftersyncran (confirmed viagit diff sha-A <new-main-tip>= empty, and matching author/committer/date/message via the GitHub commits API).origin/feature-layer-1(the actual remote branch for that PR) was, aftersync, at a different SHA thansha-A— a proper rebase result, distinct commit. So the merge commit did not simply get pushed to "the wrong copy of the same intended branch"; it ended up onrefs/heads/mainspecifically, a ref thatsyncshould never write to per its own documented behavior ("Fast-forward trunk" is described as updating the local trunk ref to match remote, not pushing to it).- No
git pushtargetingmainwas run manually at any point — every manualgit pushin this session named a specific feature branch explicitly.
Expected behavior
Per the gh-stack README, sync step 3 ("Fast-forward trunk") only fast-forwards the local trunk ref to match origin, and step 4 ("Cascade rebase") rebases stack branches onto their parents. It should never produce a merge commit, and it should never push anything to the trunk ref itself. sync should not be able to advance origin/main under any circumstances; only a GitHub PR merge should do that.
Actual behavior
- For the branch directly based on trunk, the "rebase" step performed a
git mergeof trunk into the branch instead of a rebase (confirmed via reflog entry type), while still reporting it as"Rebased ... onto main". - That merge commit was pushed to
origin/mainduring the subsequent "Push — pushes all branches" step, even though only the 4 stack branches were named/intended as push targets. syncprinted full success (✓ Pushed 4 branches,✓ Stack synced) with no warning, no diff summary, and no indication that a ref outside the 4 stack branches had been written to.- This resulted in a stack branch's code landing on the trunk branch of a production repository, completely bypassing that branch's own PR (which GitHub still reported as open/unmerged).
Impact
This landed code directly on main in a production repository, with no error, warning, or confirmation prompt from the tool. For any repo where main/trunk is a protected, review-gated branch, this defeats that protection silently.
Related
- #417 ("gh stack sync reports success after its atomic push fails") documents a related but distinct failure in the same step of
sync— the atomic push not accurately reflecting/reporting what happened to remote refs. That issue is about a push failing while being reported as success; this report is about a push landing somewhere it should never have targeted at all, also reported as success. Both point to the same underlying step ("Push — pushes all branches" insync) having an unreliable relationship between what it does to remote refs and what it tells the user.
Suggested fix direction
sync's push step should construct its list of push refspecs explicitly from the known stack branch names only, and should hard-fail (never silently succeed) if a trunk/base ref is ever included in that list or if any pushed ref does not exactly match one of the stack's tracked branch names.- The "Fast-forward trunk" and "Cascade rebase" steps should be implemented with real
git rebase, notgit merge, for the bottom-of-stack branch — and the reflog/commit shape should be asserted in tests to catch exactly this kind of mislabeling. - Consider printing the actual SHA delta being pushed per branch (old → new) in
sync's output, so a trunk-ref write would be visible immediately rather than silent.
- 主要言語
- Go
- スター
- 1.5k
- フォーク
- 73
- 平均マージ
- 1日 8時間
- マージ済み PR(30日)
- 7
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
github/gh-stack のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 92/100
-
feature request topic: cli - general
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
-
feature request topic: auto-merge
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
-
bug topic: docs
難易度 1/5 1時間未満 初心者へのやさしさ 68/100
-
feature request topic: cli - view
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
github/gh-stack の issue をすべて見る
似ている issue
-
kind/bug needs-triage
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
matrixorigin/matrixone#29223 ·
-
needs-acceptance wg/data-plane-networking
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
vllm-project/semantic-router#4024 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
alexgorbatchev/dotfiles#107 ·
-
難易度 1/5 1時間未満 初心者へのやさしさ 84/100