Zum Inhalt springen
DevOps

Stacked Pull Requests auf GitHub: große Änderungen in prüfbare Ebenen schneiden

Ein Stack ist eine geordnete Kette von Pull Requests. Jede Ebene baut auf ihrem Vorgänger auf, und nur der Fußpunkt der Kette berührt main. Statt einer großen Änderung prüfst du mehrere kleine, jede mit eigenem Diff und eigener Diskussion. Seit dem 30. Juli 2026 kann GitHub das selbst, als öffentliche Vorschau, ohne Fremdwerkzeug.

Dieser Beitrag ist ein vollständiges Tutorial an einem durchgehenden Beispiel: Rechnungsnummernkreise pro Mandant, vier Ebenen, von der Datenbank-Migration bis zum Nachtlauf. Es geht durch das Anlegen, den kaskadierenden Rebase, das Mergen von unten nach oben und die Bedingung, die daran hängt und die man beim ersten Mal übersieht.

Inhalt

Das Problem: ein Review, in dem die Migration untergeht

Das Feature klingt harmlos. Jeder Mandant bekommt eigene Rechnungsnummernkreise, fortlaufend, pro Jahr, mit eigenem Präfix. Fachlich sind das vier Dinge, die aufeinander aufbauen: eine Tabelle und eine Spalte in der Datenbank, die Vergabe-Logik, ein Endpunkt, über den man den Nummernkreis konfiguriert, und ein Nachtlauf, der Rechnungen ohne Nummer nachträglich versorgt.

Gebaut wird es auf einem Branch. Am Ende sieht das so aus:

$ git diff --stat main
 87 files changed, 1284 insertions(+), 96 deletions(-)

Dieser Pull Request geht ins Review und bleibt liegen. Nicht aus Bosheit: Wer 87 Dateien öffnet, sucht sich einen Einstieg, und der einfachste Einstieg ist immer der, bei dem man schnell etwas Sinnvolles sagen kann. Das sind die Namen im DTO, die Reihenfolge der Felder, ein fehlendes final.

Der riskante Teil ist ein anderer. Er steht in einer einzigen Datei:

databaseChangeLog:
  - changeSet:
      id: 2026-08-01-nummernkreis
      author: velaatlas
      changes:
        - createTable:
            tableName: invoice_number_range
            columns:
              - column:
                  name: id
                  type: bigint
                  autoIncrement: true
                  constraints: { primaryKey: true }
              - column:
                  name: tenant_id
                  type: bigint
                  constraints: { nullable: false }
              - column:
                  name: prefix
                  type: varchar(16)
                  constraints: { nullable: false }
              - column:
                  name: current_value
                  type: bigint
                  defaultValueNumeric: 0
                  constraints: { nullable: false }
        - addColumn:
            tableName: invoice
            columns:
              - column:
                  name: invoice_number
                  type: varchar(32)
                  constraints: { nullable: false }

Diese Migration ist der einzige Teil der Änderung, der sich nicht zurücknehmen lässt, indem man den Code zurückdreht. Sie läuft gegen eine Tabelle mit Bestandsdaten, und nullable: false ohne Default bricht dort ab, weil die vorhandenen Zeilen keine Rechnungsnummer haben. Genau dieser Fehler überlebt ein 87-Dateien-Review mit hoher Wahrscheinlichkeit, weil er zwischen Fleißarbeit steht.

Schuld daran ist die Portionierung, nicht die Sorgfalt der Reviewer.

Die Idee: eine Kette statt eines Klumpens

Ein Stack dreht die Portionierung um. Statt eines Branches gibt es vier, und jeder zielt auf seinen Vorgänger statt auf main:

main
 └─ nummernkreis-schema           PR 1   Migration, Spalte nullable
     └─ nummernkreis-domaene      PR 2   Vergabe-Logik im Service
         └─ nummernkreis-api      PR 3   Endpunkt und DTO
             └─ nummernkreis-job  PR 4   Nachtlauf für Bestandsdaten

GitHub nennt die Kette Stack, jedes Glied Layer und den Zielbranch ganz unten Trunk. Der Diff jedes Pull Requests enthält nur die Änderung dieser einen Ebene, weil sein Vergleichspunkt der Branch darunter ist und nicht main.

Für das Review ändert das die Ausgangslage. Der Pull Request mit der Migration enthält eine Datei. Wer ihn öffnet, kann über nichts anderes reden.

Auf der Autorenseite ändert es die Reihenfolge der Entscheidungen: Die Schnittführung fällt, bevor der erste Commit steht. Das ist der eigentliche Aufwand, und die Werkzeuge nehmen ihn dir nicht ab.

Voraussetzungen

Stacked Pull Requests laufen seit dem 30. Juli 2026 als öffentliche Vorschau und werden über alle Repositories ausgerollt. Bedienbar sind sie auf github.com, über die GitHub CLI und in der Mobile-App. Coding-Agenten wie GitHub Copilot greifen über den gh-stack-Skill darauf zu.

Für den lokalen Ablauf brauchst du die Erweiterung:

gh extension install github/gh-stack

Vorausgesetzt sind GitHub CLI ab 2.90.0 und Git ab 2.20. Die Version prüfst du mit:

gh --version
git --version

Zusätzlich sinnvoll, bevor du im Team anfängst: Branch-Protection-Regeln auf dem Default-Branch und die Actions-Workflows, die auf Pull Requests gegen den Default-Branch laufen. Beides greift im Stack, und beides willst du einmal an einem Test-Stack gesehen haben, bevor es ein echtes Feature trifft.

Schritt 1: den Stack anlegen

gh stack init legt den Stack lokal an und erzeugt die erste Ebene. Ohne Argumente fragt das Kommando interaktiv nach einem Branch-Namen und bietet an, den aktuellen Branch als erste Ebene zu übernehmen.

git switch main
git pull
gh stack init --base main

--base legt den Trunk fest, also den Branch, auf den die unterste Ebene später zielt. In den meisten Repositories ist das main, in manchen develop. Ohne das Flag fragt init interaktiv nach dem Namen der ersten Ebene und bietet an, den aktuellen Branch dafür zu übernehmen. Im Beispiel unten heißt diese erste Ebene nummernkreis-schema.

Schritt 2: die Migration als unterste Ebene

Ganz unten steht das, was alles andere braucht. Hier ist das die Migration. Und weil sie unten steht, muss sie eine Eigenschaft haben, die sie im großen Pull Request nicht brauchte: Sie muss für sich allein funktionieren, ohne den Code der Ebenen darüber.

Deshalb sieht das Changeset im Stack anders aus als oben. Die Tabelle bleibt wie sie war, geändert ist nur das Ende:

        - addColumn:
            tableName: invoice
            columns:
              - column:
                  name: invoice_number
                  type: varchar(32)

Der Unterschied ist eine Zeile: constraints: { nullable: false } fehlt, die Spalte invoice_number ist jetzt nullable. Damit läuft die Migration gegen Bestandsdaten durch, und der Zustand danach ist ein gültiger Zustand des Systems, auch wenn nie eine Zeile mehr dazukommt.

Committen und fertig:

git add src/main/resources/db/changelog/
git commit -m "Nummernkreis-Tabelle und nullable Rechnungsnummer"

Schritt 3: die Ebenen darüber stapeln

gh stack add legt einen neuen Branch oben auf den Stack. Mit -A werden die Änderungen dabei gleich gestaged, mit -m committet:

gh stack add nummernkreis-domaene

Dann die Vergabe-Logik schreiben:

@Service
class InvoiceNumberService {

    private final NumberRangeRepository ranges;

    InvoiceNumberService(NumberRangeRepository ranges) {
        this.ranges = ranges;
    }

    @Transactional
    String nextNumber(long tenantId, int year) {
        NumberRange range = ranges.lockByTenant(tenantId)
                .orElseThrow(() -> new NoNumberRangeException(tenantId));

        long value = range.increment();
        return "%s-%d-%05d".formatted(range.prefix(), year, value);
    }
}

Und committen:

git add src/main/java/
git commit -m "Vergabe der Rechnungsnummer je Mandant"

Die beiden nächsten Ebenen entstehen genauso. gh stack add bringt eine Kurzform mit, die staged, committet und den Branch in einem Schritt erzeugt:

gh stack add nummernkreis-api -Am "Endpunkt zum Pflegen der Nummernkreise"
gh stack add nummernkreis-job -Am "Nachtlauf vergibt Nummern für Bestandsrechnungen"

Ein Blick auf den Stand:

gh stack view

Das Kommando zeigt die Kette mit den Branches, der Position im Stack und, sobald es sie gibt, den zugehörigen Pull Requests. Mit --short wird die Ausgabe knapp, mit --json maschinenlesbar.

Schritt 4: die Pull Requests erzeugen

Bis hierhin ist alles lokal. gh stack submit pusht alle Branches, legt die fehlenden Pull Requests an und verbindet sie auf GitHub zum Stack:

gh stack submit

Der Unterschied zu gh stack push und gh stack sync ist wichtig genug, um ihn sich einmal zu merken:

Kommando Pusht Branches Legt fehlende Pull Requests an Rebased auf den Trunk
gh stack push ja nein nein
gh stack submit ja ja nein
gh stack sync ja nie ja

gh stack sync ist das Kommando für den Alltag, wenn sich main bewegt hat. Es holt den Stand vom Remote, spult den Trunk vor, rebased die Kette darauf, pusht die aktualisierten Branches und gleicht den Pull-Request-Status ab. Neue Pull Requests eröffnet es dabei nie, dafür ist submit zuständig.

Mit --auto aktiviert submit Auto-Merge, mit --open öffnet es die Pull Requests im Browser.

Schritt 5: was auf GitHub sichtbar wird

Auf GitHub trägt jeder Pull Request des Stacks eine Übersicht der ganzen Kette, mit der eigenen Position darin. Reviewer brauchen dafür keinen zusätzlichen Account und keine Browser-Erweiterung, weil die Darstellung Teil der Pull-Request-Oberfläche ist.

Praktisch heißt das: Der Reviewer der Migration sieht eine Datei und daneben den Hinweis, dass drei weitere Ebenen darauf aufbauen. Er kann die Migration beurteilen, ohne den Rest gelesen zu haben, und er sieht trotzdem, wozu sie gehört.

Wer von Graphite oder einem ähnlichen Dienst kommt, kennt die Darstellung. Der Unterschied ist, dass hier kein zusätzlicher Dienst zwischen Repository und Review sitzt und niemand im Team etwas installieren muss, um die Kette zu sehen.

Schritt 6: eine Änderung ganz unten

Jetzt kommt der Teil, den man beim ersten Stack unterschätzt. Das Review der Migration ergibt, dass ein Index auf tenant_id fehlt. Die Änderung gehört nach ganz unten, also auf die erste Ebene:

gh stack bottom

Damit springst du auf die unterste Ebene. Änderung machen, committen:

git add src/main/resources/db/changelog/
git commit -m "Index auf tenant_id"

Und dann muss alles darüber diese Änderung mitbekommen:

gh stack rebase

Das ist ein kaskadierender Rebase. Die Kette wird von unten nach oben durchgearbeitet, jede Ebene setzt dabei auf ihrem frisch aktualisierten Vorgänger auf. So kommt eine Änderung von ganz unten überall darüber an. Anschließend pushen:

gh stack push

Bei einem Konflikt bleibt der Rebase stehen und nennt die betroffenen Dateien. Du löst ihn wie gewohnt und machst weiter:

git add <datei>
gh stack rebase --continue

--abort bricht ab und stellt den Ausgangszustand wieder her. Mit --downstack und --upstack begrenzt du den Rebase auf den Teil unter oder über der aktuellen Ebene, mit --no-trunk lässt du den Trunk außen vor.

Hier steckt der reale Preis des Verfahrens. Ändert sich etwas ganz unten, läuft es durch alle Ebenen darüber, und einen Konflikt, der jede Ebene betrifft, löst du entsprechend oft. Das ist kein Fehler im Werkzeug, sondern die Folge davon, dass die Ebenen aufeinander aufbauen. Es ist der Grund, warum tiefe Stacks selten eine gute Idee sind.

Schritt 7: von unten nach oben mergen

Gemergt wird zwingend von unten nach oben. Der Pull Request mit der Migration geht zuerst nach main, dann die Domäne, dann der Endpunkt, dann der Job.

Zwei Dinge passieren dabei von selbst. Mergt die unterste Ebene, zieht GitHub den Rest der Kette auf den neuen Stand von main nach, und der nächste Pull Request zeigt danach direkt auf den Default-Branch. Mergst du mittendrin, bleibt alles darüber offen und wird ebenso umgehängt.

Über die CLI mergst du eine oder mehrere Ebenen auf einmal:

gh stack merge --merge-method merge

Die Doku listet alle drei Methoden, Merge-Commit ebenso wie Squash und Rebase. Zur Private Preview gab es allerdings Berichte, dass Squash und Rebase die Zuordnung im Stack brechen konnten. Beide schreiben die Commits der unteren Ebene neu, und der kaskadierende Rebase setzt darauf auf. Wer Squash als Repository-Vorgabe fahren will, probiert das vorher an einem Test-Stack durch. Die Merge Queue kommt separat und ist noch nicht überall da.

Lokal räumst du danach auf:

gh stack sync --prune

--prune entfernt die lokalen Branches der bereits gemergten Ebenen und rebased die verbliebenen auf den aktualisierten Trunk.

Der vollständige Ablauf am Stück

Der ganze Weg von main bis zu vier verlinkten Pull Requests, ohne Prosa dazwischen:

gh extension install github/gh-stack

git switch main
git pull
gh stack init --base main

# Ebene 1: Migration
git add src/main/resources/db/changelog/
git commit -m "Nummernkreis-Tabelle und nullable Rechnungsnummer"

# Ebene 2 bis 4
gh stack add nummernkreis-domaene -Am "Vergabe der Rechnungsnummer je Mandant"
gh stack add nummernkreis-api     -Am "Endpunkt zum Pflegen der Nummernkreise"
gh stack add nummernkreis-job     -Am "Nachtlauf vergibt Nummern für Bestandsrechnungen"

gh stack view
gh stack submit

Und der Alltag danach, wenn main sich bewegt hat oder unten etwas nachgezogen wird:

gh stack sync                    # Trunk nachholen, Kette rebasen, Status abgleichen
gh stack bottom                  # nach ganz unten springen
# ... Änderung, git commit ...
gh stack rebase                  # Änderung durch alle Ebenen darüber ziehen
gh stack push
gh stack merge --merge-method merge
gh stack sync --prune            # lokale Branches der gemergten Ebenen aufräumen

Die Kommandos auf einen Blick

Kommando Zweck Nennenswerte Flags
gh stack init Stack im Repository anlegen --base
gh stack add Neue Ebene oben auf den Stack -A, -u, -m
gh stack view Den Stack ansehen --short, --json
gh stack checkout Stack per Nummer, Pull Request, URL oder Branch auschecken
gh stack modify Ebenen interaktiv umbauen, löschen, zusammenlegen, umbenennen --continue, --abort
gh stack unstack Stack aus der Verwaltung nehmen und auf GitHub auflösen --local
gh stack submit Branches pushen, Pull Requests anlegen oder aktualisieren --auto, --open, --remote
gh stack sync Holen, rebasen, pushen, Status abgleichen --remote, --prune
gh stack rebase Kaskadierender Rebase über den Stack --downstack, --upstack, --no-trunk, --continue, --abort
gh stack push Aktive Branches des Stacks pushen --remote
gh stack link Bestehende Pull Requests auf GitHub zum Stack verbinden --base, --open, --remote
gh stack merge Eine oder mehrere Ebenen mergen --merge-method, --yes
gh stack switch Interaktiv zu einer anderen Ebene wechseln
gh stack up / down Eine Ebene nach oben oder unten
gh stack top / bottom / trunk An den Anfang, das Ende oder den Trunk springen
gh stack alias Kurzform für ein Kommando anlegen --remove

Für den Umbau der Reihenfolge gibt es nur den Weg über die CLI. gh stack modify fügt Ebenen ein, wirft sie raus, legt sie zusammen oder benennt sie um. Teams, die lokal nicht mit der CLI arbeiten, sollten die Reihenfolge deshalb vorher festlegen, weil sie später nicht ohne Weiteres umsortieren können.

Der Wendepunkt: was unten mergt, ist in Produktion

An dieser Stelle wird aus einem Werkzeug eine Entwurfsentscheidung. Die unterste Ebene mergt zuerst. Sie ist damit in main, und main geht ins nächste Deployment, lange bevor der Stack fertig ist. Zwischen dem Merge der Migration und dem Merge des Nachtlaufs können Tage liegen, und in dieser Zeit läuft in Produktion ein Zustand, den es im großen Pull Request nie gab: die Datenbank kennt die neue Spalte, der Code kennt sie nicht.

Daraus folgt eine Bedingung, die für jede Ebene gilt: Jede Ebene muss für sich allein lauffähig und sicher sein. Eine Ebene darf nichts voraussetzen, was erst zwei Ebenen höher kommt.

Für die Migration heißt das genau das, was oben schon im Changeset stand: Spalte anlegen, nullable lassen, das Pflichtfeld kommt später. Die Vergabe-Logik darf deployt sein, ohne dass sie jemand aufruft. Und der Endpunkt kann hinter einem Feature-Schalter liegen, solange der Nachtlauf noch fehlt.

Das ist dieselbe Disziplin, die eine Datenbank-Migration ohne Wartungsfenster ohnehin verlangt: erst erweitern, dann umstellen, dann aufräumen. Wer sie schon anwendet, muss für den Stack nichts Neues lernen. Wer sie nicht anwendet, merkt es hier zum ersten Mal, und das ist der beste Zeitpunkt dafür.

Der Stack macht dieses Problem lediglich sichtbar, indem er es aus dem Deployment-Tag in die Schnittführung vorzieht.

Was CI und Branch Protection im Stack tun

Eine naheliegende Sorge ist, dass Prüfungen nur die oberste Ebene treffen und der Rest ungeprüft durchrutscht. Tatsächlich greifen sie auf jeder Ebene.

Jede Ebene nimmt dieselben Hürden wie ein einzelner Pull Request gegen main: dieselben Workflows starten, dieselben CODEOWNER müssen freigeben, auch beim Pull Request mitten in der Kette.

Vier Ebenen sind damit vier vollständige CI-Läufe, und zwar pro Durchgang. Jeder gh stack sync und jeder kaskadierende Rebase stößt die Läufe auf allen Ebenen erneut an. Bei einem lebhaften main sind das vier Läufe je Trunk-Bewegung, nicht vier insgesamt. Das ist der zweite Preis nach dem Rebase, und er fällt bei langsamen Pipelines ins Gewicht.

Dasselbe gilt für erteilte Freigaben. Der kaskadierende Rebase schreibt die Branches der oberen Ebenen neu, und wenn im Repository „Dismiss stale pull request approvals“ aktiv ist, sind die Reviews dort danach weg. Je öfter sich unten etwas ändert, desto häufiger passiert das. Wer viel im Stack arbeitet, prüft diese Regel einmal bewusst, bevor sie im Alltag auffällt. Wer den Stack in einem Repository mit bestehenden Rulesets einführt, sollte deshalb zuerst einen Test-Stack durchspielen und ansehen, was tatsächlich läuft, statt es aus den Regeln abzuleiten.

Stacked Pull Requests oder ein großer Pull Request?

Ein Stack macht die Arbeit nicht kleiner. Er verteilt sie anders, und dabei verschiebt sich Aufwand vom Reviewer zum Autor.

Ein großer Pull Request Stack aus mehreren Ebenen
Aufwand beim Schneiden keiner, alles landet auf einem Branch fällt vor dem ersten Commit an
Review pro Einheit groß, Aufmerksamkeit verteilt sich ungleich klein, ein Thema pro Ebene
Parallel prüfbar nein, eine Diskussion für alles ja, mehrere Reviewer gleichzeitig
Reaktion auf Änderungen unten ein Commit dazu kaskadierender Rebase durch alle Ebenen
CI-Läufe einer einer pro Ebene
Zustand nach dem ersten Merge alles oder nichts Zwischenstände gehen in Produktion
Risiko im Detail einzelne riskante Datei geht unter riskante Datei hat ihr eigenes Review

Die Zeile, die den Ausschlag gibt, ist die vorletzte. Ein großer Pull Request ist eine Alles-oder-nichts-Entscheidung, ein Stack eine Folge von Teilentscheidungen. Das hilft, solange die Teile für sich Sinn ergeben, und wird sonst zum Ballast.

Stacked Pull Requests ohne die gh-Erweiterung

Unter der Oberfläche kommt nichts Neues dazu: Branches wie immer, Pull Requests wie immer. Neu ist allein die Verknüpfung, die GitHub dazwischen speichert, und die erzwingt keine bestimmte Werkzeugkette.

Wer die Kette lokal mit einem anderen Werkzeug verwaltet, etwa mit Jujutsu, Sapling oder git-town, hängt sie auf GitHub mit einem Kommando ein:

gh stack link

Damit werden bestehende Pull Requests zu einem Stack verbunden, ohne dass die lokale Verwaltung an gh stack übergeht. --base legt dabei den Trunk fest. Für Branches ohne offenen Pull Request legt link Entwürfe an.

Das ist auch die Antwort für alle, die den kaskadierenden Rebase längst mit Bordmitteln fahren. git rebase --update-refs zieht beim Rebase die Branch-Spitzen einer Kette mit, statt sie einzeln nachziehen zu müssen. Installiert sein muss die Erweiterung dafür trotzdem, aber sie kommt nur für dieses eine Kommando zum Einsatz, der Alltag bleibt beim gewohnten Werkzeug.

Öffentliche Vorschau: was heute noch fehlt

Der Stand ist vier Tage alt, und GitHub schreibt selbst dazu, dass sich das Feature noch ändern kann. Was heute konkret fehlt oder nicht geht:

  • Ein Stack endet an der Repository-Grenze. Wer über einen Fork arbeitet, wie im klassischen Open-Source-Ablauf, bleibt beim einzelnen Pull Request.
  • GitHub Desktop kann es nicht. Wer dort arbeitet, braucht für den Stack die CLI oder die Weboberfläche.
  • Die Merge-Queue-Unterstützung rollt noch aus. Sie ist vorgesehen, aber möglicherweise noch nicht in deinem Repository.
  • Umsortieren geht nur über die CLI. Ohne sie legst du die Reihenfolge vorher fest oder baust den Stack neu auf.
  • Server-seitige Rebases erzeugen unsignierte Commits. Wenn dein Repository signierte Commits erzwingt, rebase lokal mit gh stack rebase, sonst scheitert der Merge an einer Regel, die mit dem Stack nichts zu tun hat.

Ausprobieren solltest du es trotzdem. Nur eben nicht ausgerechnet an dem Feature, das am Freitag live muss.

Wann sich ein Stack lohnt und wann nicht

Ein Stack lohnt sich in diesen Fällen:

  • Die Änderung hat natürliche Schichten, die aufeinander aufbauen: Schema, Domäne, Schnittstelle, Betrieb.
  • Verschiedene Teile brauchen verschiedene Reviewer, etwa die Migration jemand anderen als das DTO.
  • Ein Teil ist deutlich riskanter als der Rest und soll seine eigene Aufmerksamkeit bekommen.
  • Die unteren Teile sind für sich fertig und können schon in Produktion, während oben noch gearbeitet wird.

In diesen Fällen lohnt er sich nicht:

  • Die Änderung ist eine Einheit, die sich nur künstlich zerschneiden lässt. Dann sind die Ebenen Verwaltungsaufwand ohne Nutzen.
  • Die Ebenen ergeben einzeln keinen sicheren Zustand in Produktion.
  • Der Stack würde tief werden. Jede zusätzliche Ebene kostet einen weiteren CI-Lauf und eine weitere Runde beim kaskadierenden Rebase.
  • Das Team arbeitet über Forks, oder die Merge Queue ist gesetzt und für Stacks noch nicht da.
  • Das Feature muss als Ganzes oder gar nicht live. Dann ist die Alles-oder-nichts-Eigenschaft eines großen Pull Requests genau das, was du willst.

Häufige Fragen

Was sind Stacked Pull Requests?
Abhängige Pull Requests im selben Repository, als Kette organisiert. Das Ziel eines Pull Requests ist nicht der Default-Branch, sondern der Branch der Ebene unter ihm; einzig die unterste Ebene zielt auf den Trunk. Jede Ebene hat ihren eigenen Diff und wird für sich geprüft, gemergt wird von unten nach oben.

Brauche ich die gh-Erweiterung?
Für die lokale Verwaltung und den kaskadierenden Rebase ja, für den Stack selbst nein. Darunter liegen gewöhnliche Branches und gewöhnliche Pull Requests. Wer lokal mit Jujutsu, Sapling oder git-town arbeitet, verbindet die Kette auf GitHub mit gh stack link.

Funktioniert Squash-Merge in einem Stack?
Die Doku listet ihn neben Merge-Commit und Rebase, und über die CLI wählst du die Methode mit gh stack merge --merge-method. Zur Private Preview gab es Berichte, dass Squash und Rebase die Stack-Zuordnung brechen konnten, weil beide die Commits neu schreiben. An einem Test-Stack ist das in fünf Minuten geklärt. Die Merge Queue rollt separat noch aus.

Kann ich eine mittlere Ebene zuerst mergen?
Nicht isoliert. Der Merge einer mittleren Ebene nimmt alles darunter mit, übersprungen wird nie. Den Rest darüber kannst du offen lassen, er wird automatisch umgehängt.

Laufen die CI-Checks auf jeder Ebene?
Ja. Jede Ebene nimmt dieselben Hürden wie ein einzelner Pull Request gegen den Default-Branch, Workflows wie Freigaberegeln. Vier Ebenen bedeuten also vier CI-Läufe, und jeder Rebase der Kette stößt sie erneut an.

Funktionieren Stacks über Forks hinweg?
Nein, ein Stack endet an der Repository-Grenze. Für den Ablauf über einen Fork bleibt nur der einzelne Pull Request.

Wie tief darf ein Stack sein?
GitHub nennt keine Obergrenze. Die praktische Grenze ergibt sich aus dem Aufwand: Jede Ebene kostet einen eigenen CI-Lauf, und eine Änderung ganz unten läuft als Rebase durch alle Ebenen darüber. Konflikte löst du dann mehrfach.

Was ist mit signierten Commits?
Server-seitige Rebases erzeugen unsignierte Commits. Wenn dein Repository Signaturen erzwingt, mach den Rebase lokal mit gh stack rebase, bevor du pushst.

Fazit

Vier kleine Reviews sind angenehmer als ein großes. Der Nutzen eines Stacks steckt aber woanders, in der Frage, die vor dem ersten Commit beantwortet sein muss: Ist jede Ebene für sich allein sicher?

Wer diese Frage beantworten kann, hat die Änderung verstanden. Ohne eine Antwort darauf wäre sie auch als ein großer Pull Request nicht sauber ausgeliefert worden, es wäre nur später aufgefallen.

Nimm für den ersten Versuch nicht das nächste große Feature, sondern eine Änderung, die du ohnehin schon gedanklich in zwei Teile zerlegst. Installiere die Erweiterung, leg zwei Ebenen an, schick sie mit gh stack submit raus und merge sie von unten nach oben. Der ganze Ablauf ist in einer halben Stunde durch, und danach weißt du, ob die Aufteilung in deinem Repository etwas bringt.

Quellen

Alle Code-Beispiele sind eigene. Das Beispiel-Feature (Rechnungsnummernkreise pro Mandant) ist konstruiert, die beschriebene Mechanik ist gegen die oben genannten Quellen geprüft, Stand August 2026.

$ lang DE EN ES