| name | gh-stack |
| description | Split a large change into GitHub native stacked pull requests and manage them with the gh stack extension — create or adopt a stack, cascade-rebase it, keep it in sync, and land it bottom-up with an atomic stack merge. Use when a change is too large for one PR, when asked to stack or restack PRs, when a PR's base is another feature branch, or when maintaining a chain of dependent PRs in this repo. |
GitHub Native Stacked Pull Requests
A stack is a chain of two or more PRs in one repository: the bottom targets main, and each
one above targets the branch of the PR below it. GitHub tracks the stack as a first-class object
and renders a stack map on every member, so reviewers can see the layering without reading each
body.
Use a stack when a change is large enough that one PR would be hard to review but the parts are
genuinely dependent. For one or two independent PRs, don't — a plain branch off main is simpler.
Prerequisites
gh extension install github/gh-stack
gh stack --version
Docs: https://docs.github.com/en/pull-requests/how-tos/stacked-pull-requests
Constraints worth knowing before you start:
- Same repository only. Cross-fork stacks are not supported, so this works for maintainers
pushing branches to
camunda/camunda-platform-helm, not for fork-based contributions.
- Merge bottom-up. A PR cannot land before the ones below it.
- GitHub Desktop does not support stacks.
Setting a base branch does NOT create a stack
This is the trap. Creating a PR whose --base is the branch below it produces a correct base
chain and no stack: the PR reports .stack == null and gets no stack map. The stack is an
explicit object you create or join.
gh pr create --draft --base <branch-below> --head <my-branch> --title "..." --body-file body.md
gh stack link <stack-number> <pr-number>
Verify, don't assume:
gh api /repos/camunda/camunda-platform-helm/pulls/<n> --jq '.stack'
Starting a stack
Two entry points, depending on whether the branches already exist.
From scratch, building up as you go:
gh stack init
gh stack add <next-branch>
gh stack submit
gh stack submit opens an editor to set each PR's title, body, and draft state. Pass --auto
(or run non-interactively) to accept auto-generated titles — avoid that here, because this repo
enforces Conventional Commit PR titles (see below).
From branches that already exist:
gh stack init branch1 branch2 branch3
Adopting PRs that already exist
gh stack link works without local tracking state, which makes it the right tool for a stack
that was built by hand with chained bases.
gh stack link 6884 6889 6891 6893 6902
gh stack link 6894 6904
Arguments may be branch names, PR numbers, or PR URLs. Arguments already in the stack are
skipped and existing PRs are never removed, so re-running is safe. A numeric first argument
is treated as a stack number only when a stack with that number exists; stack and PR numbers
never overlap.
Daily loop
gh stack view
gh stack sync
gh stack rebase
gh stack checkout <stack|pr|url|branch>
gh stack up / down / top / bottom / switch / trunk
gh stack sync fetches, reconciles the local stack against GitHub, fast-forwards trunk,
cascade-rebases each branch onto its updated parent, force-pushes atomically with
--force-with-lease --atomic, syncs PR state, and links the open PRs into a stack when two or
more exist.
Use gh stack sync or gh stack rebase instead of rebasing each branch by hand. Rebasing a
stack manually means picking the right old base for every branch, and getting it wrong silently
drops commits — in this repo that has already cost a lost feature commit that had to be recovered
by cherry-pick. The cascade does the bookkeeping.
Rebasing rewrites history and force-pushes, which is correct here: this repo forbids merge
commits (see AGENTS.md), so git merge is never the way to take in upstream changes.
Restructuring
gh stack modify
gh stack unstack
If a layer turns out to be independent, take it out of the stack and retarget it at main
rather than leaving reviewers to guess why it is stacked.
Landing the stack
gh stack merge
gh stack merge <stack-number>
gh stack merge <pr-number>
This is GitHub's atomic stack merge: every PR up to your chosen one merges in a single
all-or-nothing operation, so if any one cannot merge, none do. Interactively it offers a wizard
to choose how far up to merge and which merge method; --yes or a non-interactive terminal
merges the whole selection without prompting.
When a PR merges, GitHub automatically re-targets and rebases the PRs above it, so there is no
need to repoint bases by hand. Merging a mid-stack PR leaves the ones above open, re-targeted at
the stack's base.
Confirm before relying on it in this repo: the full ~33-deploy matrix runs in the merge
queue, not on the PR (see the rfr-validation skill). How an atomic stack merge interacts with
this repo's merge queue and required checks has not been verified here — check with a maintainer
before using gh stack merge on a stack that must go through the queue, and fall back to merging
one PR at a time if in doubt.
Repo conventions that still apply per PR
A stack does not exempt any member from the normal rules in AGENTS.md and
docs/contribution-and-collaboration.md:
- Conventional Commit titles, individually. Each PR is titled for its own diff.
feat:,
fix:, refactor:, docs:, and revert: are reserved for PRs that change user-facing chart
files; a PR in the stack that only touches .github/, scripts/, or tests must use ci:,
build:, chore:, or test: or CI rejects it. It is normal for one stack to mix types.
- Draft-first, then
crev. Open each PR as a draft, run
crev https://github.com/camunda/camunda-platform-helm/pull/<n> against it, address findings,
then gh pr ready <n>. Review the whole stack before landing any of it, because a change
demanded in a lower PR forces a cascade rebase of everything above.
- Regenerate artifacts in the PR that changes them, not in a later layer, or the intermediate
PR is left with a failing
make go.test.
- Do not hand-maintain position markers. GitHub owns
position and size; a [TAG N/M]
marker in the title or a stack table copied into every body is a second source of truth that
goes stale the moment the stack grows. Let the stack map speak.
Inspecting a stack from the API
Useful when scripting or when the UI is not to hand.
gh api /repos/camunda/camunda-platform-helm/pulls/<n> --jq '.stack'
gh api /repos/camunda/camunda-platform-helm/stacks/<stack-number> \
--jq '.pull_requests[] | "#\(.number) \(.head.ref) \(.title)"'
for n in <prs>; do
gh api /repos/camunda/camunda-platform-helm/pulls/$n \
--jq '"\(.stack.position)/\(.stack.size) #\(.number) draft=\(.draft) \(.title)"'
done
Gotchas
- A correct base chain is not a stack. Always check
.stack after creating a PR.
gh must be recent enough for the extension. gh extension install github/gh-stack
failing or gh stack reporting an unknown command means the CLI needs upgrading
(brew upgrade gh).
- The bottom PR drifts from
main. A long-lived stack's base commit ages; gh stack sync
fast-forwards trunk and cascades, and gh stack view marks branches needing a rebase with ⚠.
- Force-pushes invalidate in-progress reviews. Cascade-rebasing rewrites every branch above
the change, so batch review rounds rather than rebasing after each comment.
- Keep each layer independently green. A reviewer may check out any single PR; a layer that
only passes once a later one lands is mis-split.