This is a command-line tool that helps you create multiple GitHub pull requests (PRs) all at once, with a stacked order of dependencies.
Imagine that we have a change A and a change B depending on A, and we
would like to get them both reviewed. Without stacked PRs one would have to
create two PRs: A and A+B. The second PR would be difficult to review as it
includes all the changes simultaneously. With stacked PRs the first PR will
have only the change A, and the second PR will only have the change B. With
stacked PRs one can group related changes together making them easier to
review.
Example:
There are several tool that help with stacked PRs.
- stack-pr (this project): A stack is a set of commits on a branch. Each commit is one PR. The commits are modified over time via a interactive rebase-based workflow. Each PR description automatically gets a preamble section showing the full stack. The tool manages a mapping from commit => temp branch => PR with state stored in the commit body itself. Support for GitHub only via the
ghCLI. - git-spice: A stack is a set of branches. Each branch is one PR. The branches are modified over time via normal git operations. Branches must be tracked by the tool with a separate local state store. A special restack commit rebases all the branches. Support for non-linear stack branches and multiple git hosting providers.
- jj-stack: A stack is a set of Jujutsu bookmarks. Jujutsu already conceptually supports stacks locally, so this tool focuses on syncing the local repo state to GitHub.
- gh stack: GitHub's own stacked PRs (in public preview), with a
ghCLI extension. A stack is a set of branches tracked by the extension, and it is also an object on GitHub: the PRs are registered as a stack, which GitHub shows on each PR, rebases server-side, and merges in one operation (through the merge queue, where there is one). Same-repository branches only.
stack-pr can complement gh stack rather than replace it. stack-pr keeps its
commit-per-PR workflow and plain base-branch PRs, and stack-pr autoland uses
GitHub's native stacks to land them (see Merging as a
stack).
This is a non-comprehensive list of dependencies required by stack-pr:
- Install
gh, e.g.,brew install ghon MacOS. - Run
gh auth loginwith SSH
This fork is not published to PyPI. To install it directly from the GitHub repo via pipx run:
pipx install 'stack-pr[rich] @ git+https://github.com/micahjsmith/stack-pr.git'Manually, you can clone the repository and run the following command:
pipx install .stack-pr allows you to work with stacked PRs: submit, view, and land them.
If you'd rather drive the tool through git, install it as a git alias:
stack-pr installgit stack view
git stack submitThe most common workflow is simple:
- Create a feature branch from
main:
git checkout main
git pull
git checkout -b my-feature- Make your changes and create multiple commits (one commit per PR you want to create)
# Make some changes
git commit -m "First change"
# Make more changes
git commit -m "Second change"
# And so on...- Review what will be in your stack:
stack-pr view # Always safe to run, helps catch issues early- Create/update the stack of PRs:
stack-pr submitNote:
exportis an alias forsubmit.
- To update any PR in the stack:
- Amend the corresponding commit
- Run
stack-pr viewto verify your changes - Run
stack-pr submitagain
- To rebase your stack on the latest main:
git checkout my-feature
git pull origin main # Get the latest main
git rebase main # Rebase your commits on top of main
stack-pr submit # Resubmit to update all PRs- When your PRs are ready to merge, you have three options:
Option A: Using stack-pr land:
stack-pr landThis will:
- Merge the bottom-most PR in your stack
- Automatically rebase your remaining PRs
- You can run
stack-pr landagain to merge the next PR once CI passes
Option B: Using GitHub web interface:
- Merge the bottom-most PR through GitHub UI
- After the merge, on your local machine:
git checkout my-feature git pull origin main # Get the merged changes stack-pr submit # Resubmit the stack to rebase remaining PRs
- Repeat for each PR in the stack
Option C: Using stack-pr autoland:
stack-pr autolandThat's it!
Pro-tip: Run
stack-pr viewfrequently - it's a safe command that helps you understand the current state of your stack and catch any potential issues early.
stack-pr has five main commands:
submit(orexport) - create a new stack of PRs from the given set of commits. One can think of this as "push my local changes to the corresponding remote branches and update the corresponding PRs (or create new PRs if they don't exist yet)".view- inspect the given set of commits and find the linked PRs. This command does not push any changes anywhere and does not change any commits. It can be used to examine what other commands did or will do.abandon- remove all stack metadata from the given set of commits. Apart from removing the metadata from the affected commits, this command deletes the corresponding local and remote branches and closes the PRs.adopt- bring an existing, normally-created PR understack-prmanagement. This embeds stack metadata into the bottom-most commit pointing at that PR, so subsequentsubmitruns update the existing PR (preserving its review history) instead of creating a new one.land- merge the bottom-most PR in the current stack and rebase the rest of the stack on the latest main.
stack-pr also has several other commands:
autoland- land the whole stack automatically: wait for approvals and CI (retrying flaky checks), merge each PR bottom-to-top — through the GitHub merge queue where the target branch has one, or directly otherwise — rebase and re-submit the rest after each merge, and resume cleanly after an interruption (seeautoland.merge_queuebelow).config- set configuration values in the config file. Similar togit config, it takes a setting in the format<section>.<key>=<value>and updates the config file (.stack-pr.cfgby default).install- install stack-pr as a git alias so it can be invoked asgit stack(see Use as a git subcommand).help- print help. Useful asgit stack help, sincegit stack --helpis intercepted by git for aliases.
A usual workflow is the following:
while not ready to merge:
make local changes
commit to local git repo or amend existing commits
create or update the stack with `stack-pr submit`
merge changes with `stack-pr land`You can also use view at any point to examine the current state, and
abandon to drop the stack.
Under the hood, the tool creates and maintains branches named
$USERNAME/stack/$BRANCH_NUM (the name pattern can be customized via
--branch-name-template option) and embeds stack metadata into commit messages (see ).
You don't work with those managed branches or edit that metadata
manually. Instead of pushing to these branches you should use submit,
instead of deleting them you should use abandon and instead of merging them
you should use land.
The tool looks at commits in the range BASE..HEAD and creates a stack of PRs
to apply these commits to TARGET. By default, BASE is main (local
branch), HEAD is the git revision HEAD, and TARGET is main on remote
(i.e. origin/main). These parameters can be changed with options -B, -H,
and -T respectively and accept the standard git notation: e.g. one can use
-B HEAD~2, to create a stack from the last two commits.
The first step before creating a stack of PRs is to double-check the changes we’re going to post.
By default stack-pr will look at commits in main..HEAD range and will create
a PR for every commit in that range.
For instance, if we have
# git checkout my-feature
# git log -n 4 --format=oneline
**cc932b71c** (**my-feature**) Optimized navigation algorithms for deep space travel
**3475c898f** Fixed zero-gravity coffee spill bug in beverage dispenser
**99c4cd9a7** Added warp drive functionality to spaceship engine.
**d2b7bcf87** (**origin/main, main**) Added module for deploying remote space probes
Then the tool will consider the top three commits as changes, for which we’re trying to create a stack.
Pro-tip: a convenient way to see what commits will be considered by default is the following command:
alias githist='git log --abbrev-commit --oneline $(git merge-base origin/main HEAD)^..HEAD'We can double-check that by running the script with view command - it is
always a safe command to run:
# stack-pr view
...
VIEW
**Stack:**
* **cc932b71** (No PR): Optimized navigation algorithms for deep space travel
* **3475c898** (No PR): Fixed zero-gravity coffee spill bug in beverage dispenser
* **99c4cd9a** (No PR): Added warp drive functionality to spaceship engine.
SUCCESS!If everything looks correct, we can now submit the stack, i.e. create all the
corresponding PRs and cross-link them. To do that, we run the tool with
submit command:
# stack-pr submit
...
SUCCESS!The command accepts a couple of options that might be useful, namely:
--draft- mark all created PRs as draft. This helps to avoid over-burdening CI.--draft-bitmask- mark select PRs in a stack as draft using a bitmask where1indicates draft, and0indicates non-draft. For example--draft-bitmask 0010to make the third PR a draft in a stack of four. The length of the bitmask must match the number of stacked PRs. Overridden by--draftwhen passed.--reviewer="handle1,handle2"- assign specified reviewers.
If the command succeeded, we should see “SUCCESS!” in the end, and we can now
run view again to look at the new stack:
# stack-pr view
...
VIEW
**Stack:**
* **cc932b71** (#439, 'ZolotukhinM/stack/103' -> 'ZolotukhinM/stack/102'): Optimized navigation algorithms for deep space travel
* **3475c898** (#438, 'ZolotukhinM/stack/102' -> 'ZolotukhinM/stack/101'): Fixed zero-gravity coffee spill bug in beverage dispenser
* **99c4cd9a** (#437, 'ZolotukhinM/stack/101' -> 'main'): Added warp drive functionality to spaceship engine.
SUCCESS!We can also go to github and check our PRs there:
If we need to make changes to any of the PRs (e.g. to address the review
feedback), we simply amend the desired changes to the appropriate git commits
and run submit again. If needed, we can rearrange commits or add new ones.
submit simply syncs the local changes with the corresponding PRs. This is why
we use the same stack-pr submit command when we create a new stack, rebase our
changes on the latest main, update any PR in the stack, add new commits to the
stack, or rearrange commits in the stack.
When we are ready to merge our changes, we use land command.
# stack-pr land
LAND
Stack:
* cc932b71 (#439, 'ZolotukhinM/stack/103' -> 'ZolotukhinM/stack/102'): Optimized navigation algorithms for deep space travel
* 3475c898 (#438, 'ZolotukhinM/stack/102' -> 'ZolotukhinM/stack/101'): Fixed zero-gravity coffee spill bug in beverage dispenser
* 99c4cd9a (#437, 'ZolotukhinM/stack/101' -> 'main'): Added warp drive functionality to spaceship engine.
Landing 99c4cd9a (#437, 'ZolotukhinM/stack/101' -> 'main'): Added warp drive functionality to spaceship engine.
...
Rebasing 3475c898 (#438, 'ZolotukhinM/stack/102' -> 'ZolotukhinM/stack/101'): Fixed zero-gravity coffee spill bug in beverage dispenser
...
Rebasing cc932b71 (#439, 'ZolotukhinM/stack/103' -> 'ZolotukhinM/stack/102'): Optimized navigation algorithms for deep space travel
...
SUCCESS!This command lands the first PR of the stack and rebases the rest. If we run
view command after land we will find the remaining, not yet-landed PRs
there:
# stack-pr view
VIEW
**Stack:**
* **8177f347** (#439, 'ZolotukhinM/stack/103' -> 'ZolotukhinM/stack/102'): Optimized navigation algorithms for deep space travel
* **35c429c8** (#438, 'ZolotukhinM/stack/102' -> 'main'): Fixed zero-gravity coffee spill bug in beverage dispenserThis way we can land all the PRs from the stack one by one.
The example above used the default commit range - main..HEAD, but you can
specify a custom range too. Below are several commonly useful invocations of
the script:
# Submit a stack of last 5 commits
stack-pr submit -B HEAD~5
# Use 'origin/main' instead of 'main' as the base for the stack
stack-pr submit -B origin/main
# Do not include last two commits to the stack
stack-pr submit -H HEAD~2These options work for all script commands (and it’s recommended to first use
them with view to double check the result). It is possible to mix and match
them too - e.g. one can first submit the stack for the last 5 commits and then
land first three of them:
# Inspect what commits will be included HEAD~5..HEAD
stack-pr view -B HEAD~5
# Create a stack from last five commits
stack-pr submit -B HEAD~5
# Inspect what commits will be included into the range HEAD~5..HEAD~2
stack-pr view -B HEAD~5 -H HEAD~2
# Land first three PRs from the stack
stack-pr land -B HEAD~5 -H HEAD~2Note that generally one doesn't need to specify the base and head branches
explicitly - stack-pr will figure out the correct range based on the current
branch and the remote main by default.
stack-pr treats your local commits as the source of truth and force-pushes
each PR's branch to match. If a PR branch is changed directly on the remote —
most often by accepting a "Commit suggestion" during review, or editing a
file through the GitHub web UI — that commit exists only on the remote, not in
your local stack.
stack-pr pushes with --force-with-lease, so instead of silently discarding
such a change, the next submit (or land/autoland) stops with an error
naming the affected branch. Reconcile it by folding the upstream commit into the
local commit that backs that PR:
- Fetch the upstream state. This also updates the remote-tracking ref so the
next push is allowed to proceed:
git fetch origin
- Start an interactive rebase over your stack and mark the affected commit
edit(git stops on it with the commit already applied):git rebase -i origin/main
- Apply the upstream commit(s) and fold them into that commit:
git cherry-pick -n origin/<pr-branch> # the branch named in the error; # use a range A^..B for multiple git commit --amend --no-edit git rebase --continue
- Re-submit the stack:
stack-pr submit
The change now lives in the single commit that backs the PR, so the stack stays one-commit-per-PR and future updates won't lose it.
These arguments can be used with any subcommand:
-R, --remote: Remote name (default: "origin")-B, --base: Local base branch-H, --head: Local head branch (default: "HEAD")-T, --target: Remote target branch (default: "main")--hyperlinks/--no-hyperlinks: Enable/disable hyperlink support (default: enabled)-V, --verbose: Enable verbose output from Git subcommands (default: false)--branch-name-template: Template for generated branch names (default: "$USERNAME/stack"). The following variables are supported:$USERNAME: The username of the current user$BRANCH: The current branch name$ID: The location for the ID of the branch. The ID is determined by the order of creation of the branches. If$IDis not found in the template, the template will be appended with/$ID.
Submit a stack of PRs.
Options:
--keep-body/--no-keep-body: Keep the current PR body instead of regenerating it from the commit message on every submit (default: true). Pass--no-keep-bodyto overwrite the body from the commit.--keep-title/--no-keep-title: Keep the current PR title instead of overwriting it from the commit subject on every submit (default: true). Pass--no-keep-titleto overwrite the title from the commit.-d, --draft: Submit PRs in draft mode (default: false)--draft-bitmask: Bitmask for setting draft status per PR--reviewer: List of reviewers for the PRs (default: from $STACK_PR_DEFAULT_REVIEWER or config)-s, --stash: Stash all uncommitted changes before submitting the PR
Land the bottom-most PR in the current stack.
If the land.style config option has the disable value, this command is not available.
Abandon the current stack.
Takes no additional arguments beyond common ones.
Bring an existing, normally-created PR under stack-pr management. This is
useful when you opened a PR the usual way (with its own review history) and now
want to stack more PRs on top of it.
By default adopt looks at the bottom-most commit of the current range
(main..HEAD by default) and embeds stack metadata into it pointing at the
target PR. Once adopted, run stack-pr submit to update that PR and push the
rest of the stack; the original PR is updated in place rather than closed and
recreated.
Arguments:
pr(optional): PR number or URL to adopt. If omitted, the PR associated with the currently checked-out branch is used.
Options:
--commit: Commit (any git revision) to attach the PR to, when it isn't the bottom-most one - for example when inserting a new PR underneath an existing one. If omitted, the bottom-most commit of the stack is used.
Notes:
- The PR must be in the
OPENstate. - The PR's existing head branch is preserved (recorded in the metadata), so its review history and URL are kept.
submitwill force-push the adopted commit to the PR's branch, so if you have squashed or rebased since opening the PR, its diff will be updated accordingly.adoptwarns when the local commit's contents differ from the PR's head.
Typical workflow (stack a new PR on top of an existing one):
# You already have an open PR for branch 'my-feature'.
git checkout my-feature
stack-pr adopt # adopt the existing PR for 'my-feature' first
git commit -m "Second change" # stack a new change on top
stack-pr view # confirm the bottom PR is now managed
stack-pr submit # update the existing PR + create the new oneStacking a new PR underneath an existing one (so the new change lands first):
-
Collapse the branch into a single commit (one commit == one PR) with an interactive rebase, marking every commit except the first as
squash(orfixup):git checkout my-feature git rebase -i $(git merge-base origin/main HEAD) -
Adopt the existing PR onto that commit while it is still the bottom commit:
stack-pr adopt
-
Insert the new change beneath the adopted commit by building it on top of
mainand rebasing the adopted commit onto it:git checkout -b tmp-new-bottom origin/main # ... make the new change ... git commit -m "New change (lands first)" git rebase --onto tmp-new-bottom origin/main my-feature git branch -D tmp-new-bottom
-
Review and submit the stack:
stack-pr view # new commit at the bottom, existing PR on top stack-pr submit # creates the new PR; re-bases the existing one onto it
Alternatively, build the stack in any order first and then adopt the existing
PR onto the right commit directly with stack-pr adopt --commit <ref>.
Land the entire stack automatically, one PR at a time from the bottom up. Run it from the repo root while on the stack's head branch. For each PR it waits for approval and CI (re-running flaky checks), merges the PR, and after each merge rebases and re-submits the rest of the stack. Progress is checkpointed so an interrupted run can be resumed.
How a PR is merged depends on the target branch:
- With a merge queue, the PR is added to the queue, and
autolandwaits for it to merge, re-adding it (after checking approval and CI again) if it is booted. The queue merges with its own configured method. - Without one,
autolandsquash-merges the PR itself once GitHub reports it mergeable, with the same commit messagestack-pr landuses: the commit's message without the stack-info trailer, with(#<PR>)after the subject. The merge is pinned to the commitautolandchecked, so a push in between fails it rather than landing unchecked code. A failed merge is retried (up tomax_queue_retriestimes) after checking approval and CI again. If the branch must be up to date with the target before merging and has fallen behind,autolandsays so and waits for you to rebase andstack-pr submit(or to update the branch on GitHub), then carries on.
autoland asks GitHub which applies when it starts, and prints Merging directly into <target> (no merge queue) when there is no queue. Set
autoland.merge_queue to skip the question (see below). If GitHub can't
answer and the config doesn't say, autoland stops before doing anything.
Options:
--dry-run: Discover and display the stack, then exit.-n, --count N: Land only the bottomNPRs of the stack, leaving the rest open (they are rebased onto the newly-landed commits). Defaults to the whole stack. Landing goes bottom-to-top, so this always lands a prefix of the stack — useful for landing one (or a few) ready PRs at a time. In-imode you can do the same by keeping only the bottom PRs'lsteps.--branch BRANCH: Land a stack rooted onBRANCHusing a temporary worktree (so your current checkout is left untouched). The worktree is removed on success and preserved on failure for debugging.--always-cleanup: Always remove the temporary worktree, even on failure.-i, --interactive: Edit the landing plan in$EDITORfirst, insertingworkflowcheckpoints (w <workflow>— wait for a named GitHub Actions workflow to complete with the landed code) andconfirmcheckpoints (c [condition]— pause for manual confirmation) between land steps. Theconditionaftercis optional: it names what you want to verify before proceeding (e.g.c QA sign-off complete), and is shown in the prompt when autoland reaches that step (Confirm "QA sign-off complete" is complete — ready to proceed?). A barecjust promptsReady to proceed?. Whenautoland.default_workflowis configured, the pre-filled plan already ends with aw <default_workflow>step, which you can edit or delete. Command steps (x <command>— run a command, and stop unless it succeeds) can go there too; see Running commands.--plan-file PATH: Load the landing plan from a file instead of editing it interactively. The file uses the exact same format as the-ieditor (l/w <workflow>/c [condition]/x <command>lines, with#comments and blank lines ignored — see Example plan below), so you can save a plan from-i, or write one by hand, and reuse it for repeatable/scripted runs. Plans conventionally use the.autoland-plansuffix. Mutually exclusive with-i; can't be combined with-n/--count(the file already specifies which PRs to land) or--resume(which restores the plan from a checkpoint — use--replan --plan-fileto continue with a new one).--merge-as-stack/--no-merge-as-stack: Whether to merge each run of consecutivelsteps in one go, as a GitHub stack (see Merging as a stack). Overridesautoland.merge_as_stack; on by default.--resume: Resume a previously interrupted run from its checkpoint.--replan: Change the plan, or the code, of an interrupted or running autoland and continue it without losing its progress. See Changing the plan mid-land.--status: Show whether an autoland is in progress for the branch (with--branch/--state-fileto pick another), where its state and lock files are, when it last saved a checkpoint, and the plan's progress as of that checkpoint, including why a stopped run stopped. It also lists saved autolands for other branches. This is read-only and offline: it doesn't take the lock or query GitHub, so it's safe to run while a land is in progress.-o json/--output json: With--status, print the report as JSON instead. The top-level keys are always present, withnullfor anything unknown:status(in_progress,stopped, ornone),branch,base,state_file,state_file_exists,lock_file,pid,last_saved(ISO 8601),abort_reason,resume_command,plan(total/done/remainingcounts and astepslist), andother_runs. Each step hasnumber,type(land/workflow/confirm/exec),outcome(done/active/pending/failed),status,is_nextanddetail, pluspr_number/pr_url/title,workflow,condition, orcommanddepending on its type. If the state file can't be read, stdout stays empty and the error goes to stderr with a nonzero exit.--state-file PATH: Override the checkpoint path (default:~/.stack-pr/autoland/<branch>.json).--poll-interval,--max-check-retries,--max-queue-retries,--workflow-timeout: Override the corresponding[autoland]config values.
Only one autoland can run against a given branch at a time: while a run is in
progress it holds a per-branch lock (~/.stack-pr/autoland/<branch>.json.lock)
next to its checkpoint. The lock is released automatically when the run ends —
including on failure or Ctrl+C — while the checkpoint is kept so you can
--resume. If you start a new (non---resume) autoland while a checkpoint
from a previous run still exists, autoland warns that a land is already in
progress and asks whether to replan — continue that run with the plan you
just gave, keeping its progress (the default) — or overwrite it and start
over, after which the previous run can no longer be resumed. If that run is
still going, it offers to stop it and replan.
Everything repo-specific is configured under [autoland] (see Config
files), so a repository captures its workflow in
.stack-pr.cfg:
[repo]
target = main
[autoland]
merge_queue = true
required_checks = test,lint
poll_interval = 120
max_check_retries = 3
max_queue_retries = 3
default_workflow = deploy.yamlmerge_queue(default unset): whether the target branch merges through a merge queue. Unset,autolandasks GitHub at the start of each run;trueorfalseskips the question and forces that mode — for when GitHub can't say (e.g. the token can't read the branch's rules), or to skip the lookup.required_checks(default empty): comma-separated CI check names that gate a merge. When empty, all reported (non-skipped) checks must pass.default_workflow(default empty): when set, an interactive (-i) landing plan is pre-filled with aw <default_workflow>step after the land steps, so a repo's usual post-land workflow wait is there by default (still editable/removable in$EDITOR).merge_as_stack(defaulttrue): merge each run of consecutivelsteps as a GitHub stack rather than one PR at a time.
By default, autoland lands each run of two or more consecutive l steps (no
w, c, or x step between them) with GitHub's native stacked
PRs
instead of one PR at a time:
- It registers the run's PRs as a GitHub stack.
stack-pralready bases each PR on the one below it, which is what a GitHub stack requires. A stack that already exists is reused if the run sits at its bottom. - It waits for every PR in the run to be approved and to pass its checks, just as when landing them one at a time.
- It makes one merge request on the top PR of the run, which merges it and
every PR below it — all enqueued together in the merge queue, where the
repo has one, or merged directly otherwise. Each PR still lands as its own
commit (made with the merge queue's configured method, or squashed when
there is no queue, with GitHub's default squash message rather than the
one
stack-pr landuses), and every PR is closed as merged. - It rebases and re-submits the rest of the stack once, rather than after every PR.
So a plan of four bare l steps waits on the merge queue once instead of four
times. A w, c, or x step ends a run, since it has to see the PRs below it
land first; --dry-run lists the runs a plan would merge as stacks.
To keep PRs from merging together, put a c step between them in the plan,
which splits the run there (autoland asks you to confirm before continuing).
To land every PR on its own, pass --no-merge-as-stack.
If a run can't merge as a stack — GitHub refuses to create the stack, the merge
request fails, or the stack is booted from the merge queue — autoland
dissolves the stack it made and lands the rest of that run one PR at a time,
keeping any PRs the stack merge did land. Things to be aware of:
- GitHub's stacked PRs are in public preview, so the API may change.
- GitHub evaluates every PR's merge requirements against the stack's base
branch, and runs CI on every PR in a stack as if it targeted that branch.
So although the PRs above the bottom of a
stack-prstack target the branch below them, registering the stack starts theirpull_requestworkflows for the target branch, andautolandthen waits for those checks. - While PRs are in a GitHub stack, GitHub only merges them through the stack
merge API, so
gh pr mergeandstack-pr landcan't land them.autolandonly leaves a stack behind when it is interrupted mid-merge;--resumepicks that stack up again. - A PR that is already in a GitHub stack that doesn't start with the run (for
example one made with
gh stack) is left alone, and the run lands one PR at a time.
Richer live progress tables are shown when the optional rich dependency is
installed (pipx install 'stack-pr[rich]' or add the rich extra); otherwise
autoland prints plain-text status.
With -i, autoland opens a plan in $EDITOR. Each non-comment line is one
step, run top to bottom: l <pr> lands that PR, w <workflow> waits for a
named GitHub Actions workflow to finish with the landed code,
c [condition] pauses for manual confirmation (the condition is optional), and
x <command> runs a command (see Running commands). For
a three-PR stack, a plan that lands the bottom PR, waits for a deploy, gets
manual sign-off, then lands the rest looks like:
# Autoland plan — edit steps below.
# l <pr> = land that PR (a number, or its URL); a bare 'l'
# lands the next PR in the stack instead
# w <workflow> = wait for a workflow to complete
# c [condition] = pause for manual confirmation; the optional
# condition names what to verify before proceeding
# (e.g. 'c QA sign-off complete')
# x <command> = run a command, and stop unless it exits 0; it runs
# without a shell (use 'x bash -c ...' for pipes)
#
# Lines starting with # are comments and are ignored.
# Blank lines are ignored.
#
l 101 # Add /widgets API endpoint
w deploy.yaml # wait for the deploy of PR #101 to finish
c QA sign-off complete # pause until QA has signed off
l 102 # Wire up the widgets UI
l 103 # Update the docs
A bare l with no PR number lands whatever PR is next in the stack, which is
what plans looked like before l learned to name its PR. Both forms can appear
in the same plan.
The generated plan aligns the comments it writes at column 30, in the same
layout the VS Code formatter produces (see Editor support),
so a plan out of -i needs no tidying and stays aligned as you edit it there.
When autoland reaches the c step it prompts
Confirm "QA sign-off complete" is complete — ready to proceed? and waits for
y/Y. A bare c with no condition just prompts Ready to proceed?.
The same format can be loaded from a file with --plan-file instead of editing
it interactively — for example, save the block above to plan.autoland-plan
and run stack-pr autoland --plan-file plan.autoland-plan. This is handy for
repeatable or scripted landings. Any path works, but .autoland-plan is the
conventional suffix: it is what -i writes, and what editors key off to
highlight the plan syntax (see Editor support).
An x <command> step runs a command once the PRs before it have landed, and
stops the land unless it exits 0 — so a manual step that would otherwise be a
c ("provision the new tables, then confirm") can be automated:
l 101 # Add the widgets table
x ./scripts/provision.sh --env staging
l 102 # Start writing widgets
x bash -c 'curl -fsS "$HEALTH_URL" | grep -q ok'
l 103
- The command is split into words the way a POSIX shell would (quotes and
backslashes work), but it runs without a shell: no pipes, redirection,
globs, or
$VARIABLEexpansion. For those, run a shell yourself, as inx bash -c '...'. - As with every step,
#(a space, then#) starts a comment, even inside quotes, so a command can't contain one. - It runs from the top of the checkout being landed (the temporary worktree,
with
--branch), with its output going straight to the terminal, and withautoland's own environment plus:AUTOLAND_LANDED_SHA: the commit of the code landed so far — the merge commit of the last PR this run landed, or else the head of the target branch (empty if it can't be found);AUTOLAND_LANDED_PRS: the PRs the plan has landed before this step, space-separated in plan order, including any that landed in an earlier run;AUTOLAND_BRANCH,AUTOLAND_REMOTE,AUTOLAND_TARGET: the branch whose stack is landing, and the remote and branch it lands into.
- A nonzero exit status, a command killed by a signal, or a command that can't
be started (no such program, not executable) stops the land;
--statusshows why. There is no timeout. --resumeskips a command that succeeded and runs a failed or interrupted one again, so commands should be safe to re-run (idempotent). Ctrl+C while one runs stops it along withautoland. As withwandcsteps, anxstep before an already-landed PR is assumed done, and--replankeeps a finished one when the new plan has the same command after the same landed PRs.
Because each l step names the PR it lands, one plan file stays valid as the
stack lands piece by piece. A PR that has already merged is no longer part of
the stack, so autoland skips its l step — along with any w, c, and x
steps that come before it, since those must have run before that PR could merge:
l 1 # PR #1, already landed upstream
c QA sign-off complete # assumed completed
l 2 # PR #2, already landed upstream
l 3 # <- autoland will land this PR next
You can also write a plan by PR number for a stack that has not started landing
yet — l 1, l 2, l 3 — and it behaves identically to the bare-l form
until the first PR merges.
One workflow this enables: keep a design doc and an autoland plan together in the final PR of the stack. The design doc is updated as the stack changes during development, and the plan is re-used and updated as the stack lands. That gives you an "as implemented" design doc alongside a self-documenting deploy/rollout plan that others can review.
Long plans often change while they land: a confirm step turns up a bug, you fix
the code, and you add a check for the fix. --replan continues the run with
the new plan instead of starting over:
stack-pr autoland --replan # re-read the plan file the run started with
stack-pr autoland --replan --plan-file new.autoland-plan
stack-pr autoland --replan -i # edit the current plan in $EDITOR
stack-pr autoland --replan --dry-run # preview only; changes nothingWithout --plan-file or -i, --replan re-reads the plan file the run was
started with, or replays the run's own plan if it had none — which is all you
need when only the code changed. Either way it:
- Stops the autoland if one is running, after asking. It is stopped the way Ctrl+C in its terminal would stop it: it saves its checkpoint and exits, and the run continues in your terminal.
- Rediscovers the stack, since the code may have changed, and parses the new plan against it. Landed PRs are skipped as usual.
- Carries over each
w,c, andxstep the previous run completed, provided the new plan has the same step (same workflow, condition text, or command) after the same set of landed PRs. A step means "once these PRs have landed, this holds", so its result still stands. Adding, removing, or reordering steps keeps that credit; editing a step's text, or landing a different set of PRs before it, makes it run again. - Shows the replanned progress, lists any completed steps that don't carry
over, warns if GitHub doesn't have all of the stack's code (run
stack-pr submitfirst after changing code), and asks before continuing.
--replan --dry-run does steps 2–4 and stops, without stopping a running
autoland, so you can check what a replan would do before committing to it.
editors/vscode is a VS Code extension that highlights plan
files: the l / w / c / x keywords, PR numbers and URLs, workflow
names, confirmation conditions, commands, and # comments — plus an error scope for steps
autoland would reject, so a typo shows up before you run the plan. It also
formats them: Format Document aligns trailing comments at column 30 (or
past the longest l/w step in the block, when one runs that far) and tidies
step spacing, without changing what the plan does.
It applies to *.autoland-plan files — including the temporary one -i opens
in $EDITOR — and to any file starting with the # Autoland plan header. See
its README to install it.
Inspect the current stack
Takes no additional arguments beyond common ones.
Set a configuration value in the config file.
Arguments:
setting(required): Configuration setting in format<section>.<key>=<value>
Examples:
# Set verbose mode
stack-pr config common.verbose=True
# Disable usage tips (hide verbose output after commands)
stack-pr config common.show_tips=False
# Set target branch
stack-pr config repo.target=master
# Set default reviewer(s)
stack-pr config repo.reviewer=user1,user2
# Set custom branch name template
stack-pr config repo.branch_name_template=$USERNAME/stack
# Disable the land command (require GitHub web interface for merging)
stack-pr config land.style=disable
# Use "bottom-only" landing style for stacks
stack-pr config land.style=bottom-onlyThe config command modifies the config file (the .stack-pr.cfg file in the repo root by default, or the path specified by STACKPR_CONFIG environment variable). If the file doesn't exist, it will be created. If a setting already exists, it will be updated.
Install stack-pr as a git alias so it can be invoked as git <name> (see
Use as a git subcommand). This writes
alias.<name> = !stack-pr to your git config.
Options:
--name: Alias name to create (default:stack, i.e.git stack ...).--local: Write to the current repository's git config instead of the global one.
Print help, optionally for a specific subcommand. This exists primarily so
git stack help works: git intercepts git stack --help for aliases and only
prints 'stack' is aliased to '!stack-pr'.
Arguments:
topic(optional): Subcommand to show help for, e.g.stack-pr help submit(orgit stack help submit).
Default values for command line options can be specified via a config file.
Path to the config file can be specified via the STACKPR_CONFIG envvar, and by
default it's assumed to be .stack-pr.cfg in the repo root.
An example of a config file:
[common]
verbose=True
hyperlinks=True
draft=False
keep_body=True
keep_title=True
stash=False
show_tips=True
[repo]
remote=origin
target=main
reviewer=GithubHandle1,GithubHandle2
branch_name_template=$USERNAME/$BRANCH
[land]
style=bottom-only
[autoland]
merge_queue=True
required_checks=test,lint
poll_interval=120
max_check_retries=3
max_queue_retries=3
merge_timeout=3600
workflow_timeout=10800
default_workflow=deploy.yamlstack-pr does not maintain any state outside of git itself. Instead, it
tracks the stack by embedding a metadata trailer into each managed commit
message:
stack-info: PR: https://github.com/<owner>/<repo>/pull/<number>, branch: <head-branch>
This single line is the source of truth for whether a commit is "managed":
submitdecides whether to create or update a PR for each commit by looking for this trailer. A commit without it gets a new branch and a new PR (and the trailer is then written back into the commit message); a commit with it has its existing PR updated.viewreports the linked PR and branch for each commit by parsing the trailer (showingNo PRwhen it is absent).landandabandonoperate only on commits that carry a valid trailer.
The trailer records two fields: the PR link and the head branch that
stack-pr pushes for that commit (named via --branch-name-template, by
default $USERNAME/stack/$ID). The PR's base branch is not stored — it is
derived from the stack order at runtime (the previous commit's head branch, or
the target branch for the bottom-most PR).
Because the trailer is just text in the commit message, a commit can be brought
under stack-pr management by adding it (which is what adopt does for an
existing PR), and removed by deleting it (which is what abandon does).
Each PR's body contains a Stacked PRs: table of contents listing every PR in
the stack (with __->__ marking the current one). This list is regenerated on
each submit, and it is maintained as PRs land: a PR that has left the active
stack but has merged or been closed is kept in the list (pinned below the active
PRs in its original position), so later PRs retain the full history of the
stack. A PR that left the stack but is still open is dropped.
The list is stored only in the PR bodies - on each submit, stack-pr reads
the previously recorded list back from a surviving PR's body, re-checks the
status of any entries that are no longer in the active stack, and writes the
reconciled list to every PR. As a result, merged/closed entries are pinned at
the bottom of the list; inserting a brand-new commit below an
already-merged one is the one case where an entry can appear slightly out of
order.

