From 58f1e8691d50e235dda01274ce52460f07fa9d90 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Mon, 19 Oct 2026 15:57:07 +0530 Subject: [PATCH] Populate funding links and maintainers in direct-registry lookups --- client.go | 3 +- go.mod | 4 +- go.sum | 8 ++-- registries.go | 21 +++++++++ registries_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index bc0b98a..b111216 100644 --- a/client.go +++ b/client.go @@ -44,7 +44,8 @@ type PackageInfo struct { // Security advisories (ecosyste.ms only) Advisories []Advisory - // Funding and maintainers (ecosyste.ms only) + // Funding and maintainers (ecosyste.ms, or direct registries that + // expose them in package metadata) FundingLinks []string Maintainers []Maintainer } diff --git a/go.mod b/go.mod index fd14725..65f7616 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.26.7 require ( github.com/ecosyste-ms/ecosystems-go v0.5.0 github.com/git-pkgs/purl v0.1.21 - github.com/git-pkgs/registries v0.9.3 + github.com/git-pkgs/registries v0.10.1-0.20261019065955-4d31fcfe8bf0 github.com/git-pkgs/spdx v0.3.3 github.com/git-pkgs/vers v0.7.2 github.com/git-pkgs/vulns v0.2.4 @@ -16,7 +16,7 @@ require ( require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/git-pkgs/pom v0.1.7 // indirect + github.com/git-pkgs/pom v0.1.8 // indirect github.com/github/go-spdx/v2 v2.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/oapi-codegen/runtime v1.7.0 // indirect diff --git a/go.sum b/go.sum index b6a1b99..d07478d 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ecosyste-ms/ecosystems-go v0.5.0 h1:qZqgar3Do9RrtW327OXlsrGKiRvNdeX8CmXb1aX4Njo= github.com/ecosyste-ms/ecosystems-go v0.5.0/go.mod h1:IhsYBZfxkofxya0O1O2DlhDt281utWJ6VQO0mPVb99I= -github.com/git-pkgs/pom v0.1.7 h1:4yKdtw6eyShtjul6bcZdyz7yLQ+jdrYeYkKbskDGi4c= -github.com/git-pkgs/pom v0.1.7/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk= +github.com/git-pkgs/pom v0.1.8 h1:mXCuWw6XkrKKZb6IurRZyIznYLPhV5AYg5ZO38Uj/l8= +github.com/git-pkgs/pom v0.1.8/go.mod h1:sQBeYRzjCUn77fcABPn6I/a1aBHFxm55jTmkNxYtGtw= github.com/git-pkgs/purl v0.1.21 h1:PDZUX43lxIiXPn2eHcsNiiiwQo+HajntZ/4TwSvCGlw= github.com/git-pkgs/purl v0.1.21/go.mod h1:4dOmris48ehjiKmWBqSmMHguUQht48BLeXW+hQUnuj0= -github.com/git-pkgs/registries v0.9.3 h1:nXIP//6kVqLKqYXOWhEMoWUkJPDAZYVLRUYfHz3jM2Y= -github.com/git-pkgs/registries v0.9.3/go.mod h1:BpXlfRCfu4S5bTzm7u7igppVu94mXgll/PENe0AjKmI= +github.com/git-pkgs/registries v0.10.1-0.20261019065955-4d31fcfe8bf0 h1:I/2ic+feovx7MXmH60arNUGb0sLtQ68YYnii73m2xyE= +github.com/git-pkgs/registries v0.10.1-0.20261019065955-4d31fcfe8bf0/go.mod h1:bKxgdy/PwnzQXL2FnOktmSQHTKk6B85MA4VtvvYItPc= github.com/git-pkgs/spdx v0.3.3 h1:xXlUoZzjDSkwxC6mWHfaW+kc8a0+LH6d+gGHuZWftqI= github.com/git-pkgs/spdx v0.3.3/go.mod h1:n9rAicgw+Wqtfursju6oKaAL6cUCTiIS926mb1KWoKU= github.com/git-pkgs/vers v0.7.2 h1:Ctex1O+Ai/FONmyDaw37EolBXg0og6UdxbCjjYbeRHs= diff --git a/registries.go b/registries.go index 05ed135..4575431 100644 --- a/registries.go +++ b/registries.go @@ -59,6 +59,8 @@ func (c *RegistriesClient) BulkLookup(ctx context.Context, purls []string) (map[ Homepage: pkg.Homepage, Repository: pkg.Repository, RegistryURL: extractRegistryURL(purlStr, ecosystem), + FundingLinks: pkg.FundingLinks, + Maintainers: maintainersFromRegistry(pkg.Maintainers), Source: "registries", } @@ -112,6 +114,25 @@ func (c *RegistriesClient) GetVersion(ctx context.Context, purlStr string) (*Ver return &info, nil } +// maintainersFromRegistry converts maintainers returned by a registry's +// package endpoint. It returns nil when the registry supplied none. +func maintainersFromRegistry(maintainers []registries.Maintainer) []Maintainer { + if len(maintainers) == 0 { + return nil + } + result := make([]Maintainer, 0, len(maintainers)) + for _, m := range maintainers { + result = append(result, Maintainer{ + Login: m.Login, + Name: m.Name, + Email: m.Email, + URL: m.URL, + Role: m.Role, + }) + } + return result +} + func versionInfoFromRegistry(v registries.Version) VersionInfo { return VersionInfo{ Number: v.Number, diff --git a/registries_test.go b/registries_test.go index 3b3dde7..f791741 100644 --- a/registries_test.go +++ b/registries_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "reflect" "sync/atomic" "testing" "time" @@ -90,3 +91,106 @@ func TestVersionInfoFromRegistryIncludesStatusAndMetadata(t *testing.T) { t.Fatalf("yanked status metadata = %+v", yanked) } } + +func TestRegistriesClientBulkLookupCopiesFundingAndMaintainers(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/funded": + _, _ = w.Write([]byte(`{ + "_id": "funded", + "dist-tags": {"latest": "1.0.0"}, + "versions": { + "1.0.0": { + "funding": [ + "https://github.com/sponsors/funded", + {"type": "opencollective", "url": "https://opencollective.com/funded"} + ] + } + }, + "maintainers": [{"name": "alice", "email": "alice@example.com"}] +}`)) + case "/unfunded": + _, _ = w.Write([]byte(`{ + "_id": "unfunded", + "dist-tags": {"latest": "2.0.0"}, + "versions": {"2.0.0": {}} +}`)) + case "/api/v1/gems/rails.json": + _, _ = w.Write([]byte(`{ + "name": "rails", + "version": "7.1.0", + "funding_uri": "https://github.com/sponsors/rails" +}`)) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + + httpClient := registries.NewClient(registryclient.WithHTTPClient(srv.Client())) + c := &RegistriesClient{client: httpClient} + repo := "?repository_url=" + url.QueryEscape(srv.URL) + funded := "pkg:npm/funded" + repo + unfunded := "pkg:npm/unfunded" + repo + gem := "pkg:gem/rails" + repo + + result, err := c.BulkLookup(context.Background(), []string{funded, unfunded, gem}) + if err != nil { + t.Fatalf("BulkLookup: %v", err) + } + + tests := []struct { + purl string + wantFunding []string + wantMaintainers []Maintainer + }{ + { + purl: funded, + wantFunding: []string{"https://github.com/sponsors/funded", "https://opencollective.com/funded"}, + wantMaintainers: []Maintainer{{Login: "alice", Email: "alice@example.com"}}, + }, + {purl: unfunded}, + {purl: gem, wantFunding: []string{"https://github.com/sponsors/rails"}}, + } + for _, tt := range tests { + info := result[tt.purl] + if info == nil { + t.Fatalf("BulkLookup() missing result for %s", tt.purl) + } + if !reflect.DeepEqual(info.FundingLinks, tt.wantFunding) { + t.Errorf("%s FundingLinks = %#v, want %#v", tt.purl, info.FundingLinks, tt.wantFunding) + } + if !reflect.DeepEqual(info.Maintainers, tt.wantMaintainers) { + t.Errorf("%s Maintainers = %#v, want %#v", tt.purl, info.Maintainers, tt.wantMaintainers) + } + } +} + +func TestMaintainersFromRegistry(t *testing.T) { + if got := maintainersFromRegistry(nil); got != nil { + t.Errorf("maintainersFromRegistry(nil) = %#v, want nil", got) + } + if got := maintainersFromRegistry([]registries.Maintainer{}); got != nil { + t.Errorf("maintainersFromRegistry(empty) = %#v, want nil", got) + } + + got := maintainersFromRegistry([]registries.Maintainer{{ + UUID: "123", + Login: "alice", + Name: "Alice", + Email: "alice@example.com", + URL: "https://example.com/alice", + Role: "owner", + }}) + want := []Maintainer{{ + Login: "alice", + Name: "Alice", + Email: "alice@example.com", + URL: "https://example.com/alice", + Role: "owner", + }} + if !reflect.DeepEqual(got, want) { + t.Errorf("maintainersFromRegistry() = %#v, want %#v", got, want) + } +}