Skip to content

feat(runway): git merger PROMOTE - #462

Draft
behinddwalls wants to merge 1 commit into
preetam/runway-git-merger-squash-mergefrom
preetam/runway-git-merger-promote
Draft

feat(runway): git merger PROMOTE#462
behinddwalls wants to merge 1 commit into
preetam/runway-git-merger-squash-mergefrom
preetam/runway-git-merger-promote

Conversation

@behinddwalls

@behinddwalls behinddwalls commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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.

Stack

  1. chore(submitqueue): remove dead pusher extension #457
  2. feat(runway): classify invalid merge requests as terminal #458
  3. feat(runway): dlq reconciler for merge topics #459
  4. feat(runway): git-backed merger with REBASE #460
  5. feat(runway): git merger SQUASH_REBASE and MERGE #461
  6. @ feat(runway): git merger PROMOTE #462
  7. feat(runway): wire the git merger into the server #463

## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant