diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go index 428e57a4a..2bfb10adf 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go @@ -363,7 +363,8 @@ func (r *runtime) answer(m rpcMessage) { for _, p := range pulls { if p.Owner+"/"+p.Repo == repository && p.Number == int(number) { return gitseat.PullAnswer{Pull: &gitseat.Pull{Repository: repository, Number: int64(p.Number), Base: p.Base, - Head: p.HeadSHA, State: gitseat.PullStateOpen}}, nil + Head: p.HeadSHA, State: gitseat.PullStateOpen}, Merging: &gitseat.Merging{Mergeable: true, Protected: true, + Required: []string{"mesh/check"}, Writes: true}}, nil } } return gitseat.PullAnswer{Error: "no such pull request"}, nil @@ -371,16 +372,6 @@ func (r *runtime) answer(m rpcMessage) { // Green: mesh/check a success at the head asked, as the seat answers. commit, _ := a.Body["commit"].(string) return gitseat.StatusesAnswer{Commit: commit, Statuses: []gitseat.Status{{Context: "mesh/check", State: "success"}}}, nil - case mergeStateTool: - // Green: mesh/check 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/check", "state": "success"}}, - "merge": map[string]any{"branch": "main", "protected": true, "required": []string{"mesh/check"}, - "blocking": []any{}, "mergeable": true, "says": "main requires mesh/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-gate/cmd/mesh-delivery-gate/gitseat_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/gitseat_test.go index 328b8de0c..12b94b45b 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/gitseat_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/gitseat_test.go @@ -20,10 +20,10 @@ func TestTheGateReadsAndHearsTheForgeThroughTheSeatGit(t *testing.T) { "seat:git.open-pulls": gitseat.OpenPullsAnswer{Pulls: []gitseat.OpenPull{{Pull: pull, Required: []string{"mesh/check"}, Checks: map[string]gitseat.Status{"mesh/check": {Context: "mesh/check", State: "pending"}}}}}, "seat:git.contains": gitseat.ContainsAnswer{Contains: true}, - "seat:git.pull": gitseat.PullAnswer{Pull: &pull}, + "seat:git.pull": gitseat.PullAnswer{Pull: &pull, Merging: &gitseat.Merging{Mergeable: true, Protected: true, + Required: []string{"mesh/check"}, Writes: true}}, "seat:git.statuses": gitseat.StatusesAnswer{Commit: head, Combined: "pending", Statuses: []gitseat.Status{{Context: "mesh/check", State: "pending", By: "mesh"}}}, - mergeStateTool: map[string]any{"commit": head, "merge": map[string]any{"branch": "main", "mergeable": true}}, } for _, c := range []struct { name string @@ -48,10 +48,23 @@ func TestTheGateReadsAndHearsTheForgeThroughTheSeatGit(t *testing.T) { if err != nil { return nil, err } - return []any{c.Commit, *c.Pull, c.Statuses, *c.Merge.Mergeable}, nil + p := *c.Pull + conflict := *p.Mergeable + p.Mergeable = nil + return []any{c.Commit, p, c.Statuses, conflict, *c.Merge.Mergeable, c.Merge.Blocking[0].Context + " " + + c.Merge.Blocking[0].State, c.Merge.Says, *c.Merge.Writes}, nil }, want: []any{head, ForgePull{Number: 7, State: "closed", Merged: true, Base: "main", Head: "feat/x", HeadSHA: head, - MergeCommit: pull.MergeCommit, HTMLURL: pull.URL}, []ForgeStatus{{Context: "mesh/check", State: "pending", Creator: "mesh"}}, true}, - asked: []string{"seat:git.pull", "seat:git.statuses", mergeStateTool}}, + MergeCommit: pull.MergeCommit, HTMLURL: pull.URL}, []ForgeStatus{{Context: "mesh/check", State: "pending", Creator: "mesh"}}, + true, false, "mesh/check pending", "main requires mesh/check", true}, + asked: []string{"seat:git.pull", "seat:git.statuses"}}, + // What the forge would merge now comes from the pull answer's merging alone: none for a commit no pull request names. + {name: "no merge state for a commit alone", read: func(f seatForge) (any, error) { + c, err := f.Statuses("novox", "app", 0, head, "main") + if err != nil { + return nil, err + } + return []any{c.Merge.Mergeable == nil, c.Merge.Writes == nil}, nil + }, want: []any{true, true}, asked: []string{"seat:git.statuses"}}, {name: "a pull request not read", failed: "seat:git.pull", read: func(f seatForge) (any, error) { return f.Statuses("novox", "app", 7, "", "") }, asked: []string{"seat:git.pull"}}, diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go index 9d0edb064..28891258d 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go @@ -115,7 +115,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) { // merge state through the one statuses tool the seat does not carry yet (seatForge.mergeState); the forge's writes are the // bridge's (ADR 0437 §8). It uses operator-channel only to say its conditions (ADR 0437 §9, issue 1275): the teller asks // (ADR 0437 §6). - want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", mergeStateTool, "seat:mesh-hand-acts.hand-act"} + want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", "seat:mesh-hand-acts.hand-act"} if !reflect.DeepEqual(m.Uses, []string{"operator-channel"}) { t.Fatalf("uses %v: operator-channel, for its conditions alone", m.Uses) } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ports.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ports.go index 5d57538e4..3f103955b 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ports.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ports.go @@ -163,11 +163,13 @@ func gitAsk[A any](f seatForge, verb string, in any, errOf func(*A) string) (*A, } // Statuses reads a pull request by its number through `pull` (its head when no commit is named) and the commit's statuses -// through `statuses`. What the forge would merge now (the base's protection, its conflict word, whether the merging -// account writes) is read by mergeState. +// through `statuses`. What the forge would merge now (the base's protection and its required contexts, the forge's +// conflict word, whether the merging account writes) is the pull answer's `merging` (mergeState); a commit no pull +// request names has none. func (f seatForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) { repository := owner + "/" + repo c := &ForgeChecks{Commit: commit} + var merging *gitseat.Merging if number > 0 { a, err := gitAsk(f, gitseat.VerbPull, gitseat.PullInput{Repository: repository, Number: int64(number)}, func(a *gitseat.PullAnswer) string { return a.Error }) @@ -177,7 +179,7 @@ func (f seatForge) Statuses(owner, repo string, number int, commit, base string) if a.Pull == nil { return nil, fmt.Errorf("the seat git's pull named no pull request for %s#%d", repository, number) } - c.Pull = forgePullOf(*a.Pull) + c.Pull, merging = forgePullOf(*a.Pull), a.Merging if c.Commit == "" { c.Commit = a.Pull.Head } @@ -197,50 +199,58 @@ func (f seatForge) Statuses(owner, repo string, number int, commit, base string) for _, st := range s.Statuses { c.Statuses = append(c.Statuses, forgeStatusOf(st)) } - merge, mergeable, err := f.mergeState(owner, repo, number, c.Commit, base) - if err != nil { - return nil, err - } - c.Merge = merge - if c.Pull != nil { - c.Pull.Mergeable = mergeable + c.Merge = mergeState(repository, base, merging, c.Statuses) + if c.Pull != nil && merging != nil { + mergeable := merging.Mergeable + c.Pull.Mergeable = &mergeable } return c, nil } -// mergeState is what the forge would merge now: the base's protection and its required statuses, the forge's own word on -// a conflict, and whether the merging account writes the repository. The seat `git` names none of them yet (dev-G-record's -// finding on W11.18): until it does, they are read through the forge holder's statuses tool, the one read left there. -func (f seatForge) mergeState(owner, repo string, number int, commit, base string) (ForgeMerge, *bool, error) { - args := map[string]any{"owner": owner, "repo": repo, "sha": commit} - if number > 0 { - args["number"] = number +// mergeState is what the forge would merge now, from the seat git's `merging` (issues 1275, 1945; ADR 0441 §1): the base's +// protection and its required contexts, each required context not a success at the commit blocking, and whether the +// merging account writes. Without `merging` (a commit no pull request names, or a pull request merged or closed) whether +// it merges is not known. +func mergeState(repository, base string, m *gitseat.Merging, statuses []ForgeStatus) ForgeMerge { + out := ForgeMerge{Branch: base, Required: []string{}} + if m == nil { + out.Says = "the seat git says what merges only for an open pull request" + return out } - if base != "" { - args["base"] = base + out.Protected, out.Required = m.Protected, append([]string{}, m.Required...) + newest := map[string]string{} + for _, s := range statuses { + if _, seen := newest[s.Context]; !seen { + newest[s.Context] = s.State + } } - raw, err := f.ask(mergeStateTool, args) - if err != nil { - return ForgeMerge{}, nil, err + for _, context := range out.Required { + if state := newest[context]; state != "success" { + if state == "" { + state = "missing" + } + out.Blocking = append(out.Blocking, struct { + Context string `json:"context"` + State string `json:"state"` + }{context, state}) + } } - answer, output, ok := answerOf(raw) - if !ok { - return ForgeMerge{}, nil, fmt.Errorf("the forge refused its merge state: %s", lastLine(output)) + mergeable := len(out.Blocking) == 0 + out.Mergeable = &mergeable + switch { + case len(out.Required) == 0: + out.Says = base + " requires no status" + default: + out.Says = base + " requires " + strings.Join(out.Required, ", ") } - var c ForgeChecks - if err := json.Unmarshal(answer, &c); err != nil || c.Commit == "" { - return ForgeMerge{}, nil, fmt.Errorf("the forge's merge state is not readable: %s", clip(string(answer), 200)) + writes := m.Writes + out.Writes = &writes + if !writes { + out.CannotWrite = "the forge's merging account may not write " + repository + ": its statuses and its merge are refused" } - var mergeable *bool - if c.Pull != nil { - mergeable = c.Pull.Mergeable - } - return c.Merge, mergeable, nil + return out } -// mergeStateTool is the one forge read the seat `git` does not carry yet (see mergeState). -const mergeStateTool = "gitea.gitea_commit_statuses" - // OpenPulls reads every open pull request through `open-pulls`. func (f seatForge) OpenPulls() ([]OpenPull, error) { a, err := gitAsk(f, gitseat.VerbOpenPulls, gitseat.OpenPullsInput{}, func(a *gitseat.OpenPullsAnswer) string { return a.Error }) diff --git a/modules/mesh-delivery-gate/module.json b/modules/mesh-delivery-gate/module.json index b10a9275a..d766a5f54 100644 --- a/modules/mesh-delivery-gate/module.json +++ b/modules/mesh-delivery-gate/module.json @@ -257,7 +257,6 @@ "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", - "gitea.gitea_commit_statuses", "seat:mesh-hand-acts.hand-act" ], "state": [