Repository navigation
feat(autoland): land into branches without a merge queue - #79
Merged
Merged
Conversation
## Summary `stack-pr autoland` refused to run unless `autoland.merge_queue=true`, because it only knew how to land through the GitHub merge queue. That rules out dogfooding it on repos like this one, which have no queue. This adds a direct-merge mode. **Choosing the mode.** The mode is worked out once per run, before anything lands, and stored in `AutolandOptions.merge_mode` (a new `MergeMode` enum: `QUEUE` / `DIRECT`, in `autoland/merge_mode.py`): - `autoland.merge_queue` is now an optional override. Setting it to `true` or `false` forces that mode and skips the GitHub lookup. Users who already set `merge_queue=true` see no change. - If it is unset (the new default), autoland asks GitHub whether the target branch has a merge queue (`has_merge_queue`). - If GitHub can't say and nothing is configured, autoland stops before doing anything and tells you which config to set. - A direct run prints `Merging directly into <target> (no merge queue)` at the start, including in `--dry-run`, `--resume` and `--replan`. A run whose queue was auto-detected prints the queue version of that line. A run configured with `merge_queue=true` prints nothing extra, so its output stays byte-identical. **One mode for both merge paths.** The land step now calls `Waits.merge_and_wait` (renamed from `enqueue_and_wait`), which picks a path from `opts.merge_mode`: - Queue mode uses the unchanged `enqueue_and_wait`. - Direct mode uses the new `merge_directly`. The GitHub stack merge reads the same mode. It sends `direct_merge` + squash in direct mode, and in that mode it never treats "not in the queue" as a boot. **What direct mode does:** - Waits for approval and checks the same way queue mode does. Then waits until GitHub reports the PR mergeable (`CLEAN` / `UNSTABLE` / `HAS_HOOKS`). The queue-only "UNKNOWN means already queued" check is skipped. - `BEHIND` means the target branch requires PRs to be up to date and this one isn't. autoland says once what to do (rebase and `stack-pr submit`, or update the branch on GitHub) and keeps polling with a short status line until the branch is updated. It never pushes to the PR on its own. - Squash-merges with `gh pr merge --squash --match-head-commit <head>`. Pinning the head means a push after the checks passed makes the merge fail, instead of landing code nobody checked. - Confirms the PR is `MERGED`, polling briefly in case GitHub reports the merge late. - If `gh` errors, autoland first checks whether the PR merged anyway (for example, the response was lost). A real failure is retried up to `max_queue_retries` times, re-checking approval and CI before each retry, the same as queue mode. A closed PR fails the step. **Same squash commit as `stack-pr land`.** The subject/body logic is moved out of `cli.land_pr` into `cli.squash_commit_message`, and `land_pr` now uses it. Direct-mode autoland reads the PR's head commit message from GitHub and builds the squash commit with this helper: the stack-info trailer is removed and ` (#N)` is added to the subject. The queue path is unchanged, since the queue uses its own merge settings. Stack merges in direct mode go through the merge-async API, which doesn't accept a message, so they get GitHub's default squash message. **New state.** A PR being merged directly is now `PRState.MERGING` (label "Merging", new `StepTone.MERGING`, drawn in cyan) instead of being mislabeled "In merge queue". Existing state values and `STATE_VERSION` are unchanged. **Queue mode is unchanged.** All existing golden files pass without being regenerated. Queue-mode stack merges still ask GitHub whether the branch has a queue before choosing the request body, since `merge_queue=true` used to be required even on branches without one. Docs: README (autoland description and requirements, `merge_queue` config, merging as a stack), CHANGELOG, and the `autoland` / `--max-queue-retries` help text are updated. ## Test plan - [x] New golden scenarios in `tests/test_autoland_golden.py`: - `direct_one_at_a_time`: no queue detected; two PRs land directly with a workflow step between them. - `direct_merge_as_stack`: a GitHub stack merge with `direct_merge`. The fake GitHub rejects a request body that doesn't match whether the branch has a queue. - `direct_merge_retry`: the first direct merge fails, then the retry succeeds. - `merge_mode_unknown`: the queue lookup fails and nothing is configured, so autoland exits early with the config hint. - The harness now handles runs that stop before writing a checkpoint. - [x] New `tests/test_autoland_merge_mode.py`, using FakeServices: - Resolving the mode: the config override (true/false/unset), auto-detection (true/false), and detection failure. - The direct-merge wait: uses the `stack-pr land` squash message pinned to the head commit; queue mode still enqueues; a failed merge is retried after re-checking; gives up once retries run out; a merge that errored but went through; GitHub reporting the merge late; a closed PR; a `BEHIND` branch. - A direct stack merge isn't mistaken for a boot. - [x] `tests/test_autoland.py`: the merge-queue gate test (`test_run_autoland_requires_merge_queue`) is replaced by two tests of `run_autoland` with no `merge_queue` config, using a fake GitHub and with the landing flows stubbed out so no real land can run: - The target has no queue, so the run goes ahead in direct mode and says so. - GitHub can't say, so autoland exits 1 with the config hint before any flow runs. - [ ] Manual: autoland a real stack on a repo without a merge queue (e.g. this stack on micahjsmith/stack-pr) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> stack-info: PR: #79, branch: micahjsmith/stack/12
micahjsmith
force-pushed
the
micahjsmith/stack/12
branch
from
October 13, 2026 00:50
fcad8ba to
181774a
Compare
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PRs:
Summary
stack-pr autolandrefused to run unlessautoland.merge_queue=true, because it only knew how to land through the GitHub merge queue. That rules out dogfooding it on repos like this one, which have no queue. This adds a direct-merge mode.Choosing the mode. The mode is worked out once per run, before anything lands, and stored in
AutolandOptions.merge_mode(a newMergeModeenum:QUEUE/DIRECT, inautoland/merge_mode.py):autoland.merge_queueis now an optional override. Setting it totrueorfalseforces that mode and skips the GitHub lookup. Users who already setmerge_queue=truesee no change.has_merge_queue).Merging directly into <target> (no merge queue)at the start, including in--dry-run,--resumeand--replan. A run whose queue was auto-detected prints the queue version of that line. A run configured withmerge_queue=trueprints nothing extra, so its output stays byte-identical.One mode for both merge paths. The land step now calls
Waits.merge_and_wait(renamed fromenqueue_and_wait), which picks a path fromopts.merge_mode:enqueue_and_wait.merge_directly.The GitHub stack merge reads the same mode. It sends
direct_merge+ squash in direct mode, and in that mode it never treats "not in the queue" as a boot.What direct mode does:
CLEAN/UNSTABLE/HAS_HOOKS). The queue-only "UNKNOWN means already queued" check is skipped.BEHINDmeans the target branch requires PRs to be up to date and this one isn't. autoland says once what to do (rebase andstack-pr submit, or update the branch on GitHub) and keeps polling with a short status line until the branch is updated. It never pushes to the PR on its own.gh pr merge --squash --match-head-commit <head>. Pinning the head means a push after the checks passed makes the merge fail, instead of landing code nobody checked.MERGED, polling briefly in case GitHub reports the merge late.gherrors, autoland first checks whether the PR merged anyway (for example, the response was lost). A real failure is retried up tomax_queue_retriestimes, re-checking approval and CI before each retry, the same as queue mode. A closed PR fails the step.Same squash commit as
stack-pr land. The subject/body logic is moved out ofcli.land_printocli.squash_commit_message, andland_prnow uses it. Direct-mode autoland reads the PR's head commit message from GitHub and builds the squash commit with this helper: the stack-info trailer is removed and(#N)is added to the subject. The queue path is unchanged, since the queue uses its own merge settings. Stack merges in direct mode go through the merge-async API, which doesn't accept a message, so they get GitHub's default squash message.New state. A PR being merged directly is now
PRState.MERGING(label "Merging", newStepTone.MERGING, drawn in cyan) instead of being mislabeled "In merge queue". Existing state values andSTATE_VERSIONare unchanged.Queue mode is unchanged. All existing golden files pass without being regenerated. Queue-mode stack merges still ask GitHub whether the branch has a queue before choosing the request body, since
merge_queue=trueused to be required even on branches without one.Docs: README (autoland description and requirements,
merge_queueconfig, merging as a stack), CHANGELOG, and theautoland/--max-queue-retrieshelp text are updated.Test plan
tests/test_autoland_golden.py:direct_one_at_a_time: no queue detected; two PRs land directly with a workflow step between them.direct_merge_as_stack: a GitHub stack merge withdirect_merge. The fake GitHub rejects a request body that doesn't match whether the branch has a queue.direct_merge_retry: the first direct merge fails, then the retry succeeds.merge_mode_unknown: the queue lookup fails and nothing is configured, so autoland exits early with the config hint.tests/test_autoland_merge_mode.py, using FakeServices:stack-pr landsquash message pinned to the head commit; queue mode still enqueues; a failed merge is retried after re-checking; gives up once retries run out; a merge that errored but went through; GitHub reporting the merge late; a closed PR; aBEHINDbranch.tests/test_autoland.py: the merge-queue gate test (test_run_autoland_requires_merge_queue) is replaced by two tests ofrun_autolandwith nomerge_queueconfig, using a fake GitHub and with the landing flows stubbed out so no real land can run:🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com