From 6bcfef876cb3bf8bdde078febee2b0964fe1fed8 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 18 Oct 2026 18:58:14 +0100 Subject: [PATCH] Populate Packagist version artifacts from dist fetch.Resolver reads Artifacts, but Packagist only stored the dist URL in Metadata, so resolving a Composer package returned no download URL. Claude-Session: https://claude.ai/code/session_01NxsA75HBjdWQKcdx7FDTDc --- fetch/packagist_resolver_test.go | 44 ++++++++++++++++++++++++++++ internal/packagist/packagist.go | 26 ++++++++++++++++ internal/packagist/packagist_test.go | 34 +++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 fetch/packagist_resolver_test.go diff --git a/fetch/packagist_resolver_test.go b/fetch/packagist_resolver_test.go new file mode 100644 index 0000000..ce82cb8 --- /dev/null +++ b/fetch/packagist_resolver_test.go @@ -0,0 +1,44 @@ +package fetch + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/git-pkgs/registries" + _ "github.com/git-pkgs/registries/internal/packagist" +) + +func TestResolvePackagistDistURL(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/packages/monolog/monolog.json" { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte(`{"package":{"name":"monolog/monolog","versions":{"3.5.0":{"version":"3.5.0", + "dist":{"type":"zip","url":"https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448","shasum":"abc123"}}}}}`)) + })) + defer server.Close() + + reg, err := registries.New("composer", server.URL, registries.DefaultClient()) + if err != nil { + t.Fatal(err) + } + r := NewResolver() + r.RegisterRegistry(reg) + + info, err := r.Resolve(context.Background(), "composer", "monolog/monolog", "3.5.0") + if err != nil { + t.Fatalf("Resolve failed: %v", err) + } + if want := "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448"; info.URL != want { + t.Errorf("URL = %q, want %q", info.URL, want) + } + if want := "c915e2634718dbc8a4a15c61b0e62e7a44e14448.zip"; info.Filename != want { + t.Errorf("Filename = %q, want %q", info.Filename, want) + } + if info.Integrity != "sha1-abc123" { + t.Errorf("Integrity = %q, want sha1-abc123", info.Integrity) + } +} diff --git a/internal/packagist/packagist.go b/internal/packagist/packagist.go index b3e5bc0..4815b96 100644 --- a/internal/packagist/packagist.go +++ b/internal/packagist/packagist.go @@ -4,6 +4,8 @@ package packagist import ( "context" "fmt" + "net/url" + "path" "strings" "time" @@ -176,12 +178,22 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi status = core.StatusDeprecated } + var artifacts []core.Artifact + if v.Dist.URL != "" { + artifacts = []core.Artifact{{ + URL: v.Dist.URL, + Filename: distFilename(v.Dist.URL, v.Dist.Type), + Integrity: integrity, + }} + } + versions = append(versions, core.Version{ Number: v.Version, PublishedAt: publishedAt, Licenses: strings.Join(v.License, ","), Integrity: integrity, Status: status, + Artifacts: artifacts, Metadata: map[string]any{ "dist_url": v.Dist.URL, "dist_type": v.Dist.Type, @@ -192,6 +204,20 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi return versions, nil } +// distFilename is the last path segment of a dist URL. GitHub zipball URLs +// end in a bare commit hash, so zip dists without an extension get ".zip". +func distFilename(rawURL, distType string) string { + name := rawURL + if u, err := url.Parse(rawURL); err == nil { + name = u.Path + } + name = path.Base(name) + if path.Ext(name) == "" && distType == "zip" { + name += ".zip" + } + return name +} + func (r *Registry) FetchDependencies(ctx context.Context, name, version string) ([]core.Dependency, error) { url := fmt.Sprintf("%s/packages/%s.json", r.baseURL, name) diff --git a/internal/packagist/packagist_test.go b/internal/packagist/packagist_test.go index 0549f05..3497f5d 100644 --- a/internal/packagist/packagist_test.go +++ b/internal/packagist/packagist_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "reflect" "testing" "github.com/git-pkgs/registries/internal/core" @@ -231,3 +232,36 @@ func TestEcosystem(t *testing.T) { t.Errorf("expected ecosystem 'composer', got %q", reg.Ecosystem()) } } + +func TestFetchVersionsArtifacts(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"package":{"name":"monolog/monolog","versions":{ + "3.5.0":{"version":"3.5.0","dist":{"type":"zip","url":"https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448","shasum":"abc123"}}, + "3.4.0":{"version":"3.4.0","dist":{"type":"tar","url":"https://example.com/dist/monolog-3.4.0.tar?token=x"}}, + "dev-main":{"version":"dev-main"}}}}`)) + })) + defer server.Close() + + versions, err := New(server.URL, core.DefaultClient()).FetchVersions(context.Background(), "monolog/monolog") + if err != nil { + t.Fatalf("FetchVersions failed: %v", err) + } + + want := map[string][]core.Artifact{ + "3.5.0": {{ + URL: "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", + Filename: "c915e2634718dbc8a4a15c61b0e62e7a44e14448.zip", + Integrity: "sha1-abc123", + }}, + "3.4.0": {{ + URL: "https://example.com/dist/monolog-3.4.0.tar?token=x", + Filename: "monolog-3.4.0.tar", + }}, + "dev-main": nil, + } + for _, v := range versions { + if !reflect.DeepEqual(v.Artifacts, want[v.Number]) { + t.Errorf("%s artifacts = %+v, want %+v", v.Number, v.Artifacts, want[v.Number]) + } + } +}