Skip to content

Add native GitHub Stacks support (--github-stacks)#363

Open
ezyang wants to merge 1 commit into
mainfrom
gh/ezyang/73/head
Open

Add native GitHub Stacks support (--github-stacks)#363
ezyang wants to merge 1 commit into
mainfrom
gh/ezyang/73/head

Conversation

@ezyang

@ezyang ezyang commented Jul 16, 2026

Copy link
Copy Markdown
Owner

GitHub Stacks (https://github.github.com/gh-stack/, private preview) is a
first-class REST resource: POST /repos/{o}/{r}/stacks with a bottom-to-top
list of PR numbers links them into a stack, /stacks/{n}/add appends, and
/stacks/{n}/unstack dissolves. The server requires each PR's base ref to
be the head ref of the PR below it, which is exactly what --direct submits
produce (and classic base-branch layout never satisfies), so the feature
requires --direct and warns otherwise.

Enable with ghstack --direct --github-stacks or persistently via
ghstack config github_stacks true.

After push_updates finishes updating PRs, _sync_github_stack reconciles
the server-side stack with the full local chain (all_diffs, not just the
submitted prefix, so prefix-only submits don't truncate an existing
stack): in-sync stacks are a no-op, new PRs on top are appended via /add,
and any other restructure is unstack + recreate since the API has no
reorder endpoint. All stack operations are non-fatal: a 404 (Stacks not
enabled) or any other error logs a warning and the PRs remain submitted
normally.

Live testing against pytorch/pytorch surfaced an undocumented rule that
shaped the design: once a PR is in a native stack, any PATCH /pulls/{n}
that includes the base field is rejected with 422, even if the value is
unchanged. Direct mode previously sent base on every update, so
resubmitting an unchanged stack would crash. Fixes: no-op base updates
are no longer sent at all, and when a resubmit genuinely retargets a PR
(e.g. inserting a commit mid-stack) the affected stack is dissolved first
(_unstack_prs) and recreated after. An alternative was to route
retargeting through a new stack-aware endpoint, but no such endpoint
exists in the API surface gh-stack v0.0.8 uses.

github_real.arest now returns None on 204 No Content (unstack returns
it). The fake endpoint models the stacks resource, including the
base-chaining rule on create/add and the no-base-PATCH rule, so the tests
exercise the same server behavior observed live.

Test Plan:

New test covers create, idempotent resubmit, append-on-top, and
mid-stack insert (dissolve + recreate) in both direct and non-direct
modes; full suite and linters pass:

uv run pytest test/submit/github_stacks.py.test -q
uv run pytest test test_cli_config.py -q
uv run python -m black --check src/ghstack/
uv run python -m flake8 src/ghstack/
uv run python -m mypy src/ghstack/

Verified live against pytorch/pytorch (which has Stacks enabled) with
draft [skip ci] PRs since the fake endpoint can only mirror observed
behavior: initial submit created stack #190147 (#190145 <- #190146),
unchanged resubmit was a no-op, a third commit was appended via /add,
and a mid-stack insert dissolved and recreated the stack as #190150 with
correct order. All test PRs, branches, and stacks were cleaned up.

This PR was authored with an AI assistant.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

GitHub Stacks (private preview) is a first-class REST resource: POST
/repos/{owner}/{repo}/stacks links a bottom-to-top list of PR numbers
into a native stack, /stacks/{n}/add appends, and /stacks/{n}/unstack
dissolves.  This adds an opt-in flag that links the PRs ghstack submits
into such a stack, enabled per-invocation with `--github-stacks` or
persistently via `ghstack config github_stacks true`.

The feature requires --direct mode: the server requires each PR's base
ref to be the head ref of the PR below it, which is exactly what
--direct submits produce and the classic base-branch layout never
satisfies.  Without --direct, a warning is logged and no stack is
linked.

After push_updates finishes updating PRs, _sync_github_stack reconciles
the server-side stack against the full local chain (all_diffs, not just
the submitted prefix, so prefix-only submits don't truncate an existing
stack): an in-sync stack is a no-op, new PRs on top are appended via
/add, and any other restructure dissolves and recreates the stack since
the API has no reorder endpoint.  All stack operations are non-fatal —
a 404 (Stacks not enabled on the repository) or any other error logs a
warning and the PRs remain submitted normally.

Live testing surfaced an undocumented server rule that shaped the
design: once a PR is in a native stack, any PATCH /pulls/{n} that
includes the base field is rejected with 422, even if the value is
unchanged.  Direct mode previously sent base on every update, so
resubmitting an unchanged stack would crash.  To handle this, no-op
base updates are no longer sent at all, and when a resubmit genuinely
retargets a PR (e.g. inserting a commit mid-stack) the affected stack
is dissolved first via _unstack_prs and recreated afterwards.

Supporting changes: github_real's REST layer now returns None on
204 No Content (which unstack returns), and the fake GitHub endpoint
models the stacks resource — including the base-chaining rule on
create/add and the no-base-PATCH rule — so tests exercise the same
server behavior observed live.  A new test covers stack creation,
idempotent resubmit, append-on-top, prefix-only submits, and mid-stack
insert (dissolve + recreate) in both direct and non-direct modes.
The README documents the feature and its --direct requirement.

[ghstack-poisoned]
@ezyang

ezyang commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Stack from ghstack (oldest at bottom):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant