From f1e9757ab9f13859f9c9a08caf6660dd02f22d7f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:26:49 +0100 Subject: [PATCH] fix(merge-orchestration): never choose rebase; a stale :rebase decision squashes Rebase-merge replays commits UNSIGNED: 119 of 286 unsigned estate default-branch commits were such replays (patch-id twins), so it breaks required_signatures. The owner ruled on 2026-09-30 to disable rebase repo-side while keeping merge commits for proof PRs. - Strategist.decide_method: drop the commits_atomic_green -> :rebase clause. An atomic-green series squashes, and a proof still gets :merge_commit. - BatonEmitter: drop gh_flag(:rebase). A manifest written before this change falls through to --squash instead of emitting a flag that the repo now refuses. - Tests: a non-chore atomic-green context must squash, and a :rebase decision must emit --squash. Both were checked by mutants: restoring either removed line turns its test red. The first draft of the strategist test used the default :chore class and was vacuous. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136eszqrQ53Kj7aBH1D4rXK --- lib/merge_orchestration/baton_emitter.ex | 3 ++- lib/merge_orchestration/strategist.ex | 6 +++++- test/merge_orchestration/baton_emitter_test.exs | 3 ++- test/merge_orchestration/strategist_test.exs | 7 +++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/merge_orchestration/baton_emitter.ex b/lib/merge_orchestration/baton_emitter.ex index 5578a1de..0e76db0a 100644 --- a/lib/merge_orchestration/baton_emitter.ex +++ b/lib/merge_orchestration/baton_emitter.ex @@ -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" diff --git a/lib/merge_orchestration/strategist.ex b/lib/merge_orchestration/strategist.ex index 00c85d53..7898b86c 100644 --- a/lib/merge_orchestration/strategist.ex +++ b/lib/merge_orchestration/strategist.ex @@ -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 diff --git a/test/merge_orchestration/baton_emitter_test.exs b/test/merge_orchestration/baton_emitter_test.exs index 17633be7..a32d06ac 100644 --- a/test/merge_orchestration/baton_emitter_test.exs +++ b/test/merge_orchestration/baton_emitter_test.exs @@ -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 diff --git a/test/merge_orchestration/strategist_test.exs b/test/merge_orchestration/strategist_test.exs index 8683be1b..8fa378f3 100644 --- a/test/merge_orchestration/strategist_test.exs +++ b/test/merge_orchestration/strategist_test.exs @@ -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}]}))