Stacked PR workflow with the Graphite CLI (gt) — create stacks, submit, sync, restack, split/squash/fold, track existing branches, collaborate on shared stacks, configure repo/CI for Graphite, and operate the merge queue.
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
This skill is the counterpart to the graphite skill (CLI workflow). It covers the one-time and ongoing repository / GitHub / CI configuration that lets gt stacks merge cleanly. If the user is driving gt locally, use the graphite skill instead.
Source docs (cite when proposing changes):
Work top-down. Each step has a checkpoint; if it fails, fix that step before moving on — a later step won't compensate for an earlier misconfiguration.
gt submit --stack on a 2-branch test stack pushes both branches without a "too many branches in one push" error.branches-ignore: "**/graphite-base/**" and ensure CI runs on every stacked PR, not just trunk (CI configuration).
graphite-base/* ref.graphite-base/<something> (often "branch not found" right after a merge).gt submit --stack pushes every branch in the stack atomically; the cap breaks that.These look helpful but break Graphite's automatic rebases:
| Setting | Required state | Why |
|---|---|---|
| Dismiss stale approvals on new push | Disabled | Graphite rebases during merge — every rebase counts as a new push and would dismiss approvals. (Graphite ships an open-source GitHub Action as an alternative if your org needs dismissal behavior.) |
| Require approval of the most recent push | Disabled | Graphite re-targets the base branch before merge; that re-targeting is a reviewable push and blocks the merge. |
| GitHub native merge queue | Disabled | Use Graphite's merge queue (or none) — GitHub's queue doesn't understand stacks and can merge out of order. |
| Deployment checks as required status | Disabled | Not supported by Graphite today. |
| Signed commits | Off, unless every engineer has uploaded a signing key in Graphite | Otherwise Graphite's server-side rebase will produce unsigned commits and fail the check. |
Safe defaults that don't conflict with Graphite:
Graphite supports three modes. The right answer depends on org constraints, not preference.
| Mode | When to pick it | What to configure |
|---|---|---|
| No merge queue | Small team, low merge contention, no required checks that depend on a queued state | Nothing extra — gt submit --merge / "Merge stack" in Graphite UI just merges directly. |
| Graphite merge queue | You want stack-aware batching, parallel CI, and ordered merges | Enable in Graphite settings; disable GitHub's native queue in branch protection. |
| External / non-Graphite merge queue | Org standardizes on a third-party queue (e.g. mergify, kodiak, custom) | Per-repo "External Merge Queue Integration (Beta)" wiring so Graphite knows who to hand the PR off to. |
Do not enable both Graphite's queue and GitHub's native queue — they will fight over who owns the merge.
Source: setup-merge-queue-integration.
graphite-base/* branchesGraphite materializes temporary branches under graphite-base/<something> during rebase/merge. They get deleted seconds later. CI that targets them will fail with "branch not found" mid-run.
GitHub Actions:
on:
pull_request:
types: [opened, reopened, synchronize]
branches-ignore:
- "**/graphite-base/**"Avoid pull_request: types: [edited] — it fires every time a base branch is re-pointed, which Graphite does often.
Graphite gates merge on the required checks for the PR's target branch (usually main). If those checks only run on pull_request against main, the upstack PRs won't have results until their parent merges — meaning a tall stack merges one PR at a time, with full CI waits between each.
Configure workflows to run on any branch except graphite-base/*. The exception above (branches-ignore) already handles this for pull_request; mirror it for push triggers if you have them.
Source: setup-recommended-ci-settings.
For repos with tall stacks, Graphite can skip CI on intermediate PRs and only run it on a few branches per stack. Per-repo config:
Mechanism: a wrapper job calls Graphite's API and emits a boolean. Dependent jobs gate on it.
GitHub Actions sketch:
jobs:
optimize_ci:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check.outputs.skip }}
steps:
- id: check
run: |
# Set steps.check.outputs.skip from Graphite's CI-optimization endpoint.
# Prefer Graphite's official CI-optimization Action over a hand-rolled
# API call — see the stacking-and-ci doc (linked above) for the current
# action name, its inputs, and the exact output field to read.
...
test:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip != 'true'
runs-on: ubuntu-latest
steps: ...Buildkite: either a dedicated "Stack CI Optimizer" pipeline that runs before others (recommended), or an optimizer job at the start of each pipeline.
CI runs unconditionally if:
This is the safe-by-default posture — a broken optimizer never silently skips CI.
Source: stacking-and-ci.
For orgs with GitHub IP allowlists:
branches-ignore: "**/graphite-base/**" change, read the existing workflow first — some workflows use branches: explicitly and need a different shape.