Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/merge_orchestration/baton_emitter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ defmodule Hypatia.MergeOrchestration.BatonEmitter do
defp default_submit(spec), do: apply(Bag.Mesh, :submit_planned, [spec])

defp gh_flag(:squash), do: "--squash"
defp gh_flag(:rebase), do: "--rebase"
# :rebase is never emitted -- a stale decision carrying it squashes (see
# Strategist.decide_method/1); rebase is disabled estate-wide.
defp gh_flag(:merge_commit), do: "--merge"
defp gh_flag(_), do: "--squash"

Expand Down
6 changes: 5 additions & 1 deletion lib/merge_orchestration/strategist.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,15 @@ defmodule Hypatia.MergeOrchestration.Strategist do
defp min_safety(a, b), do: if(@order[a] <= @order[b], do: a, else: b)

# --- Method axis: repo policy default ⊕ commit hygiene (never confidence) ---
# Never :rebase: rebase-merge replays commits UNSIGNED (119 of 286 unsigned
# estate default-branch commits were such replays) and breaks
# required_signatures; the estate disables it repo-side (owner ruling
# 2026-09-30). An atomic-green series is squashed like any other change;
# only a proof keeps its series, as a (GitHub-signed) merge commit.
defp decide_method(ctx) do
cond do
ctx.change_class in [:chore, :bump] -> :squash
ctx.change_class == :proof -> :merge_commit
Map.get(ctx, :commits_atomic_green, false) -> :rebase
true -> :squash
end
end
Expand Down
3 changes: 2 additions & 1 deletion test/merge_orchestration/baton_emitter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ defmodule Hypatia.MergeOrchestration.BatonEmitterTest do
end

test "method maps to the gh flag; aggressive pools raise the planner risk" do
assert BatonEmitter.to_spec(dec(%{method: :rebase})).command |> List.last() == "--rebase"
# rebase replays commits unsigned: a stale :rebase decision must squash
assert BatonEmitter.to_spec(dec(%{method: :rebase})).command |> List.last() == "--squash"
assert BatonEmitter.to_spec(dec(%{method: :merge_commit})).command |> List.last() == "--merge"
assert BatonEmitter.to_spec(dec(%{pool: :p3})).risk == :high
assert BatonEmitter.to_spec(dec(%{pool: :mass_squash})).risk == :high
Expand Down
7 changes: 7 additions & 0 deletions test/merge_orchestration/strategist_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ defmodule Hypatia.MergeOrchestration.StrategistTest do
assert Strategist.decide(ctx(%{pool: :p0})).safety == :flag
end

test "an atomic-green commit series squashes: rebase is never chosen" do
# a non-chore class, so the method cond reaches its commit-hygiene clauses
# (the default :chore squashes on the first clause and would prove nothing)
d = Strategist.decide(ctx(%{change_class: :security, commits_atomic_green: true}))
assert d.method == :squash
end

test "method is set by class, not confidence: a low-confidence chore still squashes" do
d =
Strategist.decide(ctx(%{attestations: [%{bot: "ci", verdict: :approve, confidence: 0.10}]}))
Expand Down
Loading