feat: Add notifications support and templates for PR reviewer decision (#881)

This commit is contained in:
Akhilesh Pandey 2023-12-15 18:02:53 +00:00 committed by Harness
parent 9a4e4fa74d
commit 880f62a857
8 changed files with 223 additions and 22 deletions

View File

@ -22,6 +22,7 @@ import (
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
events "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/git"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types"
@ -107,6 +108,11 @@ func (c *Controller) ReviewSubmit(
if err != nil {
return err
}
c.eventReporter.ReviewSubmitted(ctx, &events.ReviewSubmittedPayload{
Base: eventBase(pr, &session.Principal),
Decision: review.Decision,
ReviewerID: review.CreatedBy,
})
_, err = c.updateReviewer(ctx, session, pr, review, commitSHA)
return err

View File

@ -0,0 +1,56 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package events
import (
"context"
"github.com/harness/gitness/events"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
const ReviewSubmittedEvent events.EventType = "review-submitted"
type ReviewSubmittedPayload struct {
Base
ReviewerID int64
Decision enum.PullReqReviewDecision
}
func (r *Reporter) ReviewSubmitted(
ctx context.Context,
payload *ReviewSubmittedPayload,
) {
if payload == nil {
return
}
eventID, err := events.ReporterSendEvent(r.innerReporter, ctx, ReviewSubmittedEvent, payload)
if err != nil {
log.Ctx(ctx).Err(err).Msgf("failed to send pull request review submitted event")
return
}
log.Ctx(ctx).Debug().Msgf("reported pull request review submitted event with id '%s'", eventID)
}
func (r *Reader) RegisterReviewSubmitted(
fn events.HandlerFunc[*ReviewSubmittedPayload],
opts ...events.HandlerOption,
) error {
return events.ReaderRegisterEvent(r.innerReader, ReviewSubmittedEvent, fn, opts...)
}

View File

@ -30,4 +30,5 @@ type Client interface {
recipients []*types.PrincipalInfo,
payload *PullReqBranchUpdatedPayload,
) error
SendReviewSubmitted(ctx context.Context, recipients []*types.PrincipalInfo, payload *ReviewSubmittedPayload) error
}

View File

@ -28,6 +28,7 @@ const (
TemplateReviewerAdded = "reviewer_added.html"
TemplateCommentCreated = "comment_created.html"
TemplatePullReqBranchUpdated = "pullreq_branch_updated.html"
TemplateNameReviewSubmitted = "review_submitted.html"
)
type MailClient struct {
@ -45,7 +46,7 @@ func (m MailClient) SendCommentCreated(
recipients []*types.PrincipalInfo,
payload *CommentCreatedPayload,
) error {
mailPayload, err := GenerateEmailFromPayload(
email, err := GenerateEmailFromPayload(
TemplateCommentCreated,
recipients,
payload.Base,
@ -56,7 +57,7 @@ func (m MailClient) SendCommentCreated(
pullreqevents.CommentCreatedEvent, err)
}
return m.Mailer.Send(ctx, *mailPayload)
return m.Mailer.Send(ctx, *email)
}
func (m MailClient) SendReviewerAdded(
@ -64,7 +65,7 @@ func (m MailClient) SendReviewerAdded(
recipients []*types.PrincipalInfo,
payload *ReviewerAddedPayload,
) error {
reviewerAddedMail, err := GenerateEmailFromPayload(
email, err := GenerateEmailFromPayload(
TemplateReviewerAdded,
recipients,
payload.Base,
@ -75,7 +76,7 @@ func (m MailClient) SendReviewerAdded(
pullreqevents.ReviewerAddedEvent, err)
}
return m.Mailer.Send(ctx, *reviewerAddedMail)
return m.Mailer.Send(ctx, *email)
}
func (m MailClient) SendPullReqBranchUpdated(
@ -83,7 +84,7 @@ func (m MailClient) SendPullReqBranchUpdated(
recipients []*types.PrincipalInfo,
payload *PullReqBranchUpdatedPayload,
) error {
mailPayload, err := GenerateEmailFromPayload(
email, err := GenerateEmailFromPayload(
TemplatePullReqBranchUpdated,
recipients,
payload.Base,
@ -94,7 +95,23 @@ func (m MailClient) SendPullReqBranchUpdated(
pullreqevents.BranchUpdatedEvent, err)
}
return m.Mailer.Send(ctx, *mailPayload)
return m.Mailer.Send(ctx, *email)
}
func (m MailClient) SendReviewSubmitted(
ctx context.Context,
recipients []*types.PrincipalInfo,
payload *ReviewSubmittedPayload,
) error {
email, err := GenerateEmailFromPayload(TemplateNameReviewSubmitted, recipients, payload.Base, payload)
if err != nil {
return fmt.Errorf(
"failed to generate mail requests after processing %s event: %w",
pullreqevents.ReviewSubmittedEvent,
err,
)
}
return m.Mailer.Send(ctx, *email)
}
func GetSubjectPullRequest(
@ -130,14 +147,14 @@ func GenerateEmailFromPayload(
return nil, err
}
var mail mailer.Payload
mail.Body = string(body)
mail.Subject = subject
mail.RepoRef = base.Repo.Path
var email mailer.Payload
email.Body = string(body)
email.Subject = subject
email.RepoRef = base.Repo.Path
recipientEmails := RetrieveEmailsFromPrincipals(recipients)
mail.ToRecipients = recipientEmails
return &mail, nil
email.ToRecipients = recipientEmails
return &email, nil
}
func RetrieveEmailsFromPrincipals(principals []*types.PrincipalInfo) []string {

View File

@ -0,0 +1,100 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"context"
"fmt"
pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/events"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
type ReviewSubmittedPayload struct {
Base *BasePullReqPayload
Author *types.PrincipalInfo
Reviewer *types.PrincipalInfo
Decision enum.PullReqReviewDecision
}
func (s *Service) notifyReviewSubmitted(
ctx context.Context,
event *events.Event[*pullreqevents.ReviewSubmittedPayload],
) error {
notificationPayload, recipients, err := s.processReviewSubmittedEvent(ctx, event)
if err != nil {
return fmt.Errorf(
"failed to process %s event for pullReqID %d: %w",
pullreqevents.ReviewSubmittedEvent,
event.Payload.PullReqID,
err,
)
}
err = s.notificationClient.SendReviewSubmitted(
ctx,
recipients,
notificationPayload,
)
if err != nil {
return fmt.Errorf(
"failed to send notification for event %s for pullReqID %d: %w",
pullreqevents.ReviewSubmittedEvent,
event.Payload.PullReqID,
err,
)
}
return nil
}
func (s *Service) processReviewSubmittedEvent(
ctx context.Context,
event *events.Event[*pullreqevents.ReviewSubmittedPayload],
) (*ReviewSubmittedPayload, []*types.PrincipalInfo, error) {
base, err := s.getBasePayload(ctx, event.Payload.Base)
if err != nil {
return nil, nil, fmt.Errorf("failed to get base payload: %w", err)
}
authorPrincipal, err := s.principalInfoCache.Get(ctx, base.PullReq.CreatedBy)
if err != nil {
return nil, nil, fmt.Errorf(
"failed to get author from principalInfoCache on %s event for pullReqID %d: %w",
pullreqevents.ReviewSubmittedEvent,
event.Payload.PullReqID,
err,
)
}
reviewerPrincipal, err := s.principalInfoCache.Get(ctx, event.Payload.ReviewerID)
if err != nil {
return nil, nil, fmt.Errorf(
"failed to get reviewer from principalInfoCache on event %s for pullReqID %d: %w",
pullreqevents.ReviewSubmittedEvent,
event.Payload.PullReqID,
err,
)
}
return &ReviewSubmittedPayload{
Base: base,
Author: authorPrincipal,
Decision: event.Payload.Decision,
Reviewer: reviewerPrincipal,
}, []*types.PrincipalInfo{authorPrincipal}, nil
}

View File

@ -32,7 +32,7 @@ func (s *Service) notifyReviewerAdded(
ctx context.Context,
event *events.Event[*pullreqevents.ReviewerAddedPayload],
) error {
payload, err := s.processReviewerAddedEvent(ctx, event)
payload, recipients, err := s.processReviewerAddedEvent(ctx, event)
if err != nil {
return fmt.Errorf(
"failed to process %s event for pullReqID %d: %w",
@ -42,11 +42,6 @@ func (s *Service) notifyReviewerAdded(
)
}
// Send notification to author and reviewer
recipients := []*types.PrincipalInfo{
payload.Base.Author,
payload.Reviewer,
}
err = s.notificationClient.SendReviewerAdded(ctx, recipients, payload)
if err != nil {
return fmt.Errorf(
@ -63,19 +58,24 @@ func (s *Service) notifyReviewerAdded(
func (s *Service) processReviewerAddedEvent(
ctx context.Context,
event *events.Event[*pullreqevents.ReviewerAddedPayload],
) (*ReviewerAddedPayload, error) {
) (*ReviewerAddedPayload, []*types.PrincipalInfo, error) {
base, err := s.getBasePayload(ctx, event.Payload.Base)
if err != nil {
return nil, fmt.Errorf("failed to get base payload: %w", err)
return nil, nil, fmt.Errorf("failed to get base payload: %w", err)
}
reviewerPrincipal, err := s.principalInfoCache.Get(ctx, event.Payload.ReviewerID)
if err != nil {
return nil, fmt.Errorf("failed to get reviewer from principalInfoCache: %w", err)
return nil, nil, fmt.Errorf("failed to get reviewer from principalInfoCache: %w", err)
}
recipients := []*types.PrincipalInfo{
base.Author,
reviewerPrincipal,
}
return &ReviewerAddedPayload{
Base: base,
Reviewer: reviewerPrincipal,
}, nil
}, recipients, nil
}

View File

@ -141,6 +141,7 @@ func NewService(
_ = r.RegisterReviewerAdded(service.notifyReviewerAdded)
_ = r.RegisterCommentCreated(service.notifyCommentCreated)
_ = r.RegisterBranchUpdated(service.notifyPullReqBranchUpdated)
_ = r.RegisterReviewSubmitted(service.notifyReviewSubmitted)
return nil
})
if err != nil {

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<p>
<b>@{{.Reviewer.DisplayName}}</b>
{{if eq .Decision "approved"}}
approved
{{else if eq .Decision "changereq"}}
requested changes to
{{end}}
pull request #{{.Base.PullReq.Number}} {{.Base.PullReq.Title}}
</p>
<p>
<a href="{{.Base.PullReqURL}}">View pull request #{{.Base.PullReq.Number}}</a>
</p>
</body>
</html>