Skip to content

github-bots/sdk: add function to set comments on GitHub Issues #762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/sethvargo/go-envconfig v1.1.1
github.com/shirou/gopsutil/v4 v4.25.2
github.com/snabb/httpreaderat v1.0.1
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/contrib/detectors/gcp v1.35.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
Expand Down Expand Up @@ -64,6 +65,7 @@ require (
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
Expand Down Expand Up @@ -104,6 +106,7 @@ require (
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
Expand Down Expand Up @@ -133,4 +136,5 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
26 changes: 21 additions & 5 deletions modules/github-bots/sdk/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,32 +279,48 @@ func (c GitHubClient) RemoveLabel(ctx context.Context, pr *github.PullRequest, l
return nil
}

// SetComment adds or replaces a bot comment on the given pull request.
func (c GitHubClient) SetComment(ctx context.Context, pr *github.PullRequest, botName, content string) error {
cs, _, err := c.inner.Issues.ListComments(ctx, *pr.Base.Repo.Owner.Login, *pr.Base.Repo.Name, *pr.Number, nil)
// setCommentHelper is a helper function that adds or replaces a bot comment on a GitHub issue or PR.
// It's used by both SetComment and SetIssueComment to avoid code duplication.
func (c GitHubClient) setCommentHelper(ctx context.Context, owner, repo string, number int, botName, content string) error {
cs, _, err := c.inner.Issues.ListComments(ctx, owner, repo, number, nil)
if err != nil {
return fmt.Errorf("listing comments: %w", err)
}
content = fmt.Sprintf("<!-- bot:%s -->\n\n%s", botName, content)

for _, com := range cs {
if strings.Contains(*com.Body, fmt.Sprintf("<!-- bot:%s -->", botName)) {
if _, resp, err := c.inner.Issues.EditComment(ctx, *pr.Base.Repo.Owner.Login, *pr.Base.Repo.Name, *com.ID, &github.IssueComment{
if _, resp, err := c.inner.Issues.EditComment(ctx, owner, repo, *com.ID, &github.IssueComment{
Body: &content,
}); err != nil || resp.StatusCode != 200 {
return validateResponse(ctx, err, resp, "editing comment")
}
return nil
}
}
if _, resp, err := c.inner.Issues.CreateComment(ctx, *pr.Base.Repo.Owner.Login, *pr.Base.Repo.Name, *pr.Number, &github.IssueComment{
if _, resp, err := c.inner.Issues.CreateComment(ctx, owner, repo, number, &github.IssueComment{
Body: &content,
}); err != nil || resp.StatusCode != 201 {
return validateResponse(ctx, err, resp, "create comment")
}
return nil
}

// SetComment adds or replaces a bot comment on the given pull request.
func (c GitHubClient) SetComment(ctx context.Context, pr *github.PullRequest, botName, content string) error {
return c.setCommentHelper(ctx, *pr.Base.Repo.Owner.Login, *pr.Base.Repo.Name, *pr.Number, botName, content)
}

// SetIssueComment adds or replaces a bot comment on the given GitHub issue.
func (c GitHubClient) SetIssueComment(ctx context.Context, issue *github.Issue, botName, content string) error {
owner, repoName, err := getIssueRepoInfo(issue)
if err != nil {
return fmt.Errorf("getting repo info: %w", err)
}

return c.setCommentHelper(ctx, owner, repoName, issue.GetNumber(), botName, content)
}

// AddComment adds a new comment to the given pull request.
func (c GitHubClient) AddComment(ctx context.Context, pr *github.PullRequest, botName, content string) error {
content = fmt.Sprintf("<!-- bot:%s -->\n\n%s", botName, content)
Expand Down
35 changes: 35 additions & 0 deletions modules/github-bots/sdk/issue_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package sdk

import (
"fmt"
"strings"

"github.com/google/go-github/v68/github"
)

// getIssueRepoInfo extracts owner and repository name from an issue
// the repository object is not available on the issue, so we need to extract it from the URL
func getIssueRepoInfo(issue *github.Issue) (owner, repoName string, err error) {
// If the issue has a repository object, we can get the owner and repo name from there
if issue.Repository != nil {
owner = issue.Repository.GetOwner().GetLogin()
repoName = issue.Repository.GetName()
return
}

// If the repository object is not available, we need to extract it from the URL
// Split the URL by "/"
parts := strings.Split(issue.GetRepositoryURL(), "/")

// URLs should be in format https://api.github.com/repos/owner/repo
// We need at least 2 parts after "repos" to get owner and repo
if len(parts) >= 2 {
// Get the last two parts
repoName = parts[len(parts)-1]
owner = parts[len(parts)-2]
return
}

// If we don't have at least 2 parts, return an error
return "", "", fmt.Errorf("found %d parts in URL %s, expected at least 2", len(parts), issue.GetRepositoryURL())
}
51 changes: 51 additions & 0 deletions modules/github-bots/sdk/issue_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package sdk

import (
"testing"

"github.com/google/go-github/v68/github"
"github.com/stretchr/testify/assert"
)

func Test_RepoInfo(t *testing.T) {
t.Run("extract from issue with URL", func(t *testing.T) {
// Create an issue with a repository URL directly
issue := &github.Issue{
RepositoryURL: github.Ptr("https://api.github.com/repos/foo/bar"),
}

org, repo, err := getIssueRepoInfo(issue)
assert.Nil(t, err)
assert.Equal(t, "foo", org)
assert.Equal(t, "bar", repo)
})

t.Run("extract from issue with Repository object", func(t *testing.T) {
// Create an issue with Repository object
owner := &github.User{Login: github.Ptr("test-owner")}
repo := &github.Repository{
Name: github.Ptr("test-repo"),
Owner: owner,
}
issue := &github.Issue{
Repository: repo,
}

org, repoName, err := getIssueRepoInfo(issue)
assert.Nil(t, err)
assert.Equal(t, "test-owner", org)
assert.Equal(t, "test-repo", repoName)
})

t.Run("error case with malformed URL", func(t *testing.T) {
// URL with fewer than 2 parts when split by "/"
issue := &github.Issue{
RepositoryURL: github.Ptr("no-slashes"),
}

org, repo, err := getIssueRepoInfo(issue)
assert.NotNil(t, err, "Expected an error but got nil")
assert.Empty(t, org, "Expected empty owner string")
assert.Empty(t, repo, "Expected empty repo string")
})
}
Loading