Skip to content

refactor(autoland): execute each step through a StepContext with injected services - #71

Merged
micahjsmith merged 1 commit into
micahjsmith/stack/4from
micahjsmith/stack/5
Oct 12, 2026
Merged

micahjsmith merged 1 commit into
micahjsmith/stack/4from
micahjsmith/stack/5

Conversation

@micahjsmith

@micahjsmith micahjsmith commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Stacked PRs:


Summary

Phase 3 of the autoland step-abstraction stack. Execution moves out of engine.execute_plan's isinstance chain and into the steps. Each step now runs against an explicit context and injected services.

  • AutolandStep.execute(self, sc: StepContext) -> StepResult is a new abstract method. LandStep, WorkflowStep, and ConfirmStep implement it with the logic of their old engine branches, unchanged.
  • Services (new autoland/services.py) is a frozen dataclass of everything a step may use: github, ui (print, input that raises EOFError, and markup escape), waits (approval, checks, merge queue, workflow), repo (landed_sha, rebase_and_resubmit), and sleep. ui, waits, and repo are Protocols, so a fake is just a small class.
  • StepContext (new steps/execution.py) is a frozen dataclass with index, total, run (the LandingContext), opts, common, and services, plus these:
    • checkpoint(): saves the run.
    • upcoming(): the StepView descriptions of the steps that follow, for the confirm prompt's "Next steps".
    • landed_sha(): the code landed so far.
    • ends_landed_prefix: whether this step is the last one in the already-landed prefix.
    • banner(title, style): the "Step i/n" heading.
  • StepResult is a frozen dataclass with abort_reason (empty means success) and landed_sha. It has done() and abort() constructors.
  • The landed SHA now travels explicitly. A land step reports the merge commit of the PR it landed in StepResult.landed_sha, and the engine records it on the run. A workflow step reads it from sc.landed_sha(). If nothing has landed yet, the engine looks up the target branch head and records it before the step waits. That is the same point at which the old code wrote it, so a Ctrl+C checkpoint taken during the wait still contains it. The already-landed prefix works through ends_landed_prefix: only the prefix's last step looks up its SHA, as before.
  • The engine keeps the plan walk, the checkpoint before each step, recording what steps report, and aborting. It has no step-type checks left.
  • Native stack merging stays in the engine for now. It lives in a small _NativeStackMerge.before_step hook, marked temporary. It runs before each step and does what the old land branch did: merge the run of land steps starting there, then fall back to landing one at a time for the rest of that run. It does not belong in LandStep, because steps must stay independent of each other. Phase 4's plan-optimization pass replaces it with a policy that groups land steps into one execution unit.
  • Production services delegate at call time. Services.production() resolves runtime.console, gh.github, waits.wait_for_*, waits._refresh_last_landed_sha, waits.rebase_and_resubmit, and runtime.resilient_sleep through their module attributes each time they are called. So every existing test patch, including all of the golden harness's seams, still intercepts what steps do. Phase 6 moves tests over to injected fakes, and then this delegation can go.
  • execute_plan gains a keyword-only services argument, which defaults to production. commands.py passes Services.production().
  • waits.wait_for_workflow gains an optional target_sha, which defaults to the run's landed SHA as before.
  • display._next_steps_lines is gone. The confirm step now formats its own "Next steps" list from sc.upcoming().

No user-visible change. Console output, checkpoints, and status output are byte-for-byte identical.

Test plan

  • The golden autoland scenarios (tests/test_autoland_golden.py) pass with no golden files regenerated.
  • New tests/test_autoland_step_execution.py runs each step type in isolation, with a hand-built StepContext and fake UI, waits, and repo, and no module patching:
    • land: the happy path with a rebase; the top of the stack, with no rebase; a PR that merges while waiting; a PR that was already merged; an already-landed step reporting its SHA only at the end of the prefix; aborts on approval, checks, and queue failures; and a failed rebase that still reports the landed SHA.
    • workflow: waits for the given SHA; failure; already done.
    • confirm: retries until y, with an escaped condition and the numbered "Next steps" (moved from the old _next_steps_lines tests); the final step; EOF aborts with today's message; already confirmed.
  • Engine tests with injected services: a workflow before any land waits for the target branch head, and one after a land waits for the SHA that land reported; an abort reason is recorded and checkpointed.
  • The full suite passes with an empty GH_CONFIG_DIR and GH_TOKEN unset.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com

…cted services

## Summary

Phase 3 of the autoland step-abstraction stack. Execution moves out of `engine.execute_plan`'s `isinstance` chain and into the steps. Each step now runs against an explicit context and injected services.

- **`AutolandStep.execute(self, sc: StepContext) -> StepResult`** is a new abstract method. `LandStep`, `WorkflowStep`, and `ConfirmStep` implement it with the logic of their old engine branches, unchanged.
- **`Services`** (new `autoland/services.py`) is a frozen dataclass of everything a step may use: `github`, `ui` (`print`, `input` that raises `EOFError`, and markup `escape`), `waits` (approval, checks, merge queue, workflow), `repo` (`landed_sha`, `rebase_and_resubmit`), and `sleep`. `ui`, `waits`, and `repo` are `Protocol`s, so a fake is just a small class.
- **`StepContext`** (new `steps/execution.py`) is a frozen dataclass with `index`, `total`, `run` (the `LandingContext`), `opts`, `common`, and `services`, plus these:
  - `checkpoint()`: saves the run.
  - `upcoming()`: the `StepView` descriptions of the steps that follow, for the confirm prompt's "Next steps".
  - `landed_sha()`: the code landed so far.
  - `ends_landed_prefix`: whether this step is the last one in the already-landed prefix.
  - `banner(title, style)`: the "Step i/n" heading.
- **`StepResult`** is a frozen dataclass with `abort_reason` (empty means success) and `landed_sha`. It has `done()` and `abort()` constructors.
- **The landed SHA now travels explicitly.** A land step reports the merge commit of the PR it landed in `StepResult.landed_sha`, and the engine records it on the run. A workflow step reads it from `sc.landed_sha()`. If nothing has landed yet, the engine looks up the target branch head and records it before the step waits. That is the same point at which the old code wrote it, so a Ctrl+C checkpoint taken during the wait still contains it. The already-landed prefix works through `ends_landed_prefix`: only the prefix's last step looks up its SHA, as before.
- **The engine** keeps the plan walk, the checkpoint before each step, recording what steps report, and aborting. It has no step-type checks left.
- **Native stack merging stays in the engine for now.** It lives in a small `_NativeStackMerge.before_step` hook, marked temporary. It runs before each step and does what the old land branch did: merge the run of land steps starting there, then fall back to landing one at a time for the rest of that run. It does not belong in `LandStep`, because steps must stay independent of each other. Phase 4's plan-optimization pass replaces it with a policy that groups land steps into one execution unit.
- **Production services delegate at call time.** `Services.production()` resolves `runtime.console`, `gh.github`, `waits.wait_for_*`, `waits._refresh_last_landed_sha`, `waits.rebase_and_resubmit`, and `runtime.resilient_sleep` through their module attributes each time they are called. So every existing test patch, including all of the golden harness's seams, still intercepts what steps do. Phase 6 moves tests over to injected fakes, and then this delegation can go.
- `execute_plan` gains a keyword-only `services` argument, which defaults to production. `commands.py` passes `Services.production()`.
- `waits.wait_for_workflow` gains an optional `target_sha`, which defaults to the run's landed SHA as before.
- `display._next_steps_lines` is gone. The confirm step now formats its own "Next steps" list from `sc.upcoming()`.

No user-visible change. Console output, checkpoints, and status output are byte-for-byte identical.

## Test plan

- [x] The golden autoland scenarios (`tests/test_autoland_golden.py`) pass with no golden files regenerated.
- [x] New `tests/test_autoland_step_execution.py` runs each step type in isolation, with a hand-built `StepContext` and fake UI, waits, and repo, and no module patching:
  - land: the happy path with a rebase; the top of the stack, with no rebase; a PR that merges while waiting; a PR that was already merged; an already-landed step reporting its SHA only at the end of the prefix; aborts on approval, checks, and queue failures; and a failed rebase that still reports the landed SHA.
  - workflow: waits for the given SHA; failure; already done.
  - confirm: retries until `y`, with an escaped condition and the numbered "Next steps" (moved from the old `_next_steps_lines` tests); the final step; EOF aborts with today's message; already confirmed.
- [x] Engine tests with injected services: a workflow before any land waits for the target branch head, and one after a land waits for the SHA that land reported; an abort reason is recorded and checkpointed.
- [x] The full suite passes with an empty `GH_CONFIG_DIR` and `GH_TOKEN` unset.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
stack-info: PR: #71, branch: micahjsmith/stack/5
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