diff --git a/modules/mesh-delivery/cmd/mesh-delivery/bus_test.go b/modules/mesh-delivery/cmd/mesh-delivery/bus_test.go index a9562208..363da871 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/bus_test.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/bus_test.go @@ -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 diff --git a/modules/mesh-delivery/cmd/mesh-delivery/group_merge.go b/modules/mesh-delivery/cmd/mesh-delivery/group_merge.go index 6ebc8b31..049d8179 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/group_merge.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/group_merge.go @@ -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 +} diff --git a/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go b/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go index 315b5337..c7b08cb0 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go @@ -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()) + } +}