diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 9376ddad4..c09067eb6 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -1574,41 +1574,92 @@ func GetTag(t translations.TranslationHelperFunc) inventory.ServerTool { } // First get the tag reference - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag) + ref, refResp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get tag reference", - resp, + refResp, err, ), nil, nil } - defer func() { _ = resp.Body.Close() }() + defer func() { _ = refResp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + if refResp.StatusCode != http.StatusOK { + body, err := io.ReadAll(refResp.Body) if err != nil { return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get tag reference", resp, body), nil, nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get tag reference", refResp, body), nil, nil + } + + // Handle both annotated and lightweight tags. + // Annotated tags have ref.Object.Type == "tag" and point to a tag object. + // Lightweight tags have ref.Object.Type == "commit" and point directly to a commit. + if ref.Object.Type != nil && *ref.Object.Type == "commit" { + // Lightweight tag — resolve the commit directly + commit, commitResp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get commit for lightweight tag", + commitResp, + err, + ), nil, nil + } + defer func() { _ = commitResp.Body.Close() }() + + if commitResp.StatusCode != http.StatusOK { + body, err := io.ReadAll(commitResp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get commit for lightweight tag", commitResp, body), nil, nil + } + + // Return a tag-like structure for consistency + lightweightTag := &github.Tag{ + SHA: ref.Object.SHA, + Tag: github.Ptr(tag), + Object: &github.GitObject{ + Type: github.Ptr("commit"), + SHA: ref.Object.SHA, + }, + } + if commit.Message != nil { + lightweightTag.Message = commit.Message + } + if commit.Author != nil { + lightweightTag.Tagger = &github.CommitAuthor{ + Name: commit.Author.Name, + Email: commit.Author.Email, + Date: commit.Author.Date, + } + } + + r, err := json.Marshal(lightweightTag) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil } - // Then get the tag object - tagObj, resp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA) + // Annotated tag — fetch the tag object + tagObj, tagResp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get tag object", - resp, + tagResp, err, ), nil, nil } - defer func() { _ = resp.Body.Close() }() + defer func() { _ = tagResp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + if tagResp.StatusCode != http.StatusOK { + body, err := io.ReadAll(tagResp.Body) if err != nil { return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get tag object", resp, body), nil, nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get tag object", tagResp, body), nil, nil } r, err := json.Marshal(tagObj) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index ae2ece0f6..4980f7998 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -2840,7 +2840,16 @@ func Test_GetTag(t *testing.T) { mockTagRef := &github.Reference{ Ref: github.Ptr("refs/tags/v1.0.0"), Object: &github.GitObject{ - SHA: github.Ptr("v1.0.0-tag-sha"), + SHA: github.Ptr("v1.0.0-tag-sha"), + Type: github.Ptr("tag"), + }, + } + + mockLightweightTagRef := &github.Reference{ + Ref: github.Ptr("refs/tags/v2.0.0"), + Object: &github.GitObject{ + SHA: github.Ptr("lightweight-commit-sha"), + Type: github.Ptr("commit"), }, } @@ -2854,6 +2863,15 @@ func Test_GetTag(t *testing.T) { }, } + mockCommit := &github.Commit{ + SHA: github.Ptr("lightweight-commit-sha"), + Message: github.Ptr("Initial commit"), + Author: &github.CommitAuthor{ + Name: github.Ptr("Test User"), + Email: github.Ptr("test@example.com"), + }, + } + tests := []struct { name string mockedClient *http.Client @@ -2892,6 +2910,48 @@ func Test_GetTag(t *testing.T) { expectError: false, expectedTag: mockTagObj, }, + { + name: "successful lightweight tag retrieval", + mockedClient: NewMockedHTTPClient( + WithRequestMatchHandler( + GetReposGitRefByOwnerByRepoByRef, + expectPath( + t, + "/repos/owner/repo/git/ref/tags/v2.0.0", + ).andThen( + mockResponse(t, http.StatusOK, mockLightweightTagRef), + ), + ), + WithRequestMatchHandler( + GetReposGitCommitsByOwnerByRepoByCommitSHA, + expectPath( + t, + "/repos/owner/repo/git/commits/lightweight-commit-sha", + ).andThen( + mockResponse(t, http.StatusOK, mockCommit), + ), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "tag": "v2.0.0", + }, + expectError: false, + expectedTag: &github.Tag{ + SHA: github.Ptr("lightweight-commit-sha"), + Tag: github.Ptr("v2.0.0"), + Message: github.Ptr("Initial commit"), + Tagger: &github.CommitAuthor{ + Name: github.Ptr("Test User"), + Email: github.Ptr("test@example.com"), + }, + Object: &github.GitObject{ + Type: github.Ptr("commit"), + SHA: github.Ptr("lightweight-commit-sha"), + }, + }, + }, { name: "tag reference not found", mockedClient: NewMockedHTTPClient( @@ -2911,6 +2971,29 @@ func Test_GetTag(t *testing.T) { expectError: true, expectedErrMsg: "failed to get tag reference", }, + { + name: "lightweight tag commit not found", + mockedClient: NewMockedHTTPClient( + WithRequestMatch( + GetReposGitRefByOwnerByRepoByRef, + mockLightweightTagRef, + ), + WithRequestMatchHandler( + GetReposGitCommitsByOwnerByRepoByCommitSHA, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Commit does not exist"}`)) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "tag": "v2.0.0", + }, + expectError: true, + expectedErrMsg: "failed to get commit for lightweight tag", + }, { name: "tag object not found", mockedClient: NewMockedHTTPClient( @@ -2976,6 +3059,11 @@ func Test_GetTag(t *testing.T) { assert.Equal(t, *tc.expectedTag.Message, *returnedTag.Message) assert.Equal(t, *tc.expectedTag.Object.Type, *returnedTag.Object.Type) assert.Equal(t, *tc.expectedTag.Object.SHA, *returnedTag.Object.SHA) + if tc.expectedTag.Tagger != nil { + require.NotNil(t, returnedTag.Tagger, "expected Tagger to be set") + assert.Equal(t, *tc.expectedTag.Tagger.Name, *returnedTag.Tagger.Name) + assert.Equal(t, *tc.expectedTag.Tagger.Email, *returnedTag.Tagger.Email) + } }) } }