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.
70
88%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
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.