Skip to content

Commit e3fe626

Browse files
feat: add github.pullRequestComment.commentTag to upsert github comments (#358)
* add comment tag to update existing comments Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> * make Send function testable Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> * test if comment gets updated Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> * add docs Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> * use string format and add constants to reuse pattern Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> * fixes after github lib update Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> * fix linting Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> --------- Signed-off-by: Bruno Ferreira <bmibferreira@gmail.com> Co-authored-by: Pasha Kostohrys <pavel@codefresh.io>
1 parent 87bf057 commit e3fe626

File tree

3 files changed

+310
-14
lines changed

3 files changed

+310
-14
lines changed

docs/services/github.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ template.app-deployed: |
8484
content: |
8585
Application {{.app.metadata.name}} is now running new version of deployments manifests.
8686
See more here: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true
87+
commentTag: "continuous-delivery/{{.app.metadata.name}}"
8788
checkRun:
8889
name: "continuous-delivery/{{.app.metadata.name}}"
8990
details_url: "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
@@ -107,4 +108,6 @@ template.app-deployed: |
107108
Setting this option to `false` is required if you would like to deploy older refs in your default branch.
108109
For more information see the [GitHub Deployment API Docs](https://docs.github.com/en/rest/deployments/deployments?apiVersion=2022-11-28#create-a-deployment).
109110
- If `github.pullRequestComment.content` is set to 65536 characters or more, it will be truncated.
111+
- The `github.pullRequestComment.commentTag` parameter is used to identify the comment. If a comment with the specified tag is found, it will be updated (upserted). If no comment with the tag is found, a new comment will be created.
110112
- Reference is optional. When set, it will be used as the ref to deploy. If not set, the revision will be used as the ref to deploy.
113+

pkg/services/github.go

Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,15 @@ type GitHubDeployment struct {
7878
}
7979

8080
type GitHubPullRequestComment struct {
81-
Content string `json:"content,omitempty"`
81+
Content string `json:"content,omitempty"`
82+
CommentTag string `json:"commentTag,omitempty"`
8283
}
8384

8485
const (
8586
repoURLtemplate = "{{.app.spec.source.repoURL}}"
8687
revisionTemplate = "{{.app.status.operationState.syncResult.revision}}"
88+
commentTagFormat = "<!-- argocd-notifications %s -->"
89+
contentFormat = "%s\n%s"
8790
)
8891

8992
func (g *GitHubNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) {
@@ -306,6 +309,13 @@ func (g *GitHubNotification) GetTemplater(name string, f texttemplate.FuncMap) (
306309
return err
307310
}
308311
notification.GitHub.PullRequestComment.Content = contentData.String()
312+
notification.GitHub.PullRequestComment.CommentTag = g.PullRequestComment.CommentTag
313+
314+
if g.PullRequestComment.CommentTag != "" {
315+
notification.GitHub.PullRequestComment.Content = fmt.Sprintf(contentFormat,
316+
notification.GitHub.PullRequestComment.Content,
317+
fmt.Sprintf(commentTagFormat, g.PullRequestComment.CommentTag))
318+
}
309319
}
310320

311321
if g.CheckRun != nil {
@@ -369,7 +379,7 @@ func (g *GitHubNotification) GetTemplater(name string, f texttemplate.FuncMap) (
369379
}, nil
370380
}
371381

372-
func NewGitHubService(opts GitHubOptions) (NotificationService, error) {
382+
func NewGitHubService(opts GitHubOptions) (*gitHubService, error) {
373383
url := "https://api.github.com"
374384
if opts.EnterpriseBaseURL != "" {
375385
url = opts.EnterpriseBaseURL
@@ -405,16 +415,65 @@ func NewGitHubService(opts GitHubOptions) (NotificationService, error) {
405415

406416
return &gitHubService{
407417
opts: opts,
408-
client: client,
418+
client: &githubClientAdapter{client: client},
409419
}, nil
410420
}
411421

412422
type gitHubService struct {
413-
opts GitHubOptions
423+
opts GitHubOptions
424+
client githubClient
425+
}
426+
427+
// Define interfaces for the GitHub client
428+
type githubClient interface {
429+
GetIssues() issuesService
430+
GetPullRequests() pullRequestsService
431+
GetRepositories() repositoriesService
432+
GetChecks() checksService
433+
}
434+
435+
type issuesService interface {
436+
ListComments(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error)
437+
CreateComment(ctx context.Context, owner, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
438+
EditComment(ctx context.Context, owner, repo string, commentID int64, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
439+
}
414440

441+
type pullRequestsService interface {
442+
ListPullRequestsWithCommit(ctx context.Context, owner string, repo string, sha string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error)
443+
}
444+
445+
type repositoriesService interface {
446+
CreateStatus(ctx context.Context, owner, repo, ref string, status *github.RepoStatus) (*github.RepoStatus, *github.Response, error)
447+
ListDeployments(ctx context.Context, owner, repo string, opts *github.DeploymentsListOptions) ([]*github.Deployment, *github.Response, error)
448+
CreateDeployment(ctx context.Context, owner, repo string, request *github.DeploymentRequest) (*github.Deployment, *github.Response, error)
449+
CreateDeploymentStatus(ctx context.Context, owner, repo string, deploymentID int64, request *github.DeploymentStatusRequest) (*github.DeploymentStatus, *github.Response, error)
450+
}
451+
452+
type checksService interface {
453+
CreateCheckRun(ctx context.Context, owner, repo string, opts github.CreateCheckRunOptions) (*github.CheckRun, *github.Response, error)
454+
}
455+
456+
// Adapter implementation
457+
type githubClientAdapter struct {
415458
client *github.Client
416459
}
417460

461+
func (g *githubClientAdapter) GetIssues() issuesService {
462+
return g.client.Issues
463+
}
464+
465+
func (g *githubClientAdapter) GetPullRequests() pullRequestsService {
466+
return &pullRequestsServiceAdapter{service: g.client.PullRequests}
467+
}
468+
469+
func (g *githubClientAdapter) GetRepositories() repositoriesService {
470+
return g.client.Repositories
471+
}
472+
473+
func (g *githubClientAdapter) GetChecks() checksService {
474+
return g.client.Checks
475+
}
476+
418477
func trunc(message string, n int) string {
419478
if utf8.RuneCountInString(message) > n {
420479
return string([]rune(message)[0:n-3]) + "..."
@@ -448,7 +507,7 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
448507
if notification.GitHub.Status != nil {
449508
// maximum is 140 characters
450509
description := trunc(notification.Message, 140)
451-
_, _, err := g.client.Repositories.CreateStatus(
510+
_, _, err := g.client.GetRepositories().CreateStatus(
452511
context.Background(),
453512
u[0],
454513
u[1],
@@ -468,7 +527,7 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
468527
if notification.GitHub.Deployment != nil {
469528
// maximum is 140 characters
470529
description := trunc(notification.Message, 140)
471-
deployments, _, err := g.client.Repositories.ListDeployments(
530+
deployments, _, err := g.client.GetRepositories().ListDeployments(
472531
context.Background(),
473532
u[0],
474533
u[1],
@@ -491,7 +550,7 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
491550
if len(deployments) != 0 {
492551
deployment = deployments[0]
493552
} else {
494-
deployment, _, err = g.client.Repositories.CreateDeployment(
553+
deployment, _, err = g.client.GetRepositories().CreateDeployment(
495554
context.Background(),
496555
u[0],
497556
u[1],
@@ -507,7 +566,7 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
507566
return err
508567
}
509568
}
510-
_, _, err = g.client.Repositories.CreateDeploymentStatus(
569+
_, _, err = g.client.GetRepositories().CreateDeploymentStatus(
511570
context.Background(),
512571
u[0],
513572
u[1],
@@ -528,11 +587,9 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
528587
if notification.GitHub.PullRequestComment != nil {
529588
// maximum is 65536 characters
530589
body := trunc(notification.GitHub.PullRequestComment.Content, 65536)
531-
comment := &github.IssueComment{
532-
Body: &body,
533-
}
590+
commentTag := notification.GitHub.PullRequestComment.CommentTag
534591

535-
prs, _, err := g.client.PullRequests.ListPullRequestsWithCommit(
592+
prs, _, err := g.client.GetPullRequests().ListPullRequestsWithCommit(
536593
context.Background(),
537594
u[0],
538595
u[1],
@@ -544,7 +601,54 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
544601
}
545602

546603
for _, pr := range prs {
547-
_, _, err = g.client.Issues.CreateComment(
604+
if commentTag != "" {
605+
// If comment tag is provided, try to find and update existing comment
606+
tagPattern := fmt.Sprintf(commentTagFormat, commentTag)
607+
comments, _, err := g.client.GetIssues().ListComments(
608+
context.Background(),
609+
u[0],
610+
u[1],
611+
pr.GetNumber(),
612+
nil,
613+
)
614+
if err != nil {
615+
return err
616+
}
617+
618+
var existingComment *github.IssueComment
619+
for _, comment := range comments {
620+
if strings.Contains(comment.GetBody(), tagPattern) {
621+
existingComment = comment
622+
break
623+
}
624+
}
625+
626+
if existingComment != nil {
627+
// Update existing comment
628+
updatedBody := fmt.Sprintf(contentFormat, body, tagPattern)
629+
existingComment.Body = &updatedBody
630+
_, _, err = g.client.GetIssues().EditComment(
631+
context.Background(),
632+
u[0],
633+
u[1],
634+
existingComment.GetID(),
635+
existingComment,
636+
)
637+
if err != nil {
638+
return err
639+
}
640+
continue
641+
}
642+
643+
// If no existing comment found, create new one with tag
644+
body = fmt.Sprintf(contentFormat, body, tagPattern)
645+
}
646+
647+
// Create new comment
648+
comment := &github.IssueComment{
649+
Body: &body,
650+
}
651+
_, _, err = g.client.GetIssues().CreateComment(
548652
context.Background(),
549653
u[0],
550654
u[1],
@@ -576,7 +680,7 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
576680
}
577681
}
578682

579-
_, _, err = g.client.Checks.CreateCheckRun(
683+
_, _, err = g.client.GetChecks().CreateCheckRun(
580684
context.Background(),
581685
u[0],
582686
u[1],
@@ -598,3 +702,17 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
598702

599703
return nil
600704
}
705+
706+
// PullRequestsServiceAdapter adapts GitHub's PullRequestsService to our interface
707+
type pullRequestsServiceAdapter struct {
708+
service *github.PullRequestsService
709+
}
710+
711+
func (a *pullRequestsServiceAdapter) ListPullRequestsWithCommit(ctx context.Context, owner string, repo string, sha string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) {
712+
// Convert PullRequestListOptions to ListOptions
713+
listOpts := &github.ListOptions{
714+
Page: opts.Page,
715+
PerPage: opts.PerPage,
716+
}
717+
return a.service.ListPullRequestsWithCommit(ctx, owner, repo, sha, listOpts)
718+
}

0 commit comments

Comments
 (0)