Merge a group whole or not at all, and say loudly when it cannot (issues 1275, 1276) #375

Merged
mesh-tools merged 3 commits from fix/1275-group-merges-whole-or-not into main 2026-10-19 18:45:31 +00:00
3 changed files with 48 additions and 1 deletions
Showing only changes of commit 301cd2287f - Show all commits
@@ -306,6 +306,17 @@ func (r *runtime) answer(m rpcMessage) {
return toolAnswer(map[string]any{"asked": "check-1"}), nil
case "gitea.gitea_open_pulls":
return toolAnswer(map[string]any{"pulls": pulls}), nil
case "gitea.gitea_commit_statuses":
// Green: both merge checks a success at the head asked, as the gitea module answers.
sha, _ := a.Body["sha"].(string)
if sha == "" {
sha = "0000000"
}
return toolAnswer(map[string]any{"commit": sha, "statuses": []any{
map[string]any{"context": "mesh/merge-gate", "state": "success"},
map[string]any{"context": "mesh/repo-check", "state": "success"}},
"merge": map[string]any{"branch": "main", "protected": true, "required": []string{"mesh/merge-gate", "mesh/repo-check"},
"blocking": []any{}, "mergeable": true, "says": "main requires mesh/merge-gate, mesh/repo-check"}}), nil
case "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status", "gitea.gitea_merge_pull_request",
"gitea.gitea_add_comment":
return toolAnswer(map[string]any{}), nil
@@ -193,8 +193,10 @@ func (h *Holder) groupLines(now time.Time) []StalledLine {
merged++
}
}
heldOn := h.deliveries[holdMember(g)]
switch {
case g.Hold != nil && g.Hold.Kind == "blocked" && !running:
// A held member the merge tool refused is said by readyLines already: one line for an id, not two.
case g.Hold != nil && g.Hold.Kind == "blocked" && !running && !(heldOn != nil && heldOn.Ready != nil && heldOn.Ready.RefusedSince != nil):
out = append(out, StalledLine{ID: g.Hold.Member, Number: numberOf(h.deliveries[g.Hold.Member]), State: first.State,
For: now.Sub(g.Hold.Since).Round(time.Second).String(), Bound: "0s", Severity: "urgent",
H2: "none: the required status turns success, which asks the merge again — or the operator's decision",
@@ -218,3 +220,10 @@ func numberOf(d *Delivery) int {
}
return d.Number
}
func holdMember(g *Group) string {
if g.Hold == nil {
return ""
}
return g.Hold.Member
}
@@ -221,3 +221,30 @@ func TestAClosedGroupsMemberRefusedForItsOwnMissingStatusAsksItsOwnCheck(t *test
t.Fatalf("own checks %v, composed checks %v; wanted the member's own check once and no composed check", own, composed)
}
}
// 1275/AC3 (review round 1): a member that is both refused and held for a required status is said by one stalled line,
// not two with the same id.
func TestAMemberBothRefusedAndHeldForAStatusIsOneStalledLine(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
setChecks(w, gCat, gHk, "success", "") // now its status is missing too; the next pass is held
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "success"})
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if g := w.h.groups["feat/g"]; g.Hold == nil {
t.Fatalf("the group was not held for the missing status")
}
n := 0
for _, l := range w.h.Stalled() {
if l.ID == IDOf(gCat, gHk) {
n++
}
}
if n != 1 {
t.Fatalf("%d stalled lines for one member: %+v", n, w.h.Stalled())
}
}