feat(pusher): add Pusher extension and wire it into the merge controller#151
Merged
Conversation
behinddwalls
reviewed
May 7, 2026
## Summary Introduces the `extension/pusher` interface that lands a list of `entity.Change` values onto a target branch with all-or-nothing atomicity, and a git-backed implementation that operates against a local checkout. The merge controller now calls `Pusher.Push` for each batch, transitions the batch to Succeeded on success or Failed on `pusher.ErrConflict`, and nacks any other push error so the queue can retry. ## Pusher interface (extension/pusher) - `Push([]Change) (Result, error)` with an explicit atomicity contract: on a non-nil error nothing has reached the remote. - Per-change `ChangeOutcome` reports either `OutcomeStatusCommitted` with the produced commit SHAs in apply order (one Change can land as multiple commits, e.g. a stack), or `OutcomeStatusAlreadyExisted` with no commits when the change is already present on the target. - `ErrConflict` sentinel marks user-caused apply failures so callers can route them to a non-retry path. ## git implementation (extension/pusher/git) - Per-Push cycle: fetch -> reset --hard origin/<target> -> cherry-pick every URI's head SHA -> push HEAD to refs/heads/<target>. - Cherry-pick uses `--allow-empty` and recovers from "previous cherry-pick is now empty" via `--skip`; genuinely empty resulting commits are rolled back. Both surface as `AlreadyExisted`. - Empty-commit detection compares tree SHAs read via `git cat-file commit` rather than relying on `diff-tree --quiet`'s exit code 1, which has multiple meanings. - A mutex serializes concurrent invocations against the shared checkout. - Push is wrapped with `core/metrics` Begin/Complete so the operation emits the standard push.called / push.succeeded / push.failed / push.latency / push.latency_histogram with error-classification tags. Sub-event counters (push.empty_changes, push.reset_errors, push.cherry_pick_conflicts, push.git_push_errors, push.stale_base_retries, push.stale_base_giveup) live under the same op so dashboards filter on `push.*` to see the whole picture. ## Concurrent-push contention handling - After a push failure the implementation refetches the remote and compares the current tip to the SHA captured at reset time. If it advanced, the failure is treated as contention and the full fetch/reset/cherry-pick/push cycle is retried. Other failures propagate immediately. - Detection by ref-state comparison rather than git error message parsing — robust across git versions and rejection sources (NFF, hook reject, ref-store errors). - Retries are capped at `Params.MaxPushAttempts` (default 10) to bound the worst case on a pathologically busy remote. ## Merge controller (orchestrator/controller/merge) - Takes a `pusher.Pusher` dependency, loads each request in `batch.Contains` to collect changes in batch order, calls Push, and classifies the outcome inline with three explicit cases (success, conflict, generic error) — no helper, no error wrapping at this layer (retryability classification will be added by separate infra). - Version arithmetic stays in the controller per the optimistic locking contract: newVersion is computed before UpdateState and assigned to the in-memory entity only on success. ## Tests - Real-git fixture for the git Pusher: bare remote + checkout + author clone, with a `pre-receive` race hook that on its Nth call moves refs/heads/main to the Nth pre-staged SHA via update-ref (with GIT_QUARANTINE_PATH/GIT_OBJECT_DIRECTORY/ GIT_ALTERNATE_OBJECT_DIRECTORIES unset to bypass git's pre-receive quarantine) and exits 1, driving the retry loop with real git mechanics. Covers single/stacked URIs, already-existed, mixed-partial, conflict, recovery-after-conflict, reset-between-calls, retry-on-contention, and giveup-after-cap. - Merge controller tests rewritten with the new pusher mock and cover successful merge, multi-change ordering, conflict -> Failed, infra-error returns, terminal-batch idempotency, and store/publish failures. ## Wiring - `example/server/orchestrator/main.go`: `newPusher()` reads `PUSHER_CHECKOUT_PATH` (required), `PUSHER_REMOTE` (default "origin"), `PUSHER_TARGET` (default "main"). - Makefile `mocks` target adds `./extension/pusher/...`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
behinddwalls
approved these changes
May 7, 2026
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.
Summary
extension/pusherinterface with all-or-nothingPushsemantics, per-change outcomes (committed vs already-existed), and anErrConflictsentinel for user-caused failures.Params.MaxPushAttempts(default 10).Pusher.Push, transitions the batch toSucceeded/Failed, and nacks transient infra errors. Three outcome cases are inlined (success,ErrConflict→ Failed, generic error → return) — retryability classification will live in separate infra.example/server/orchestratorwires a git pusher fromPUSHER_CHECKOUT_PATH/PUSHER_REMOTE/PUSHER_TARGETenv vars.Test plan
make test— 30 unit tests pass, including new pusher and rewritten merge controller suitesGIT_QUARANTINE_PATH/GIT_OBJECT_DIRECTORY/GIT_ALTERNATE_OBJECT_DIRECTORIESto mutate refs from inside pre-receive)make fmt,make gazelle,make tidy— tree clean🤖 Generated with Claude Code