Merge an entire stack of pull requests with one label. Each branch is rebased onto the trunk after its predecessor merges, then auto-merges when CI passes.
Works with any stacked-PR workflow — git-spice, Graphite, or manually stacked branches. The action doesn't depend on either tool and never touches their navigation comments.
- You add a label (default:
merge-stack) to the top-most PR of the stack you want to ship. - The start action walks down the base chain of that PR, applies the same label to every PR below it, and enables GitHub's native squash-auto-merge on the bottom PR (the one whose base is the trunk).
- GitHub merges the bottom PR when its CI passes.
- The continue action fires on each merge: it rebases every
remaining labeled branch onto its new base in turn (using
git rebase --onto <new-base> <pre-merge-tip>to cleanly drop the squash-merged ancestor commits), force-pushes the rebased SHAs, and enables auto-merge on the new bottom PR. - Repeat until the entire chain is merged.
If any branch fails to rebase cleanly, the continue action strips the label from every remaining labeled PR and comments on each with the failed run URL — so the stack pauses safely instead of leaving half-rebased branches around.
You can label any PR in the stack — the action merges everything
up to and including the labeled PR. For example, in a stack of
#57 → #58 → #59 → #60 → #61 → #62, labeling #59 merges #57,
#58, and #59 in order, then stops.
PRs that sit above the labeled PR in the same stack (here: #60, #61,
#62) are rebased onto the new main so they don't show stale
"conflicting" status in the GitHub UI, but they are not merged.
If you decide to ship them later, label the top of the remaining
stack and the cascade resumes.
In the repository where you're using this action:
- Settings → General → Pull Requests → "Allow auto-merge" must be enabled.
- Settings → General → Pull Requests → "Automatically delete head branches" is strongly recommended. GitHub auto-retargets dependent PRs to the trunk when the merged branch is deleted, which this action relies on.
- CI should run only on PRs targeting your trunk branch, e.g.:
Otherwise CI runs redundantly on every intermediate PR. (This is also the standard pattern for stacked-PR repos in general.)
on: pull_request: branches: [main]
A token secret in your repo or org:
- A Personal Access Token (classic) with
reposcope, or a fine-grained PAT withContents: writeandPull requests: write. - The built-in
GITHUB_TOKENwill not work; see Why a PAT? below.
Add two thin caller workflows to your repository.
name: Merge Stack — Start
on:
pull_request:
types: [labeled]
permissions:
contents: read
pull-requests: write
concurrency:
group: merge-stack-start-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
start:
# Only act on human-applied `merge-stack` labels on non-fork PRs.
# The sender check prevents recursive firing when the action labels
# the next PR down the chain itself.
if: >-
github.event.label.name == 'merge-stack'
&& github.event.sender.type == 'User'
&& github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: kalbasit/stackmerge-action/start@main
with:
token: ${{ secrets.GHA_PAT_TOKEN }}name: Merge Stack — Continue
on:
pull_request:
types: [closed]
permissions:
contents: write
pull-requests: write
concurrency:
group: merge-stack-continue-${{ github.repository }}
cancel-in-progress: false
jobs:
continue:
if: >-
github.event.pull_request.merged == true
&& contains(github.event.pull_request.labels.*.name, 'merge-stack')
&& github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: kalbasit/stackmerge-action/continue@main
with:
token: ${{ secrets.GHA_PAT_TOKEN }}Replace GHA_PAT_TOKEN with whatever you've named your PAT secret.
Pin to a tag (e.g. @v1) instead of @main once you've validated the
behavior in your repo.
Both actions accept the same inputs:
| Input | Default | Description |
|---|---|---|
token |
(required) | GitHub token. PAT with repo scope. |
label |
merge-stack |
The label that triggers stack merging. |
trunk |
main |
The trunk branch name. |
If you customize label, remember to update the if: condition in
your caller workflows to match.
Your caller workflow must grant at least:
- start:
contents: read,pull-requests: write - continue:
contents: write,pull-requests: write
GitHub's built-in GITHUB_TOKEN has two limitations that block this
workflow:
- It cannot enable auto-merge on PRs it didn't author.
- Commits pushed with
GITHUB_TOKENdo not trigger other workflows. Since the continue action force-pushes restacked branches, those branches need to re-run CI for the next auto-merge to fire — that requires a PAT.
- The action only operates on PRs from the same repository — fork PRs are skipped. Stacks pretty much only work intra-repo anyway.
- Triggers use
pull_request(notpull_request_target); the calling workflow file is fetched from the PR head, so changes to the caller take effect on the same PR that introduces them. This matches how most CI workflows are configured. - The "find the bottom of the chain" logic explicitly excludes the
just-merged PR by number and head-ref to work around GitHub's
eventual consistency on
--state open. - The action does not regenerate any tool-specific navigation comments (e.g. git-spice's stack tree). The user's local view stays the source of truth for those.
Apache 2.0 — see LICENSE.