feat(runway): git merger PROMOTE - #462
Draft
behinddwalls wants to merge 1 commit into
Draft
Conversation
## Summary ### Why? `PROMOTE` is the last strategy in the wire contract without an apply path. It is also the one that does not fit the shared machinery: the transforming strategies build new commits locally and push `HEAD:target`, while PROMOTE advances the target to a commit that already exists, unchanged. ### What? Adds the `promote` path, dispatched directly from `process` rather than through `applyTransforming`. **Fast-forward only.** After resetting to the remote tip, promote classifies the named commit three ways. Already the tip, or contained in it — idempotent success, no push. A strict descendant of the tip — a genuine fast-forward, pushed as `<sha>:refs/heads/<target>`. Anything else has diverged and is a terminal `ErrConflict`; PROMOTE never creates a commit to reconcile the two. **Exclusivity.** `resolveAndValidate` rejects a PROMOTE that is not the entire request — one step, one change, one URI — as `ErrInvalidRequest`. Two reasons, both structural: a pre-existing commit cannot descend from commits an earlier transforming step just produced, and the push targets an exact SHA rather than the locally-built HEAD, so there is nothing for a preceding step to contribute. **Contention.** The same bounded retry as the transforming path, but the loop re-runs the classification rather than the apply: if the push is rejected the tip may have moved, and the commit that was a fast-forward a moment ago may now be contained (success) or divergent (conflict). The push is a single atomic ref update, so PROMOTE needs no separate atomicity argument — there is no window in which the remote holds a partial result. A dry-run check performs the identical classification and returns without pushing, reporting no output. With this the merger implements every strategy in the contract; `isConcreteStrategy` now admits all four. ## Test Plan ✅ `bazel test //runway/...` — 6/6 pass (git suite 32.1s) New cases: fast-forward promote, promote of a commit already contained in the tip, divergent promote rejected as a conflict, both dry-run classifications, and the two composition rules (PROMOTE with a second step, PROMOTE with a second URI) rejected as invalid requests.
This was referenced Jul 29, 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
Why?
PROMOTEis the last strategy in the wire contract without an apply path. It is also the one that does not fit the shared machinery: the transforming strategies build new commits locally and pushHEAD:target, while PROMOTE advances the target to a commit that already exists, unchanged.What?
Adds the
promotepath, dispatched directly fromprocessrather than throughapplyTransforming.Fast-forward only. After resetting to the remote tip, promote classifies the named commit three ways. Already the tip, or contained in it — idempotent success, no push. A strict descendant of the tip — a genuine fast-forward, pushed as
<sha>:refs/heads/<target>. Anything else has diverged and is a terminalErrConflict; PROMOTE never creates a commit to reconcile the two.Exclusivity.
resolveAndValidaterejects a PROMOTE that is not the entire request — one step, one change, one URI — asErrInvalidRequest. Two reasons, both structural: a pre-existing commit cannot descend from commits an earlier transforming step just produced, and the push targets an exact SHA rather than the locally-built HEAD, so there is nothing for a preceding step to contribute.Contention. The same bounded retry as the transforming path, but the loop re-runs the classification rather than the apply: if the push is rejected the tip may have moved, and the commit that was a fast-forward a moment ago may now be contained (success) or divergent (conflict).
The push is a single atomic ref update, so PROMOTE needs no separate atomicity argument — there is no window in which the remote holds a partial result. A dry-run check performs the identical classification and returns without pushing, reporting no output.
With this the merger implements every strategy in the contract;
isConcreteStrategynow admits all four.Test Plan
✅
bazel test //runway/...— 6/6 pass (git suite 32.1s)New cases: fast-forward promote, promote of a commit already contained in the tip, divergent promote rejected as a conflict, both dry-run classifications, and the two composition rules (PROMOTE with a second step, PROMOTE with a second URI) rejected as invalid requests.
Stack