Add stacked PRs design doc

17f19edb2514 · AtlantisPleb · 2026-08-22T10:11:48-05:00 · parent e8fe7a4088c0

Add stacked PRs design doc

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 91 · 2026-08-22T15:11:49.582400Z

Changed files

  • added docs/stacked-prs.md

Diff

1 file changed, +2228 -0

docs/stacked-prs.md added +2228

@@ -0,0 +1,2228 @@

1
# Stacked PRs: the problem, GitHub’s design, and how to build them from scratch
2
3
As of **August 22, 2026**, GitHub’s native Stacked Pull Requests feature is in public preview. GitHub announced the preview on July 30, 2026. ([The GitHub Blog][1])
4
5
The most important idea is this:
6
7
> **A stacked PR system is not merely a chain of pull requests with unusual base branches. It is an orchestration layer over ordinary Git branches and PRs.**
8
9
The branch chain gives you small, reviewable diffs. The first-class stack abstraction gives you everything else: ordering, policy inheritance, cascading rebases, stack-aware CI, merge-queue behavior, conflict handling, and the ability to merge a contiguous portion of the stack as one operation.
10
11
---
12
13
## 1. The basic model
14
15
Suppose a developer wants to ship a feature in three logical pieces:
16
17
1. Add the database schema.
18
2. Add the API.
19
3. Add the user interface.
20
21
Without stacking, the developer has two bad options:
22
23
* Put everything in one large PR.
24
* Wait for each PR to merge before beginning the next.
25
26
With stacked PRs, the Git history looks like this:
27
28
```text
29
A                         main
30
 \
31
  B                       feature/schema
32
   \
33
    C                     feature/api
34
     \
35
      D                   feature/ui
36
```
37
38
The corresponding pull requests are:
39
40
```text
41
PR 101: feature/schema → main
42
PR 102: feature/api    → feature/schema
43
PR 103: feature/ui     → feature/api
44
```
45
46
GitHub calls PR 101 the **bottom** of the stack because it is closest to the trunk branch. PR 103 is the **top** because it is furthest away. Each PR presents only the changes introduced by its own layer, even though the top branch contains the cumulative result of every layer below it. ([GitHub Docs][2])
47
48
Formally, let a stack be:
49
50
```text
51
S = [P₁, P₂, …, Pₙ]
52
```
53
54
where:
55
56
```text
57
base(P₁) = trunk
58
base(Pᵢ) = head(Pᵢ₋₁), for i > 1
59
```
60
61
For a healthy, fully restacked stack, the current commit graph should also satisfy:
62
63
```text
64
isAncestor(trunkTip, H₁)
65
isAncestor(H₁, H₂)
66
67
isAncestor(Hₙ₋₁, Hₙ)
68
```
69
70
where `Hᵢ` is the current head commit of the branch belonging to `Pᵢ`.
71
72
There are two different notions of “base”:
73
74
```text
75
directBase(Pᵢ) =
76
    trunk,               when i = 1
77
    head branch of Pᵢ₋₁, when i > 1
78
79
effectiveBase(Pᵢ) = trunk
80
```
81
82
That distinction is fundamental:
83
84
* The **direct base** determines what the reviewer sees in the PR diff.
85
* The **effective base** determines which branch protections, workflows, CODEOWNERS rules, and merge policies apply.
86
87
GitHub explicitly applies the stack’s trunk-branch policies to every PR in the stack, rather than allowing an intermediate feature branch to weaken or redefine policy for upper layers. ([GitHub Docs][3])
88
89
---
90
91
# 2. What problem stacked PRs solve
92
93
## 2.1 They decouple authoring throughput from merge latency
94
95
In a conventional PR workflow, a developer often finishes one change and then waits:
96
97
```text
98
write → open PR → wait for review → merge → begin next change
99
```
100
101
The waiting may take hours or days. Yet the developer already knows what the next dependent change should be.
102
103
A stack changes the workflow to:
104
105
```text
106
write layer 1 → open PR 1
107
write layer 2 → open PR 2
108
write layer 3 → open PR 3
109
```
110
111
Review and implementation proceed concurrently.
112
113
The developer does not need to merge schema work before writing API work. They can build the API on the schema branch while preserving a separate review boundary.
114
115
GitHub specifically presents this as a way to continue building dependent work without waiting for earlier PRs to merge. ([GitHub Docs][2])
116
117
## 2.2 They make reviews smaller without destroying dependency structure
118
119
A large feature might contain:
120
121
* schema changes,
122
* migrations,
123
* server types,
124
* business logic,
125
* API endpoints,
126
* client state,
127
* UI,
128
* tests,
129
* instrumentation.
130
131
A single 3,000-line PR is difficult to review. Splitting it into unrelated PRs is also difficult because later pieces cannot compile or function without earlier ones.
132
133
Stacking preserves the real dependency:
134
135
```text
136
UI depends on API
137
API depends on schema
138
```
139
140
while giving reviewers focused units:
141
142
```text
143
PR 1: Review the schema design.
144
PR 2: Review the API implementation.
145
PR 3: Review the UI.
146
```
147
148
This is qualitatively different from merely breaking work into commits. Each layer gets its own:
149
150
* discussion,
151
* reviewers,
152
* approval state,
153
* CI result,
154
* merge policy,
155
* audit trail,
156
* issue linkage,
157
* release metadata.
158
159
## 2.3 They make sequencing explicit
160
161
The stack is an explicit declaration that:
162
163
```text
164
P₁ must land before P₂
165
P₂ must land before P₃
166
```
167
168
Without a first-class s[118;1:3utack, this information is usually buried in:
169
170
* branch ancestry,
171
* PR descriptions,
172
* “Depends on #123” comments,
173
* naming conventions,
174
* or the author’s memory.
175
176
A first-class stack turns dependency order into enforceable data.
177
178
## 2.4 They automate cascading branch maintenance
179
180
Suppose review of the bottom PR requests a change:
181
182
```text
183
A---B---C---D
184
185
  modify B
186
```
187
188
After modifying or rebasing `B`, the old `C` and `D` are based on the wrong history. They must be replayed:
189
190
```text
191
A---B'
192
     \
193
      C'
194
       \
195
        D'
196
```
197
198
Doing this manually is error-prone. A stack system can perform a **waterfall rebase** from the bottom upward, stopping at the first conflict and retaining enough state to continue or abort.
199
200
GitHub supports cascading rebases, and its open-source `gh-stack` CLI persists local stack and interrupted-rebase state under `.git/gh-stack`. ([GitHub Docs][2])
201
202
## 2.5 They prevent policy bypass through intermediate branches
203
204
A naïve chained-PR implementation often evaluates this PR:
205
206
```text
207
feature/api → feature/schema
208
```
209
210
as though `feature/schema` were its true destination.
211
212
That can create serious inconsistencies:
213
214
* Workflows configured for `main` might not run.
215
* `main`’s required checks might not apply.
216
* CODEOWNERS from a lower feature branch might replace the protected version.
217
* A lower layer could alter policy and thereby change the requirements for upper layers.
218
219
GitHub avoids this by evaluating every layer against the stack’s ultimate base branch. A workflow whose pull-request filter targets `main` can therefore run for every PR in a stack rooted at `main`, even when a PR’s direct base is another feature branch. ([GitHub Docs][3])
220
221
## 2.6 They make merging a coordinated operation
222
223
If three dependent PRs are ready, naïvely merging them requires:
224
225
1. Merge PR 1.
226
2. Retarget or rebase PR 2.
227
3. Wait for checks.
228
4. Merge PR 2.
229
5. Retarget or rebase PR 3.
230
6. Wait again.
231
7. Merge PR 3.
232
233
A stack-aware system can validate the entire contiguous prefix and merge it through a single user operation.
234
235
GitHub permits merging from the bottom through a selected PR. Selecting the top PR merges all PRs below it as well. It does not allow skipping a lower PR and merging a middle layer independently. ([GitHub Docs][4])
236
237
## 2.7 They are particularly well suited to coding agents
238
239
A coding agent naturally produces sequential units:
240
241
```text
242
Task 1: Add data model
243
Task 2: Add service
244
Task 3: Add endpoint
245
Task 4: Add frontend
246
Task 5: Add tests
247
```
248
249
Without stacks, the agent either creates a giant PR or must wait for humans between tasks. With stacks, it can maintain small review boundaries while continuing autonomously.
250
251
GitHub explicitly associates stacked PRs with higher-volume development and AI-assisted coding workflows. ([GitHub Docs][2])
252
253
---
254
255
# 3. What stacked PRs do not solve
256
257
Stacked PRs are a **linear dependency model**, not a universal dependency graph.
258
259
They do not naturally represent this:
260
261
```text
262
        PR B
263
       /    \
264
PR A          PR D
265
       \    /
266
        PR C
267
```
268
269
That is a DAG, not a stack.
270
271
They also do not solve:
272
273
* Cross-repository atomic changes.
274
* Independent changes that happen to be authored together.
275
* Semantic merge conflicts.
276
* Poor review discipline.
277
* Excessive CI cost.
278
* Long-lived branch divergence.
279
* Release-train coordination across many unrelated stacks.
280
* The need for feature flags when partially landed work must remain inactive.
281
282
GitHub’s current implementation is restricted to PRs in the same repository, which keeps the public model linear and avoids cross-repository branch, permission, and object-storage complications. ([GitHub Docs][2])
283
284
A useful rule is:
285
286
> Stack changes that must land in a specific linear order. Do not stack changes merely because one developer authored them consecutively.
287
288
---
289
290
# 4. Manual chained PRs versus a first-class stack
291
292
Developers have manually created stacked PRs for years by pointing each PR at the branch below it. That supplies only part of the system.
293
294
| Capability                          |  Manual branch chain | First-class stack |
295
| ----------------------------------- | -------------------: | ----------------: |
296
| Small per-layer diff                |                  Yes |               Yes |
297
| Explicit stack identity             |                   No |               Yes |
298
| Durable ordering                    |             Informal |          Enforced |
299
| Trunk policy applied to every layer |           Usually no |               Yes |
300
| Stack-aware CI metadata             |                   No |               Yes |
301
| Cascading rebase                    | Manual/tool-specific |        Integrated |
302
| Merge contiguous prefix             |     Multiple actions |     One operation |
303
| Merge-queue ordering                |              Unaware |       Stack-aware |
304
| Conflict recovery state             |               Ad hoc | Durable operation |
305
| Stack lifecycle/webhooks            |                   No |               Yes |
306
| Concurrency control                 |                   No |               Yes |
307
308
A branch chain answers:
309
310
> “What commit is this branch based on?”
311
312
A stack object answers:
313
314
> “Which PRs constitute one ordered unit of dependent work, what trunk do they ultimately target, what policy applies to them, and what operations may be performed across the group?”
315
316
That is why a serious implementation needs a server-side stack object.
317
318
---
319
320
# 5. How GitHub implemented stacked PRs
321
322
GitHub’s exact internal service architecture is not publicly documented. However, its public APIs, documentation, UI behavior, and open-source CLI reveal the external data model and much of the operational design.
323
324
## 5.1 Ordinary Git branches remain the storage model
325
326
GitHub did not introduce a new Git object type.
327
328
A stack still consists of:
329
330
* ordinary commits,
331
* ordinary branch refs,
332
* ordinary pull requests,
333
* ordinary base/head branch relationships.
334
335
The additional stack is collaboration metadata that links and orders those PRs. This is evident from GitHub’s stack API and from `gh-stack`, which creates or updates regular branches and PRs before creating or extending the server-side stack. ([GitHub Docs][5])
336
337
That is a good compatibility decision. Existing Git clients, fetches, branch protections, commit pages, and PR machinery continue to work.
338
339
## 5.2 GitHub has a first-class server-side stack object
340
341
GitHub’s REST representation includes concepts such as:
342
343
* stack ID,
344
* repository-scoped stack number,
345
* ultimate base ref,
346
* ordered pull requests,
347
* open/completed state,
348
* timestamps.
349
350
The creation API receives PR numbers ordered from bottom to top and validates that each PR’s base branch matches the preceding PR’s head branch. GitHub also exposes an operation that appends a PR to the top of an existing stack and returns a conflict response if a concurrent modification prevents the update. ([GitHub Docs][5])
351
352
Its GraphQL model exposes a `PullRequestStack` and ordered `PullRequestStackEntry` objects. Each entry has a one-based position and links to a PR. ([GitHub Docs][6])
353
354
A likely normalized representation is therefore approximately:
355
356
```text
357
pull_request_stacks
358
  id
359
  repository_id
360
  number
361
  base_ref
362
  state
363
364
pull_request_stack_entries
365
  id
366
  stack_id
367
  pull_request_id
368
  position
369
```
370
371
That table design is an inference, not a claim about GitHub’s private schema. But it closely matches the public object model.
372
373
## 5.3 The stack is durable rather than inferred on every request
374
375
GitHub could theoretically inspect PR bases and reconstruct branch chains dynamically. It instead provides APIs for explicitly creating, extending, and dissolving stacks.
376
377
That matters because branch topology alone is ambiguous.
378
379
For example:
380
381
* A developer may temporarily base one feature branch on another without intending a stack.
382
* A PR could be retargeted accidentally.
383
* Closed and merged PRs complicate inference.
384
* A branch could appear in several possible chains.
385
* Operations need a stable ID and optimistic-concurrency boundary.
386
* Webhooks and audit logs need an identifiable object.
387
388
The Git graph should be treated as a structural invariant of the stack, not as its sole identity.
389
390
## 5.4 The local CLI tracks more than branch names
391
392
GitHub’s `gh-stack` code maintains local metadata and records, for each stacked branch, both its current head and a base commit boundary. The project’s source describes that base as the parent branch’s head SHA at the last synchronization or rebase, used to identify the commits unique to the layer. ([GitHub][7])
393
394
This is one of the most important implementation details in the entire system.
395
396
Consider:
397
398
```text
399
A---B---C
400
401
main = A
402
PR 1 head = B
403
PR 2 head = C
404
```
405
406
Now PR 1 is squash-merged, producing commit `S`:
407
408
```text
409
A---S              main
410
411
A---B---C          old feature history
412
```
413
414
To restack PR 2, the system must replay only the work after `B`:
415
416
```bash
417
git rebase --onto S B C
418
```
419
420
It must **not** replay everything after `A`, because that would reapply PR 1’s changes on top of the squashed version.
421
422
The old boundary SHA `B` tells the system where PR 2’s unique layer begins.
423
424
Branch names are insufficient because `feature/schema` may have moved, been deleted, or been replaced by a squash commit. A robust stack implementation must retain commit boundaries as immutable OIDs.
425
426
## 5.5 GitHub uses waterfall rebasing
427
428
A cascade rebase proceeds from the trunk upward:
429
430
```text
431
rebase layer 1 onto current trunk
432
rebase layer 2 onto new layer 1
433
rebase layer 3 onto new layer 2
434
435
```
436
437
If a conflict occurs, the operation pauses at the affected layer. GitHub’s CLI supports continuing or aborting an interrupted stack rebase and persists the necessary local state. ([GitHub][8])
438
439
The direction matters. Rebasing the top first would use parent commits that are about to be replaced.
440
441
## 5.6 Pushes use optimistic ref safety
442
443
GitHub’s CLI pushes active branches with per-ref `--force-with-lease`, protecting against overwriting a remote branch that changed unexpectedly.
444
445
The CLI documentation notes that its multi-branch push is not completely atomic: one branch update can succeed while another fails. It therefore needs reconciliation and retry behavior. ([GitHub][8])
446
447
A GitHub clone that owns its Git backend can provide stronger server-side semantics through a batch compare-and-swap ref API.
448
449
## 5.7 GitHub virtualizes the policy base
450
451
GitHub distinguishes the immediate PR base from the stack’s trunk base.
452
453
For all stacked layers, GitHub evaluates:
454
455
* branch protection,
456
* required checks,
457
* workflow applicability,
458
* required reviews,
459
* CODEOWNERS,
460
461
against the stack’s ultimate target branch. In particular, changing a CODEOWNERS file in a lower PR does not alter ownership requirements for upper PRs; the policy definition is taken from the stack base. ([GitHub Docs][3])
462
463
This is both a security requirement and a usability requirement.
464
465
## 5.8 GitHub includes stack context in CI events
466
467
GitHub’s pull-request event payload exposes stack metadata including:
468
469
* stack number,
470
* stack size,
471
* the PR’s position,
472
* the effective base ref and SHA.
473
474
That lets CI systems make stack-aware decisions. ([GitHub Docs][3])
475
476
For example:
477
478
```yaml
479
if: github.event.pull_request.stack.position ==
480
    github.event.pull_request.stack.size
481
```
482
483
could run a particularly expensive end-to-end suite only for the top layer, while cheaper required checks still run for every layer.
484
485
The downside is CI multiplication. A ten-layer stack may trigger ten sets of workflows, and changing the bottom can invalidate all ten.
486
487
## 5.9 GitHub supports contiguous-prefix merges
488
489
Suppose the stack is:
490
491
```text
492
PR 1
493
PR 2
494
PR 3
495
PR 4
496
```
497
498
Permitted operations include:
499
500
```text
501
merge PR 1
502
merge PRs 1–2
503
merge PRs 1–3
504
merge PRs 1–4
505
```
506
507
But not:
508
509
```text
510
merge only PR 2
511
merge PRs 2–3 while PR 1 remains open
512
```
513
514
All selected layers and all layers below the selected one must satisfy their requirements. ([GitHub Docs][4])
515
516
This follows directly from the dependency model: a layer cannot land without its prerequisites.
517
518
## 5.10 Stack merging is asynchronous
519
520
GitHub exposes an asynchronous merge endpoint for stacked PRs. The request can include:
521
522
* expected head SHA,
523
* merge method,
524
* direct merge versus merge queue,
525
* and the selected PR.
526
527
The server returns an operation UUID and an HTTP `202 Accepted`; the client polls for the result. GitHub also returns conflicts for an already-running incompatible operation. ([GitHub Docs][9])
528
529
This is the correct architectural model. A stack merge may need to:
530
531
* validate several PRs,
532
* query checks and reviews,
533
* create multiple commits,
534
* interact with the merge queue,
535
* update several branches,
536
* close several PRs,
537
* generate events,
538
* and restack remaining upper branches.
539
540
That should not be implemented as a fragile synchronous HTTP request.
541
542
## 5.11 GitHub supports the normal merge methods
543
544
GitHub documents stack behavior for:
545
546
* **Merge commit:** one merge commit for the selected group, preserving the commits in the selected branch history.
547
* **Squash:** one squashed commit per PR.
548
* **Rebase:** commits from each PR are replayed in stack order.
549
550
For merge-queue operation, stack ordering is preserved, and ejecting a lower member necessarily affects the members above it. ([GitHub Docs][10])
551
552
## 5.12 “Atomic” has an important caveat
553
554
GitHub describes stack merging as one coordinated operation, but its troubleshooting documentation acknowledges that an unexpected failure may occur after some lower PRs have already landed. In that case, the landed PRs remain merged and the failed and upper PRs remain open. ([GitHub Docs][11])
555
556
Therefore, “atomic” should be understood as:
557
558
* one user-visible operation,
559
* one preflight,
560
* one ordered orchestration,
561
562
not necessarily a mathematically strict all-or-nothing transaction spanning Git refs, SQL rows, CI, queues, and webhooks.
563
564
A new implementation can make Git-ref movement more atomic than this, but it still needs durable recovery across storage systems.
565
566
## 5.13 Current limitations
567
568
GitHub’s documented preview currently has several meaningful constraints:
569
570
* All PRs must be in the same repository.
571
* GitHub Desktop does not provide stack management.
572
* Auto-merge is not supported for stacked PRs.
573
* Merge queue is supported.
574
* REST and webhooks support stack operations; GraphQL exposes a read model.
575
* The feature remains subject to change during public preview. ([GitHub Docs][2])
576
577
---
578
579
# 6. Architecture for implementing stacked PRs in a GitHub clone
580
581
A clean implementation should separate the system into seven responsibilities:
582
583
```text
584
┌───────────────────────────────────────────────────────────────┐
585
│ Web UI / CLI / API                                            │
586
├──────────────────────────────┬────────────────────────────────┤
587
│ Pull Request Service         │ Stack Service                  │
588
│ reviews, comments, diffs     │ membership, ordering, state    │
589
├──────────────────────────────┼────────────────────────────────┤
590
│ Policy + CI Planner          │ Operation Orchestrator         │
591
│ effective base, checks       │ rebase, merge, recovery        │
592
├──────────────────────────────┴────────────────────────────────┤
593
│ Merge Queue                                                   │
594
├───────────────────────────────────────────────────────────────┤
595
│ Git Data Plane                                                │
596
│ objects, refs, diffs, merge/replay, CAS ref transactions      │
597
├───────────────────────────────────────────────────────────────┤
598
│ Metadata DB + Outbox + Workers                                │
599
└───────────────────────────────────────────────────────────────┘
600
```
601
602
In a forge where Git object storage is already separated from collaboration metadata, the stack should live in the **collaboration database and orchestration layer**.
603
604
The Git data plane should not know what a “pull request stack” is. It should expose generic primitives:
605
606
```text
607
ResolveRef
608
ReadCommitGraph
609
IsAncestor
610
ComputeDiff
611
MergeTrees
612
ReplayCommits
613
CreateCommit
614
BatchUpdateRefsCAS
615
PinObjects
616
```
617
618
The stack service composes those primitives.
619
620
That separation keeps Git storage reusable for:
621
622
* ordinary PRs,
623
* merge queues,
624
* coding-agent workspaces,
625
* patch previews,
626
* release branches,
627
* mirrors,
628
* and ephemeral environments.
629
630
---
631
632
# 7. Recommended data model
633
634
## 7.1 Stack records
635
636
```sql
637
CREATE TABLE pull_request_stacks (
638
    id                  UUID PRIMARY KEY,
639
    repository_id       UUID NOT NULL,
640
    number              BIGINT NOT NULL,
641
    trunk_ref_name      TEXT NOT NULL,
642
    state               TEXT NOT NULL,
643
    version             BIGINT NOT NULL DEFAULT 1,
644
    created_by          UUID NOT NULL,
645
    created_at          TIMESTAMPTZ NOT NULL,
646
    updated_at          TIMESTAMPTZ NOT NULL,
647
648
    UNIQUE (repository_id, number)
649
);
650
```
651
652
Suggested states:
653
654
```text
655
OPEN
656
COMPLETED
657
DISSOLVED
658
```
659
660
Avoid encoding transient operational states directly on the stack. Put those in an operation table.
661
662
## 7.2 Ordered entries
663
664
```sql
665
CREATE TABLE pull_request_stack_entries (
666
    id                  UUID PRIMARY KEY,
667
    stack_id            UUID NOT NULL,
668
    pull_request_id     UUID NOT NULL,
669
    position            INTEGER NOT NULL,
670
671
    boundary_oid        BYTEA NOT NULL,
672
    observed_head_oid   BYTEA NOT NULL,
673
674
    removed_at          TIMESTAMPTZ,
675
676
    UNIQUE (stack_id, position)
677
);
678
```
679
680
`boundary_oid` is the critical field.
681
682
For each layer:
683
684
```text
685
bottom entry boundary = trunk SHA at its last valid restack
686
upper entry boundary  = previous layer’s head SHA at its last valid restack
687
```
688
689
Use an opaque Git OID type or a representation capable of storing both SHA-1 and SHA-256 object IDs. Do not hard-code a 40-character SHA-1 schema.
690
691
Add a partial uniqueness constraint so a PR cannot be an active member of two stacks simultaneously.
692
693
## 7.3 Durable operations
694
695
```sql
696
CREATE TABLE stack_operations (
697
    id                      UUID PRIMARY KEY,
698
    stack_id                UUID NOT NULL,
699
    kind                    TEXT NOT NULL,
700
    state                   TEXT NOT NULL,
701
    target_position         INTEGER,
702
    expected_stack_version  BIGINT NOT NULL,
703
    idempotency_key         TEXT NOT NULL,
704
705
    request                 JSONB NOT NULL,
706
    snapshot                JSONB,
707
    planned_result          JSONB,
708
    error                   JSONB,
709
710
    created_at              TIMESTAMPTZ NOT NULL,
711
    started_at              TIMESTAMPTZ,
712
    completed_at            TIMESTAMPTZ,
713
714
    UNIQUE (stack_id, idempotency_key)
715
);
716
```
717
718
Operation kinds:
719
720
```text
721
CREATE
722
APPEND
723
RESTRUCTURE
724
REBASE
725
MERGE
726
QUEUE
727
UNSTACK
728
DISSOLVE
729
REPAIR
730
```
731
732
Operation states:
733
734
```text
735
PENDING
736
RUNNING
737
WAITING_FOR_CONFLICT_RESOLUTION
738
WAITING_FOR_CHECKS
739
SUCCEEDED
740
PARTIALLY_SUCCEEDED
741
FAILED
742
CANCELLED
743
```
744
745
For detailed recovery, add:
746
747
```sql
748
stack_operation_steps (
749
    operation_id,
750
    sequence,
751
    entry_id,
752
    action,
753
    state,
754
    old_oid,
755
    new_oid,
756
    error
757
);
758
```
759
760
## 7.4 Object retention
761
762
Every `boundary_oid` must remain reachable.
763
764
A commit that is no longer referenced by a public branch can eventually be garbage-collected. If that commit is the only record of a layer boundary, future restacking becomes unreliable.
765
766
Use either:
767
768
* hidden internal refs, such as `refs/internal/stacks/<stack-id>/<entry-id>`,
769
* object leases,
770
* or an explicit Git-object retention table understood by the garbage collector.
771
772
Do not advertise those internal refs to normal clients.
773
774
---
775
776
# 8. Structural invariants
777
778
Separate **structural validity** from **current mergeability**.
779
780
## 8.1 Hard structural invariants
781
782
A stack should require:
783
784
```text
785
All PRs belong to the same repository.
786
All entries are unique.
787
Positions are contiguous: 1…n.
788
The first PR targets the stack trunk.
789
Every later PR targets the preceding PR’s head branch.
790
No PR belongs to two active stacks.
791
No branch appears twice.
792
No cycle exists.
793
```
794
795
While a PR is stacked, generic “change base branch” operations should either:
796
797
* be routed through the stack service,
798
* or be rejected with an explanation.
799
800
Otherwise an ordinary PR update can silently corrupt the stack.
801
802
## 8.2 Dynamic validity
803
804
A stack can remain a recognized stack while temporarily needing a rebase.
805
806
For example:
807
808
```text
809
A---X                   main
810
 \
811
  B---C                 stack branches
812
```
813
814
The stack metadata remains valid, but `main`’s new commit `X` is not an ancestor of the bottom branch.
815
816
Represent that as:
817
818
```text
819
stack state: OPEN
820
health: NEEDS_REBASE
821
```
822
823
Other useful health states:
824
825
```text
826
HEALTHY
827
NEEDS_REBASE
828
CONFLICTED
829
MISSING_REF
830
HEAD_CHANGED
831
POLICY_BLOCKED
832
OPERATION_IN_PROGRESS
833
```
834
835
Do not dissolve the stack merely because its Git topology is temporarily stale.
836
837
## 8.3 Mergeability invariants
838
839
Before merging a selected prefix, require:
840
841
```text
842
current trunk tip is an ancestor of layer 1 head
843
layer 1 head is an ancestor of layer 2 head
844
845
selected layers form the lowest contiguous open prefix
846
all expected head SHAs still match
847
all required checks are current
848
all required approvals are current
849
no conflicting operation is active
850
```
851
852
For a simpler first version, prohibit merge commits inside individual layers when using the rebase merge method. Supporting arbitrary merge topologies complicates commit selection and replay semantics considerably.
853
854
---
855
856
# 9. Creating a stack
857
858
A creation operation should look like this:
859
860
```text
861
POST /repos/:owner/:repo/stacks
862
{
863
  "trunk_ref": "main",
864
  "pull_requests": [101, 102, 103],
865
  "expected_heads": {
866
    "101": "…",
867
    "102": "…",
868
    "103": "…"
869
  },
870
  "idempotency_key": "…"
871
}
872
```
873
874
Server algorithm:
875
876
```text
877
1. Begin metadata transaction.
878
2. Lock the repository’s relevant PR and stack rows.
879
3. Check repository and PR permissions.
880
4. Require every PR to be open and in the same repository.
881
5. Require no active membership in another stack.
882
6. Resolve all base and head refs from the Git service.
883
7. Validate the direct-base chain.
884
8. Snapshot current head OIDs.
885
9. Assign boundary OIDs.
886
10. Create stack and ordered entries.
887
11. Pin all boundary objects.
888
12. Increment stack version.
889
13. Write outbox events.
890
14. Commit metadata transaction.
891
15. Schedule policy and CI recalculation.
892
```
893
894
A useful UI may detect a potential manual chain and offer:
895
896
> “These three PRs form a branch chain. Convert them into a stack?”
897
898
But conversion should be explicit. Automatically treating every chain as a stack would produce surprising policy and merge behavior.
899
900
---
901
902
# 10. Computing the PR diff
903
904
GitHub PRs conventionally display a three-dot comparison: changes from the merge base of the base and head branches to the head. ([GitHub Docs][12])
905
906
Conceptually:
907
908
```bash
909
git diff base...head
910
```
911
912
or:
913
914
```text
915
M = mergeBase(baseHead, prHead)
916
diff(M, prHead)
917
```
918
919
For a healthy stacked layer:
920
921
```text
922
mergeBase(parentHead, childHead) = parentHead
923
```
924
925
so the PR displays only the child layer.
926
927
For the example:
928
929
```text
930
A---B---C---D
931
```
932
933
the reviews are:
934
935
```text
936
PR 1 diff: A → B
937
PR 2 diff: B → C
938
PR 3 diff: C → D
939
```
940
941
The system should offer two views:
942
943
### Layer diff
944
945
```text
946
direct base → this PR head
947
```
948
949
This is the primary review view.
950
951
### Cumulative preview
952
953
```text
954
trunk → this PR head
955
```
956
957
This answers:
958
959
> “What will the repository look like if everything through this point lands?”
960
961
If a lower branch is rewritten and an upper branch has not yet been restacked, the merge base may move backward and the layer diff may suddenly include lower-layer changes. The UI should not present that enormous diff without explanation. It should show:
962
963
```text
964
This layer is based on an outdated parent commit.
965
Rebase the stack to restore the intended review boundary.
966
```
967
968
---
969
970
# 11. Policy and CI architecture
971
972
## 11.1 Store both direct and effective bases
973
974
For each PR evaluation:
975
976
```text
977
direct_base_ref    = immediate parent branch
978
effective_base_ref = stack trunk branch
979
```
980
981
Use `direct_base_ref` for:
982
983
* diff presentation,
984
* branch ancestry,
985
* layer boundary,
986
* base branch display.
987
988
Use `effective_base_ref` for:
989
990
* branch protection,
991
* rulesets,
992
* required workflows,
993
* allowed merge methods,
994
* required approvals,
995
* CODEOWNERS configuration,
996
* merge-queue configuration.
997
998
This should be a first-class field in the policy request rather than a collection of stack-specific exceptions scattered throughout the codebase.
999
1000
For example:
1001
1002
```json
1003
{
1004
  "pull_request_id": "pr_102",
1005
  "head_oid": "C",
1006
  "direct_base": {
1007
    "ref": "feature/schema",
1008
    "oid": "B"
1009
  },
1010
  "effective_base": {
1011
    "ref": "main",
1012
    "oid": "A"
1013
  },
1014
  "stack": {
1015
    "id": "stack_17",
1016
    "position": 2,
1017
    "size": 3
1018
  }
1019
}
1020
```
1021
1022
## 11.2 Evaluate configuration from the trunk tree
1023
1024
Suppose PR 1 changes:
1025
1026
```text
1027
.github/CODEOWNERS
1028
.github/workflows/test.yml
1029
```
1030
1031
PR 2 must not immediately receive weaker rules based on the unmerged configuration.
1032
1033
Resolve policy configuration from:
1034
1035
```text
1036
effective_base_oid
1037
```
1038
1039
not from the direct parent branch’s tree.
1040
1041
After the lower change lands in trunk, later evaluations may legitimately use the new policy.
1042
1043
## 11.3 What code state should CI test?
1044
1045
For a healthy stack, each layer’s head already contains every lower layer:
1046
1047
```text
1048
H₁ = trunk + layer 1
1049
H₂ = trunk + layer 1 + layer 2
1050
H₃ = trunk + layer 1 + layer 2 + layer 3
1051
```
1052
1053
Therefore, testing `H₂` validates the cumulative repository state through layer 2.
1054
1055
A robust CI identity is:
1056
1057
```text
1058
(pr_id, head_oid, effective_base_oid, workflow_definition_oid)
1059
```
1060
1061
Do not key required-check validity only by `head_oid`.
1062
1063
If trunk advances, the same PR head may no longer represent a current, mergeable result. Changing the effective base must invalidate or re-evaluate relevant checks.
1064
1065
## 11.4 Synthetic test refs
1066
1067
For stacks that are not currently rebased, or for merge-queue speculation, generate a private test commit/ref:
1068
1069
```text
1070
refs/internal/checks/<run-id>
1071
```
1072
1073
Its tree should represent:
1074
1075
```text
1076
current trunk + all stack layers through this PR
1077
```
1078
1079
The CI system tests that immutable synthetic SHA.
1080
1081
This gives reproducible results and avoids running CI against a moving branch name.
1082
1083
## 11.5 Managing CI explosion
1084
1085
A stack with `n` layers can cause approximately `n` evaluations whenever the bottom changes.
1086
1087
Provide explicit workflow policies such as:
1088
1089
```text
1090
run_on: every_layer
1091
run_on: top_layer_only
1092
run_on: bottom_layer_only
1093
run_on: changed_paths
1094
run_on: merge_group_only
1095
```
1096
1097
A sensible pattern is:
1098
1099
```text
1100
Every layer:
1101
  compile
1102
  type-check
1103
  unit tests
1104
  policy checks
1105
1106
Top layer or merge group:
1107
  full integration suite
1108
  browser tests
1109
  expensive performance tests
1110
```
1111
1112
However, never silently skip a check that branch protection declares required. Optimization must be part of the repository’s declared policy.
1113
1114
---
1115
1116
# 12. Cascading rebase implementation
1117
1118
A stack rebase is the most difficult day-to-day operation.
1119
1120
## 12.1 Why the boundary OID is required
1121
1122
Suppose:
1123
1124
```text
1125
A---B₁---B₂---C₁---C₂
1126
```
1127
1128
where:
1129
1130
```text
1131
PR 1 unique commits: B₁, B₂
1132
PR 2 unique commits: C₁, C₂
1133
```
1134
1135
The stored entry boundaries are:
1136
1137
```text
1138
PR 1 boundary = A
1139
PR 2 boundary = B₂
1140
```
1141
1142
If `main` advances to `X`, the desired result is:
1143
1144
```text
1145
A---X---B₁'---B₂'---C₁'---C₂'
1146
```
1147
1148
The conceptual commands are:
1149
1150
```bash
1151
git rebase --onto X   A   B₂
1152
git rebase --onto B₂' B₂  C₂
1153
```
1154
1155
`git rebase --onto` is precisely the Git operation for selecting commits after an old boundary and replaying them onto a new parent. ([Git SCM][13])
1156
1157
## 12.2 Server-side algorithm
1158
1159
```text
1160
function rebaseStack(stackId, expectedVersion):
1161
    lock stack
1162
    snapshot trunk and every branch head
1163
1164
    newParent = currentTrunkHead
1165
1166
    for entry in bottomToTop:
1167
        oldBoundary = entry.boundaryOid
1168
        oldHead = entry.observedHeadOid
1169
1170
        verify live branch still equals oldHead
1171
1172
        uniqueCommits =
1173
            commits reachable from oldHead
1174
            but not reachable from oldBoundary
1175
1176
        result =
1177
            replay uniqueCommits onto newParent
1178
1179
        if result has conflicts:
1180
            persist conflict workspace and operation state
1181
            return WAITING_FOR_CONFLICT_RESOLUTION
1182
1183
        plannedRefUpdates.append(
1184
            branchRef,
1185
            expectedOld = oldHead,
1186
            new = result.newHead
1187
        )
1188
1189
        plannedEntryUpdates.append(
1190
            boundaryOid = newParent,
1191
            observedHeadOid = result.newHead
1192
        )
1193
1194
        newParent = result.newHead
1195
1196
    atomically compare-and-swap all branch refs
1197
    update metadata
1198
    emit PR synchronize and stack rebase events
1199
```
1200
1201
The system should create all new commits under temporary internal refs before touching public branch refs.
1202
1203
## 12.3 Conflict handling
1204
1205
For each conflicting layer, persist:
1206
1207
* old boundary,
1208
* old head,
1209
* proposed new parent,
1210
* commit currently being replayed,
1211
* index/tree conflict state,
1212
* conflict paths,
1213
* prior successful step results,
1214
* expected branch heads.
1215
1216
Offer two resolution modes:
1217
1218
### Local continuation
1219
1220
The CLI fetches the operation’s state, places the repository into a rebase, and later uploads the resolved commits.
1221
1222
### Hosted continuation
1223
1224
The forge exposes a temporary workspace or editor, applies conflict resolutions, and resumes the worker.
1225
1226
After resolution, the server must still verify that no branch changed in the meantime.
1227
1228
## 12.4 Ref transaction
1229
1230
Standard Git supports compare-and-swap updates by supplying the expected old object ID and can batch updates through `git update-ref --stdin`. Git documents transactional `start`, `prepare`, and `commit` phases for grouped ref changes. ([Git SCM][14])
1231
1232
A custom Git service should expose something like:
1233
1234
```protobuf
1235
rpc BatchUpdateRefs(BatchUpdateRefsRequest)
1236
    returns (BatchUpdateRefsResponse);
1237
1238
message RefUpdate {
1239
  string ref_name = 1;
1240
  GitOid expected_old_oid = 2;
1241
  GitOid new_oid = 3;
1242
}
1243
```
1244
1245
The entire update fails when any expected old OID does not match.
1246
1247
This is stronger than independently running several force pushes.
1248
1249
## 12.5 Commit signatures
1250
1251
A server-side rebase necessarily creates new commits.
1252
1253
It cannot preserve the original cryptographic signatures because:
1254
1255
* parent OIDs change,
1256
* commit OIDs change,
1257
* the original author’s private key is unavailable.
1258
1259
Possible policies are:
1260
1261
* mark server-restacked commits as unsigned,
1262
* sign them with a forge service identity,
1263
* or require a local rebase when user signatures are mandatory.
1264
1265
GitHub warns that server-generated rebase commits may be unsigned, while locally generated commits follow the user’s Git signing configuration. ([GitHub Docs][11])
1266
1267
---
1268
1269
# 13. Merging a stack
1270
1271
## 13.1 API shape
1272
1273
```text
1274
PUT /repos/:owner/:repo/pulls/:number/merge-async
1275
```
1276
1277
Request:
1278
1279
```json
1280
{
1281
  "expected_stack_version": 27,
1282
  "expected_heads": {
1283
    "101": "…",
1284
    "102": "…",
1285
    "103": "…"
1286
  },
1287
  "merge_method": "squash",
1288
  "merge_action": "direct_merge",
1289
  "idempotency_key": "client-generated-uuid"
1290
}
1291
```
1292
1293
Response:
1294
1295
```json
1296
{
1297
  "operation_id": "…",
1298
  "state": "pending"
1299
}
1300
```
1301
1302
The selected PR determines the target prefix.
1303
1304
Selecting PR 103 means:
1305
1306
```text
1307
merge all open entries from the bottom through PR 103
1308
```
1309
1310
## 13.2 Preflight
1311
1312
Before creating any public ref update:
1313
1314
```text
1315
1. Lock stack operation slot.
1316
2. Snapshot stack version, trunk SHA, and all branch heads.
1317
3. Verify selected entries are a contiguous lowest prefix.
1318
4. Verify the current ancestry chain.
1319
5. Verify expected head SHAs.
1320
6. Verify all selected PRs are open and mergeable.
1321
7. Evaluate approvals, CODEOWNERS, rules, and checks.
1322
8. Verify actor permission.
1323
9. Verify merge method is permitted.
1324
10. Ensure no queue/rebase/merge operation conflicts.
1325
```
1326
1327
Do as much work as possible before modifying state.
1328
1329
## 13.3 Merge-commit method
1330
1331
Suppose the selected prefix ends at head `Hₖ`.
1332
1333
Because the stack is required to be fully linear and based on the current trunk, `Hₖ` already contains the cumulative code for every selected PR.
1334
1335
Create one merge commit:
1336
1337
```text
1338
parents:
1339
  current trunk
1340
  Hₖ
1341
1342
tree:
1343
  tree(Hₖ)
1344
```
1345
1346
The result is:
1347
1348
```text
1349
         B---C---D
1350
        /         \
1351
A------             M
1352
```
1353
1354
where `M` is the group merge commit.
1355
1356
This preserves all commits from all selected layers while presenting one merge event on the trunk.
1357
1358
## 13.4 Squash method
1359
1360
For squash merging, create one commit per PR, not one commit for the entire stack:
1361
1362
```text
1363
A---S₁---S₂---S₃
1364
```
1365
1366
where:
1367
1368
```text
1369
tree(S₁) = tree(H₁)
1370
tree(S₂) = tree(H₂)
1371
tree(S₃) = tree(H₃)
1372
```
1373
1374
and each commit’s parent is the previous generated squash commit.
1375
1376
The diff represented by each commit is naturally the corresponding layer:
1377
1378
```text
1379
diff(A, S₁)   = layer 1
1380
diff(S₁, S₂)  = layer 2
1381
diff(S₂, S₃)  = layer 3
1382
```
1383
1384
This avoids accidentally combining all PRs into a single squash commit and preserves one trunk-level commit per reviewed unit.
1385
1386
## 13.5 Rebase method
1387
1388
For the rebase method, replay the unique commits of every selected layer in order:
1389
1390
```text
1391
layer 1 commits
1392
then layer 2 commits
1393
then layer 3 commits
1394
```
1395
1396
The final tree must equal the selected top branch’s tree.
1397
1398
For a fully linear, current stack, the operation is deterministic. The server can reproduce each non-merge commit with:
1399
1400
* the same tree,
1401
* the same author information,
1402
* the same message,
1403
* a new parent,
1404
* new committer metadata.
1405
1406
If arbitrary merge commits are supported inside a layer, the system needs an explicit policy such as:
1407
1408
* flatten them,
1409
* preserve merges,
1410
* or reject the merge method.
1411
1412
Do not leave this undefined.
1413
1414
## 13.6 Restacking the remaining upper layers
1415
1416
Suppose a four-PR stack merges only PRs 1–2:
1417
1418
```text
1419
PR 1: merged
1420
PR 2: merged
1421
PR 3: remains open
1422
PR 4: remains open
1423
```
1424
1425
After the operation:
1426
1427
```text
1428
PR 3 should target trunk
1429
PR 4 should still target PR 3
1430
```
1431
1432
PR 3’s commits must be replayed onto the newly created trunk result using its old boundary SHA. PR 4 must then be replayed onto the new PR 3 head.
1433
1434
This restacking should be included in the same planned operation when possible.
1435
1436
Do not delete the merged lower branches until upper-layer restacking has succeeded or the retained boundary OIDs have been safely pinned.
1437
1438
## 13.7 Building Git objects before updating refs
1439
1440
Create all prospective commits and trees first.
1441
1442
Git’s modern `merge-tree --write-tree` can compute a merge result without modifying a working tree or index, which is useful for server-side planning. ([Git SCM][15])
1443
1444
The general transaction is:
1445
1446
```text
1447
Phase A: Create unreachable candidate objects.
1448
Phase B: Validate all policies and expected refs.
1449
Phase C: Batch-CAS public refs.
1450
Phase D: Finalize SQL metadata and emit events.
1451
```
1452
1453
If Phase B fails, candidate objects remain unreachable and can later be garbage-collected.
1454
1455
## 13.8 Git and SQL cannot trivially share one transaction
1456
1457
Even with a fully atomic Git ref transaction, you still have two durable systems:
1458
1459
```text
1460
Git ref store
1461
SQL metadata store
1462
```
1463
1464
A crash can occur after the Git refs move but before SQL marks the PRs merged.
1465
1466
Use:
1467
1468
* durable operation records,
1469
* an idempotency key,
1470
* snapshots of old and intended refs,
1471
* a transactional outbox,
1472
* and a reconciliation worker.
1473
1474
For example:
1475
1476
```text
1477
operation planned
1478
1479
Git refs updated
1480
    ↓ crash
1481
SQL still says RUNNING
1482
1483
reconciler observes refs equal planned result
1484
1485
finalizes PR and stack metadata
1486
```
1487
1488
Do not try to hide this problem with a large in-process mutex.
1489
1490
---
1491
1492
# 14. Merge-queue integration
1493
1494
Treat the selected stack prefix as one **logical queue item** with ordered internal members:
1495
1496
```text
1497
QueueItem {
1498
  stack_id
1499
  selected_entries: [P₁, P₂, P₃]
1500
  expected_heads
1501
  effective_base_oid
1502
}
1503
```
1504
1505
The speculative merge group should apply:
1506
1507
```text
1508
current queue base
1509
then P₁
1510
then P₂
1511
then P₃
1512
```
1513
1514
The queue may reorder independent queue items, but it must never reorder entries within a stack.
1515
1516
If a lower stacked PR is ejected because of a failure, all selected entries above it must also leave that speculative group because their prerequisite is no longer present. GitHub uses this kind of stack-aware queue behavior and documents special handling for stack grouping and ejection. ([GitHub Docs][4])
1517
1518
Queue validation should be keyed to:
1519
1520
```text
1521
queue base SHA
1522
stack version
1523
every selected head SHA
1524
policy version
1525
```
1526
1527
Any change invalidates the speculative result.
1528
1529
---
1530
1531
# 15. API design
1532
1533
A practical REST surface could be:
1534
1535
```text
1536
GET    /repos/:owner/:repo/stacks
1537
POST   /repos/:owner/:repo/stacks
1538
GET    /repos/:owner/:repo/stacks/:number
1539
1540
POST   /repos/:owner/:repo/stacks/:number/append
1541
POST   /repos/:owner/:repo/stacks/:number/restructure
1542
POST   /repos/:owner/:repo/stacks/:number/rebase
1543
POST   /repos/:owner/:repo/stacks/:number/unstack
1544
POST   /repos/:owner/:repo/stacks/:number/dissolve
1545
1546
PUT    /repos/:owner/:repo/pulls/:number/merge-async
1547
GET    /repos/:owner/:repo/stack-operations/:operation_id
1548
POST   /repos/:owner/:repo/stack-operations/:operation_id/continue
1549
POST   /repos/:owner/:repo/stack-operations/:operation_id/abort
1550
```
1551
1552
Every mutating request should support:
1553
1554
```text
1555
Idempotency-Key
1556
If-Match or expected_stack_version
1557
expected head OIDs
1558
```
1559
1560
An individual PR representation should include:
1561
1562
```json
1563
{
1564
  "stack": {
1565
    "id": "stack_17",
1566
    "number": 17,
1567
    "position": 2,
1568
    "size": 4,
1569
    "trunk_ref": "main",
1570
    "trunk_oid": "…",
1571
    "health": "healthy"
1572
  }
1573
}
1574
```
1575
1576
GraphQL is particularly useful for reading the stack map:
1577
1578
```graphql
1579
type PullRequestStack {
1580
  id: ID!
1581
  number: Int!
1582
  trunkRefName: String!
1583
  state: PullRequestStackState!
1584
  health: PullRequestStackHealth!
1585
  version: Int!
1586
  entries: [PullRequestStackEntry!]!
1587
}
1588
1589
type PullRequestStackEntry {
1590
  id: ID!
1591
  position: Int!
1592
  pullRequest: PullRequest!
1593
  boundaryOid: GitObjectID!
1594
  observedHeadOid: GitObjectID!
1595
}
1596
```
1597
1598
It is reasonable to keep complex operations REST/job-based initially while exposing the read model through GraphQL.
1599
1600
---
1601
1602
# 16. Events and webhooks
1603
1604
Recommended events:
1605
1606
```text
1607
pull_request_stack.created
1608
pull_request_stack.appended
1609
pull_request_stack.restructured
1610
pull_request_stack.dissolved
1611
1612
pull_request.stacked
1613
pull_request.unstacked
1614
pull_request.stack_position_changed
1615
1616
pull_request_stack.rebase_started
1617
pull_request_stack.rebase_conflicted
1618
pull_request_stack.rebase_completed
1619
1620
pull_request_stack.merge_started
1621
pull_request_stack.merge_queued
1622
pull_request_stack.merge_partially_completed
1623
pull_request_stack.merge_completed
1624
pull_request_stack.merge_failed
1625
```
1626
1627
Every event should include:
1628
1629
```text
1630
stack ID and number
1631
operation ID
1632
stack version
1633
actor
1634
old and new ordering
1635
trunk ref and SHA
1636
affected PR IDs
1637
old and new head SHAs
1638
timestamp
1639
```
1640
1641
Deliver through a transactional outbox rather than publishing directly from the request handler. Consumers must deduplicate by event ID.
1642
1643
For ordinary `pull_request` synchronization events, include stack context:
1644
1645
```json
1646
{
1647
  "stack": {
1648
    "number": 17,
1649
    "position": 2,
1650
    "size": 4,
1651
    "base": {
1652
      "ref": "main",
1653
      "sha": "…"
1654
    }
1655
  }
1656
}
1657
```
1658
1659
That mirrors the useful part of GitHub’s CI event model. ([GitHub Docs][3])
1660
1661
---
1662
1663
# 17. Concurrency model
1664
1665
Stack operations touch several mutable objects:
1666
1667
* stack ordering,
1668
* PR base branches,
1669
* branch refs,
1670
* trunk ref,
1671
* approvals,
1672
* checks,
1673
* merge queue entries.
1674
1675
Use several layers of concurrency protection.
1676
1677
## Database lock
1678
1679
Permit only one structural or Git-mutating operation per stack at a time.
1680
1681
```text
1682
SELECT ... FOR UPDATE
1683
```
1684
1685
or use a stack-scoped advisory lock.
1686
1687
## Optimistic stack version
1688
1689
Every structural mutation increments:
1690
1691
```text
1692
stack.version
1693
```
1694
1695
Clients provide the version they observed. A mismatch returns `409 Conflict`.
1696
1697
## Git compare-and-swap
1698
1699
Every branch update includes:
1700
1701
```text
1702
expected_old_oid
1703
new_oid
1704
```
1705
1706
A push made after the operation snapshot causes the transaction to fail rather than overwrite the user’s work.
1707
1708
## Operation idempotency
1709
1710
Retrying the same request must return the same operation rather than enqueueing a second merge or rebase.
1711
1712
## Worker lease
1713
1714
Long-running operations need:
1715
1716
```text
1717
lease_owner
1718
lease_expires_at
1719
heartbeat_at
1720
```
1721
1722
A worker that dies can be replaced safely.
1723
1724
## Reconciliation
1725
1726
Periodically inspect incomplete operations and compare:
1727
1728
```text
1729
actual refs
1730
planned refs
1731
metadata state
1732
```
1733
1734
Then complete, retry, or mark the operation partial.
1735
1736
---
1737
1738
# 18. Permissions and security
1739
1740
A stack adds several operations more powerful than an ordinary PR edit.
1741
1742
## Creating or restructuring a stack
1743
1744
Require permission to modify every affected PR’s base relationship.
1745
1746
## Server-side rebase
1747
1748
Require permission to rewrite every affected branch ref.
1749
1750
Do not allow a user with permission over only their own upper branch to force-update another author’s lower branch.
1751
1752
## Stack merge
1753
1754
Require normal trunk merge permission and satisfaction of all effective-base policies.
1755
1756
## Forks
1757
1758
Avoid cross-fork stacks in the first version.
1759
1760
They introduce:
1761
1762
* different repository object stores,
1763
* different ref owners,
1764
* token permission boundaries,
1765
* workflow-secret restrictions,
1766
* potentially untrusted code,
1767
* inability to atomically update refs,
1768
* branch deletion and retention complications.
1769
1770
GitHub’s same-repository restriction is operationally sensible, although GitHub has not publicly stated that these are its exact internal reasons. ([GitHub Docs][2])
1771
1772
## Auditability
1773
1774
Record:
1775
1776
```text
1777
actor
1778
operation
1779
old and new stack ordering
1780
old and new refs
1781
approvals/checks snapshot
1782
merge method
1783
queue decision
1784
conflict resolutions
1785
service identity that created commits
1786
```
1787
1788
A stack merge should be reconstructable after the fact.
1789
1790
---
1791
1792
# 19. User interface design
1793
1794
The PR page should show a stack map:
1795
1796
```text
1797
✓ #101 Add schema                 merged
1798
✓ #102 Add API                    approved
1799
● #103 Add UI                     you are here
1800
○ #104 Add integration tests      checks running
1801
```
1802
1803
Each entry should display:
1804
1805
* position,
1806
* direct base,
1807
* head branch,
1808
* approval state,
1809
* required-check state,
1810
* conflict/rebase state,
1811
* whether changing it affects upper entries.
1812
1813
Important actions:
1814
1815
```text
1816
Review this layer
1817
Preview cumulative changes
1818
Move up/down stack
1819
Rebase stack
1820
Update entire stack
1821
Merge through this PR
1822
Add PR to top
1823
Remove from stack
1824
Repair stack
1825
```
1826
1827
A merge button on PR 103 should say:
1828
1829
```text
1830
Merge 3 pull requests
1831
```
1832
1833
not merely:
1834
1835
```text
1836
Merge pull request
1837
```
1838
1839
Before rewriting a lower branch, show impact:
1840
1841
```text
1842
This will rewrite 4 branches above this layer,
1843
invalidate 5 approvals, and rerun 23 required checks.
1844
```
1845
1846
That is much more useful than presenting cascading rebase as a harmless implementation detail.
1847
1848
---
1849
1850
# 20. CLI design
1851
1852
A minimal CLI could expose:
1853
1854
```bash
1855
forge stack init
1856
forge stack add
1857
forge stack submit
1858
forge stack view
1859
forge stack up
1860
forge stack down
1861
forge stack checkout
1862
forge stack rebase
1863
forge stack continue
1864
forge stack abort
1865
forge stack push
1866
forge stack sync
1867
forge stack merge
1868
forge stack remove
1869
```
1870
1871
A local metadata cache might contain:
1872
1873
```json
1874
{
1875
  "stack_id": "…",
1876
  "stack_version": 27,
1877
  "trunk": {
1878
    "ref": "main",
1879
    "oid": "…"
1880
  },
1881
  "branches": [
1882
    {
1883
      "name": "feature/schema",
1884
      "pr": 101,
1885
      "boundary_oid": "…",
1886
      "head_oid": "…"
1887
    }
1888
  ]
1889
}
1890
```
1891
1892
But the server must remain authoritative.
1893
1894
The local file is useful for:
1895
1896
* fast navigation,
1897
* identifying layer commit ranges,
1898
* preparing offline commits,
1899
* recovering interrupted local rebases.
1900
1901
It must not be trusted to override current server membership or remote ref state.
1902
1903
---
1904
1905
# 21. Important edge cases
1906
1907
## Lower branch force-pushed
1908
1909
Mark every upper entry as needing restack. Do not assume the old boundary remains present unless it is pinned.
1910
1911
## Trunk advances
1912
1913
Mark the entire stack as needing rebase or enqueue a safe automatic rebase, depending on policy.
1914
1915
## Middle PR closes
1916
1917
Upper layers remain structurally dependent on missing work. Mark them blocked. GitHub similarly treats closing or invalidating a middle member as preventing the entries above it from landing normally. ([GitHub Docs][11])
1918
1919
## Branch deleted
1920
1921
Keep the stack object and show `MISSING_REF`. Offer restoration from the observed head OID when permission allows.
1922
1923
## Generic PR base edit
1924
1925
Reject or convert into an explicit restructure operation.
1926
1927
## PR added to two stacks
1928
1929
Reject. Linear stack membership should be unique.
1930
1931
## A lower PR is squash-merged outside the stack operation
1932
1933
Use the stored boundary and merged result to restack upper layers rather than attempting to rediscover commit ranges from branch names.
1934
1935
## A boundary commit becomes unreachable
1936
1937
Prevent this through hidden refs or object leases.
1938
1939
## User pushes during rebase
1940
1941
The final CAS fails. Preserve the user’s branch and mark the operation stale.
1942
1943
## Review comments after rebase
1944
1945
Map comments by blob/path/context where possible. Mark comments outdated when the relevant patch no longer exists. Apply the repository’s normal approval-dismissal policy to rewritten heads.
1946
1947
## Duplicate or cherry-picked commits
1948
1949
Do not use patch-ID heuristics as the primary definition of a layer. Stored boundary OIDs and ancestry are more deterministic.
1950
1951
## Merge commits within a layer
1952
1953
Define behavior explicitly. A first version can reject them for stack rebase operations rather than producing surprising flattened histories.
1954
1955
## Huge stacks
1956
1957
Set practical limits for:
1958
1959
* number of layers,
1960
* queue group size,
1961
* rebase operation duration,
1962
* webhook payload size,
1963
* simultaneous CI runs.
1964
1965
GitHub’s merge-queue documentation includes special stack group-sizing behavior, illustrating that stack size becomes an operational concern rather than merely a UI concern. ([GitHub Docs][4])
1966
1967
## Partial operation failure
1968
1969
Represent it directly:
1970
1971
```text
1972
PARTIALLY_SUCCEEDED
1973
```
1974
1975
Include which PRs landed and which did not. Never report a generic failure after some trunk refs have already moved.
1976
1977
---
1978
1979
# 22. Testing strategy
1980
1981
Stacked PRs need substantially more than ordinary unit tests.
1982
1983
## Property tests
1984
1985
Generate random linear commit histories and verify:
1986
1987
```text
1988
each layer diff equals its unique change
1989
cascade rebase preserves final tree
1990
squash produces one commit per layer
1991
group merge final tree equals selected top tree
1992
remaining upper layers retain their unique changes
1993
```
1994
1995
## Operation matrix
1996
1997
Test:
1998
1999
```text
2000
merge / squash / rebase
2001
× full stack / partial prefix
2002
× trunk unchanged / trunk advanced
2003
× no upper layers / remaining upper layers
2004
× local push / concurrent push
2005
× success / conflict / worker crash
2006
```
2007
2008
## Fault injection
2009
2010
Crash after every durable step:
2011
2012
```text
2013
after operation creation
2014
after candidate object creation
2015
after ref prepare
2016
after ref commit
2017
after PR metadata update
2018
after stack update
2019
before outbox publication
2020
```
2021
2022
Verify that reconciliation reaches a correct state.
2023
2024
## Policy tests
2025
2026
Verify:
2027
2028
* trunk workflows run on intermediate layers,
2029
* CODEOWNERS comes from trunk,
2030
* lower unmerged policy changes do not weaken upper requirements,
2031
* stale checks are invalidated when trunk changes,
2032
* approvals are dismissed according to policy after rewrites.
2033
2034
## Queue tests
2035
2036
Verify:
2037
2038
* stack members retain order,
2039
* independent queue items can move around the stack,
2040
* lower-member ejection ejects dependent upper members,
2041
* a changed head invalidates the entire corresponding group.
2042
2043
## Garbage-collection tests
2044
2045
Delete public refs, run aggressive object GC, and verify pinned boundary commits remain available until the stack no longer needs them.
2046
2047
## Performance tests
2048
2049
Benchmark:
2050
2051
* stack views at 10, 50, and 100 layers,
2052
* ancestry validation,
2053
* per-layer diff calculation,
2054
* cascading rebase,
2055
* batch ref updates,
2056
* event fan-out,
2057
* CI invalidation.
2058
2059
---
2060
2061
# 23. Sensible implementation roadmap
2062
2063
## Phase 1: First-class metadata and review UX
2064
2065
Build:
2066
2067
* stack and entry records,
2068
* explicit create/append/remove,
2069
* stack map,
2070
* structural validation,
2071
* ordinary per-layer PR diffs,
2072
* bottom-only merge.
2073
2074
This already gives users a legible stack without attempting dangerous history rewriting.
2075
2076
## Phase 2: Effective-base policy and CI
2077
2078
Add:
2079
2080
* direct versus effective base,
2081
* stack metadata in events,
2082
* trunk-based branch protection,
2083
* CODEOWNERS evaluation,
2084
* current-base-aware check identities.
2085
2086
Do this before presenting stacks as production-safe.
2087
2088
## Phase 3: Rebase engine
2089
2090
Add:
2091
2092
* boundary OID storage,
2093
* object pinning,
2094
* waterfall replay,
2095
* temporary operation refs,
2096
* conflict pause/continue/abort,
2097
* compare-and-swap branch updates,
2098
* local CLI support.
2099
2100
Boundary OIDs should actually be stored from Phase 1, even if the rebase engine comes later.
2101
2102
## Phase 4: Multi-PR merge
2103
2104
Add:
2105
2106
* asynchronous merge operations,
2107
* contiguous-prefix validation,
2108
* merge/squash/rebase methods,
2109
* upper-layer restacking,
2110
* idempotency,
2111
* reconciliation,
2112
* partial-failure reporting.
2113
2114
## Phase 5: Merge queue
2115
2116
Add:
2117
2118
* logical stack queue items,
2119
* speculative group SHAs,
2120
* in-stack ordering,
2121
* cascading ejection,
2122
* group-size limits.
2123
2124
## Phase 6: Advanced editing
2125
2126
Add:
2127
2128
* insert into middle,
2129
* split a layer,
2130
* combine adjacent layers,
2131
* reorder layers,
2132
* move a suffix to another stack,
2133
* agent-driven automatic stack construction.
2134
2135
These are powerful but substantially harder because they require transplanting commit ranges and preserving review attribution.
2136
2137
---
2138
2139
# 24. Recommended design decisions
2140
2141
For a new GitHub clone, I would make these choices:
2142
2143
1. **Use ordinary branches and PRs.**
2144
   Make the stack an additional collaboration object, not a new Git primitive.
2145
2146
2. **Persist the stack explicitly.**
2147
   Do not infer it from branch relationships on every request.
2148
2149
3. **Support only linear stacks initially.**
2150
   Build a separate dependency-graph feature later rather than corrupting stack semantics.
2151
2152
4. **Separate direct base from effective base everywhere.**
2153
   This prevents both CI confusion and policy bypass.
2154
2155
5. **Store and pin immutable boundary OIDs from day one.**
2156
   This is what makes squash merges, rebases, and upper-layer restacking reliable.
2157
2158
6. **Make the server authoritative.**
2159
   Local CLI metadata is a cache and editing aid.
2160
2161
7. **Implement every mutation as a durable operation.**
2162
   Use operation IDs, idempotency, leases, snapshots, and recovery.
2163
2164
8. **Use batch compare-and-swap ref updates.**
2165
   Never rewrite a stack through a sequence of unconditional force pushes.
2166
2167
9. **Treat rebase and conflict recovery as core functionality.**
2168
   A stack without reliable maintenance becomes unusable as soon as the bottom changes.
2169
2170
10. **Make stack-wide effects visible.**
2171
    Show when an action will rewrite upper branches, invalidate approvals, or rerun CI.
2172
2173
11. **Keep forks and cross-repository stacks out of the first version.**
2174
    They multiply permissions, security, and transaction complexity.
2175
2176
12. **Design the merge queue with stacks in mind from the beginning.**
2177
    Retrofitting grouped dependency ordering into a queue later is painful.
2178
2179
---
2180
2181
# Bottom line
2182
2183
A stacked PR system has three layers:
2184
2185
```text
2186
1. Git topology
2187
   Ordinary branches form a linear commit chain.
2188
2189
2. Collaboration model
2190
   A durable stack object orders ordinary PRs and assigns an ultimate trunk.
2191
2192
3. Orchestration
2193
   Policy, CI, rebasing, merging, queueing, recovery, and events operate
2194
   across the ordered group.
2195
```
2196
2197
The Git topology is the easy part. Developers have manually chained branches for years.
2198
2199
The real product is the orchestration layer:
2200
2201
* preserving each layer’s review boundary,
2202
* evaluating every layer under trunk policy,
2203
* rebasing from the bottom upward,
2204
* retaining old boundary SHAs,
2205
* atomically or recoverably updating several refs,
2206
* merging only contiguous prefixes,
2207
* restacking whatever remains,
2208
* and accurately reflecting all of that in CI, review state, the merge queue, and the UI.
2209
2210
For a GitHub clone, the central architectural insight is to keep stacks in the **forge’s metadata and operation layer**, while giving the Git data plane strong generic primitives for commit-graph queries, replay, object retention, and transactional compare-and-swap ref updates.
2211
2212
[1]: https://github.blog/changelog/2026-07-30-stacked-pull-requests-are-now-in-public-preview/ "https://github.blog/changelog/2026-07-30-stacked-pull-requests-are-now-in-public-preview/"
2213
[2]: https://docs.github.com/en/pull-requests/get-started/about-stacked-prs "https://docs.github.com/en/pull-requests/get-started/about-stacked-prs"
2214
[3]: https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/optimizing-ci-for-stacked-pull-requests "https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/optimizing-ci-for-stacked-pull-requests"
2215
[4]: https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/merging-stacked-pull-requests "https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/merging-stacked-pull-requests"
2216
[5]: https://docs.github.com/en/rest/pulls/stacks "https://docs.github.com/en/rest/pulls/stacks"
2217
[6]: https://docs.github.com/en/graphql/reference/pulls "https://docs.github.com/en/graphql/reference/pulls"
2218
[7]: https://github.com/github/gh-stack/blob/main/internal/stack/stack.go "https://github.com/github/gh-stack/blob/main/internal/stack/stack.go"
2219
[8]: https://github.com/github/gh-stack "https://github.com/github/gh-stack"
2220
[9]: https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28 "https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28"
2221
[10]: https://docs.github.com/en/pull-requests/reference/stacked-pull-requests "https://docs.github.com/en/pull-requests/reference/stacked-pull-requests"
2222
[11]: https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-stacked-pull-requests "https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-stacked-pull-requests"
2223
[12]: https://docs.github.com/en/pull-requests/reference/branches "https://docs.github.com/en/pull-requests/reference/branches"
2224
[13]: https://git-scm.com/docs/git-rebase "https://git-scm.com/docs/git-rebase"
2225
[14]: https://git-scm.com/docs/git-update-ref "https://git-scm.com/docs/git-update-ref"
2226
[15]: https://git-scm.com/docs/git-merge-tree "https://git-scm.com/docs/git-merge-tree"
2227
2228

This page updates live while a promote is in flight · changelog