From 767928eb944df9dbc3e612e684fad80f49b38cd6 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 11:29:31 +0100 Subject: [PATCH] Forge bridge reads gitea_api's {"result": body} wrapper in one place (issue 1966) Both ForgeMade and Tags took the tool's wrapper for the pull request / the tag list, so every forge-made merge read "#0 is not merged" and bridge_tags failed, leaving builds unnumbered. Quick fix: unwrap "result" in forgeAPI (the one Go reader of gitea_api). Thorough fix (chosen, the kind was met twice and hid behind a drifted fake): bridge.API answers what the tool answers, forgeBody is the one reader of that shape and refuses an answer without a result, naming it; ForgeMade refuses number 0 as "no pull request"; the unit fakes answer the tool's real wrapped shape, with rows for the bare body. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../main_test.go | 10 ++++++++- .../internal/bridge/bridge_test.go | 7 ++++-- .../internal/bridge/forgemade.go | 8 ++++++- .../internal/bridge/tags.go | 22 +++++++++++++++++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go index 98bbd789..0be6d7ad 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go @@ -184,6 +184,8 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { api: forgeAnswers(t, "/repos/novox/a/pulls/9", true, "fix/x", h2, c2, "dev"), fail: "not the trunk"}, {name: "a forge pull request with no head branch", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9, api: forgeAnswers(t, "/repos/novox/a/pulls/9", true, "refs/pull/9/head", h2, c2, "main"), fail: "no head branch"}, + {name: "a forge answer that is the bare body, no wrapper", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9, + api: func(string) (json.RawMessage, error) { return json.RawMessage(`{"number":9,"merged":true}`), nil }, fail: `{"result"`}, {name: "a forge that does not answer", raws: map[string][]byte{}, repository: "novox/a", merge: c2, api: func(string) (json.RawMessage, error) { return nil, errors.New("the forge is down") }, fail: "the forge is down"}, } { @@ -221,6 +223,12 @@ func forgeAnswers(t *testing.T, path string, merged bool, branch, head, merge, b if p != path { return nil, errors.New("the forge has no " + p) } - return body, nil + return wrapped(body), nil } } + +// wrapped is gitea_api's answer for a body: {"result": } (mesh-catalog modules/gitea/tools/index.ts). +func wrapped(body []byte) json.RawMessage { + b, _ := json.Marshal(map[string]json.RawMessage{"result": body}) + return b +} diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go index 9da7668e..6744636b 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go @@ -428,10 +428,10 @@ func TestBridgeTagsAnswersAPackagesTagsByPrefixPageByPage(t *testing.T) { // DLV var tags []map[string]string _ = json.Unmarshal(full, &tags) tags = append(tags, map[string]string{"name": "go/v0.1.13"}, map[string]string{"name": "v0.0.1"}) - b, _ := json.Marshal(tags) + b, _ := json.Marshal(map[string]any{"result": tags}) return b, nil } - return json.RawMessage(`[{"name":"go/v0.1.2"}]`), nil + return json.RawMessage(`{"result":[{"name":"go/v0.1.2"}]}`), nil } a, err := Tags(api, "novox/mesh-sdk", "go/v") if err != nil || !slicesEqual(a.Tags, []string{"go/v0.1.13", "go/v0.1.2"}) || a.More || len(asked) != 2 || @@ -441,6 +441,9 @@ func TestBridgeTagsAnswersAPackagesTagsByPrefixPageByPage(t *testing.T) { // DLV if _, err := Tags(func(string) (json.RawMessage, error) { return nil, errors.New("down") }, "novox/mesh-sdk", ""); err == nil || !strings.Contains(err.Error(), "page 1") { t.Errorf("a forge not answering = %v, want an error naming the page", err) } + if _, err := Tags(func(string) (json.RawMessage, error) { return json.RawMessage(`[{"name":"go/v0.1.2"}]`), nil }, "novox/mesh-sdk", ""); err == nil || !strings.Contains(err.Error(), `{"result"`) { + t.Errorf("a bare list with no wrapper = %v, want the shape named", err) + } if _, err := Tags(api, "mesh-sdk", ""); err == nil { t.Error("a repository with no owner answered") } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/forgemade.go b/modules/mesh-delivery-forge-bridge/internal/bridge/forgemade.go index d09e2929..811504cc 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/forgemade.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/forgemade.go @@ -29,6 +29,10 @@ func ForgeMade(api API, owner, repo string, number int, merge string) (ForgePull if err != nil { return ForgePull{}, fmt.Errorf("the forge did not answer %s: %w", path, err) } + body, err := forgeBody(raw) + if err != nil { + return ForgePull{}, fmt.Errorf("the forge's answer for %s: %w", path, err) + } var p struct { Number int `json:"number"` Merged bool `json:"merged"` @@ -41,10 +45,12 @@ func ForgeMade(api API, owner, repo string, number int, merge string) (ForgePull Ref string `json:"ref"` } `json:"base"` } - if err := json.Unmarshal(raw, &p); err != nil { + if err := json.Unmarshal(body, &p); err != nil { return ForgePull{}, fmt.Errorf("the forge's answer for %s is no pull request: %w", path, err) } switch { + case p.Number == 0: + return ForgePull{}, fmt.Errorf("the forge's answer for %s is no pull request (no number)", path) case !p.Merged: return ForgePull{}, fmt.Errorf("the forge says the pull request %s/%s#%d is not merged", owner, repo, p.Number) case p.MergeCommitSHA != merge: diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go b/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go index 4ae8c146..b4a942ec 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go @@ -18,9 +18,23 @@ const ( TagsMaxPages = 40 ) -// API asks the forge one GET of its API (the forge module's gitea_api), answering the body. +// API asks the forge one GET of its API (the forge module's gitea_api), answering what the tool answers: the body +// wrapped as {"result": }. Every reader of an API answer takes the body through forgeBody, the one place that +// knows that shape (issue 1966: two readers took the wrapper for the body). type API func(path string) (json.RawMessage, error) +// forgeBody is the body of the forge's answer: the tool's "result". An answer without one is refused, naming the shape, +// never read as an empty body. +func forgeBody(raw json.RawMessage) (json.RawMessage, error) { + var w struct { + Result json.RawMessage `json:"result"` + } + if err := json.Unmarshal(raw, &w); err != nil || len(w.Result) == 0 || string(w.Result) == "null" { + return nil, fmt.Errorf(`the forge module's answer is not {"result": }`) + } + return w.Result, nil +} + // TagsAnswer is what bridge_tags answers. type TagsAnswer struct { Repository string `json:"repository"` @@ -48,10 +62,14 @@ func Tags(api API, repository, prefix string) (TagsAnswer, error) { if err != nil { return TagsAnswer{}, fmt.Errorf("the forge did not answer the tags of %s (page %d): %w", out.Repository, page, err) } + body, err := forgeBody(raw) + if err != nil { + return TagsAnswer{}, fmt.Errorf("the forge's tags of %s (page %d): %w", out.Repository, page, err) + } var tags []struct { Name string `json:"name"` } - if err := json.Unmarshal(raw, &tags); err != nil { + if err := json.Unmarshal(body, &tags); err != nil { return TagsAnswer{}, fmt.Errorf("the forge's tags of %s (page %d) are no list of tags: %w", out.Repository, page, err) } for _, t := range tags {