Manage stacked PRs in scylla-cluster-tests using the gh-stack CLI extension (GitHub's native stacked PR support, public preview since 2026-07-31). Use when asked to split a large change into a chain of small reviewable PRs, check stack status, add/push/sync/rebase/merge stack layers, or navigate between stack branches. Covers the fork-vs-upstream remote nuance specific to SCT's contribution model. Adapted from the official github/gh-stack agent skill.
Manage stacked PRs in scylla-cluster-tests using the gh-stack CLI extension (GitHub's native stacked PR support, public preview since 2026-07-31). Use when asked to split a large change into a chain of small reviewable PRs, check stack status, add/push/sync/rebase/merge stack layers, or navigate between stack branches. Covers the fork-vs-upstream remote nuance specific to SCT's contribution model. Adapted from the official github/gh-stack agent skill.
GitHub's native stacked pull requests went to public preview on 2026-07-31 - available to any repository automatically, no per-repo toggle or approval needed.
The fork caveat (read this first)
Cross-fork stacks are not supported. Every branch in a stack must live in the same repository. Regular SCT contributions here go through a personal fork (origin -> fruch/scylla-cluster-tests, PRs opened cross-repo against upstream -> ) - that model does work for a multi-layer stack, because a mid-stack PR's base branch has to exist in the same repo as its head, and a fork-only branch isn't visible to as a valid base.
scylladb/scylla-cluster-tests
not
upstream
You (this account) have admin/push access directly on scylladb/scylla-cluster-tests, so the fix is: for stacked work only, push the stack's branches directly to upstream instead of to the fork. init is a local-only command (it only takes --base); the remote comes into play on the commands that talk to GitHub:
Keep using the fork (origin) as before for regular, single-branch PRs - only stacks need the --remote upstream treatment. --remote is accepted by push, submit, sync, and rebase; checkout, modify, and trunk have no --remote flag; if upstream isn't your git config remote.pushDefault, set it before using those, or resolve the remote explicitly.
Prerequisites
Check the extension is installed and current:
gh extension list | grep gh-stack || gh extension install github/gh-stack
gh extension upgrade stack # upgrade takes the extension name, not the repo slug; flags below assume v0.1.0+
Pre-enable git rerere before the first gh stack init in a repo - init enables it for you but may ask for confirmation on first run, which hangs a non-interactive session even when branch arguments are given:
git config rerere.enabled true
Do not run gh stack alias's default. It aliases gh stack to gs, which collides with Ghostscript on machines that have it installed. If you want a shorthand, pick a name that doesn't collide:
gh stack alias gst
Everything below uses the unambiguous gh stack ... form. Default trunk for this repo is master, not main.
Agent rules (must-follow, non-interactive use)
Always supply branch names as positional arguments to init, add, and checkout. Without them these commands launch interactive prompts that hang in a non-interactive session. Branch arguments alone don't make first-time init fully non-interactive - it can still prompt to enable git rerere; pre-configure it as shown in Prerequisites.
Always use --auto with gh stack submit. Without it, submit opens an interactive editor. With --auto, new PRs default to draft; pass --open too if they should be ready for review.
Always use --json with gh stack view. Without it, view renders an interactive view.
Always pass --remote upstream to push/submit/sync/rebase for stacked work here (see fork caveat above).
Plan layers by dependency order before writing code: foundational changes (shared types, config) go in lower branches; dependent changes (consumers, UI) go in higher branches.
Use standard git add/git commit to control exactly which changes land in which branch. The add -Am "msg" branch shortcut is available but bypasses deliberate staging - reserve it for single-commit layers.
Navigate down to fix a lower layer. If you're on a high layer and need to change something below it, don't patch around it in place - gh stack down (or checkout) to the right branch, commit there, then gh stack rebase --upstack --remote upstream and navigate back up.
Use gh stack merge --yes to merge stacked PRs; gh pr merge does not understand stacks. It merges bottom-up; direct merges are atomic, all-or-nothing. If the base branch uses a merge queue, the PRs are instead enqueued together but may land in separate groups - don't assume rollback semantics there. Scope with a PR number (gh stack merge 15601 --yes merges up to and including that PR) or a stack number.
gh stack checkout <pr-number> can hit an unbypassable conflict prompt if a different local stack already exists on those branches. Run gh stack unstack --local first (keeps the GitHub stack intact), then retry.
Arguments
$ARGUMENTS may contain:
view (default) - show current stack state
init <branches...> - start a new stack from the current branch (remember --base master; init has no --remote flag)
add <branch> - add a new layer on top of the current stack
push - push every branch in the stack (remember --remote upstream)
submit - push + create/update PRs for the whole stack (remember --remote upstream)
sync - fetch, cascade-rebase, push, and sync PR state (the routine command; remember --remote upstream)
rebase - cascading rebase across the stack (finer control / conflict resolution)
trunk - jump to the stack's trunk branch
merge [stack-number | pr-number] - merge the whole current stack, or up to and including a given PR
checkout <stack-number | pr-number | pr-url | branch> - check out a stack
modify - interactive TUI to reorder/rename/fold/drop branches (not agent-drivable)
unstack [number] - remove local tracking and/or the GitHub stack grouping
view
gh stack view --json # required for non-interactive use
gh stack view --short # compact, human use only
Existing branches are adopted; missing ones are created from the trunk. Checks out the last branch given. init is local-only and has no --remote flag - the upstream remote is chosen later, on push/submit/sync/rebase.
add
gh stack add api-routes
git add sdcm/api/routes.py
git commit -m "feature(api-routes): add REST routes for cluster operations" \
-m "Expose the cluster API endpoints consumed by the next stack layer."# shortcut: stage + commit + branch in one step (message must still follow SCT's# conventional-commit format: type(scope): subject header plus a body)
gh stack add -Am "feature(api-routes): add REST routes for cluster operations
Expose the cluster API endpoints consumed by the next stack layer." api-routes
Must be run from the topmost branch (or trunk, for the first layer). Branch names are used verbatim. Commit messages must pass SCT's commitlint rules (type(scope): subject header, body of 30+ characters - see .github/copilot-instructions.md).
push / submit
gh stack push --remote upstream
gh stack submit --remote upstream --auto # required for non-interactive use; new PRs default to draft
gh stack submit --remote upstream --auto --open # create as ready for review instead
Creates a PR per branch that lacks one (base = nearest non-merged ancestor, i.e. master for the bottom layer), updates base branches for existing PRs, and links them into a Stack on GitHub.
sync
gh stack sync --remote upstream
gh stack sync --remote upstream --prune # also delete local branches for merged PRs
If a rebase conflict is hit, all branches are restored and it tells you to run gh stack rebase instead.
rebase
gh stack rebase --remote upstream # whole stack
gh stack rebase --upstack --remote upstream # current branch upward only
gh stack rebase --downstack --remote upstream # trunk to current branch only
gh stack rebase --continue# after resolving conflicts (git add the files first); local-only, no --remote
gh stack rebase --abort # bail out, restore all branches; local-only, no --remote
trunk / navigation
gh stack trunk
gh stack up [n] / gh stack down [n]
gh stack top / gh stack bottom
merge [stack-number | pr-number]
gh stack merge --yes# whole current stack, bottom to top
gh stack merge 15601 --yes# merge up to and including PR #15601
The optional argument is a stack number or a PR number - branch names are not accepted. Direct merges are all-or-nothing: if any PR can't be merged, none are. With a merge queue on the base branch, the PRs are enqueued together instead but may land in separate queue groups.
checkout
gh stack checkout 7 # by stack number
gh stack checkout 15601 # by PR number (pulls from GitHub)
gh stack checkout feature-auth # by branch, local tracking only
unstack [number]
gh stack unstack # current stack: local tracking + GitHub grouping (PRs are NOT deleted)
gh stack unstack 7 # a specific stack, from anywhere in the repo
gh stack unstack --local# local tracking only, never touches GitHub
Exit codes
Code
Meaning
Action
0
Success
Proceed normally
1
Generic error
Read stderr for details (may be a commit/push failure)
2
Not in a stack
gh stack init
3
Rebase conflict
Resolve conflicted files, git add, gh stack rebase --continue
4
GitHub API failure
Check gh auth status, retry
5
Invalid arguments or flags
Fix the command invocation
6
Branch belongs to multiple stacks
gh stack checkout <specific-branch> to disambiguate
7
Rebase already in progress
gh stack rebase --continue (after resolving) or gh stack rebase --abort
8
Stack is locked
Another gh stack process is writing; wait and retry
9
Stacked PRs unavailable on this repo
Report it to the user; there is no per-repo toggle to flip (public preview) - the platform or rollout doesn't cover this repository yet
10
Interrupted modify session needs recovery
gh stack modify --abort to restore the pre-modify state (this skill doesn't drive modify)
Key principles
Each layer is a focused, independently reviewable change - a stack should read as one cohesive story
Stacks are strictly linear - one parent, at most one child per branch; use separate stacks for parallel workstreams
CI and branch protection are enforced on every PR in the stack, not just ones targeting master
Merges go bottom-up; gh stack merge handles the ordering
Stacked branches live on upstream (scylladb/scylla-cluster-tests), not the personal fork