feat: [CODE-580]: code comment webhook (#706)

This commit is contained in:
Abhinav Singh 2023-10-23 23:59:45 +00:00 committed by Harness
parent 1dab5384c0
commit 3b2ed1de50
13 changed files with 178 additions and 12 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/gitrpc"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types"
@ -192,7 +193,19 @@ func (c *Controller) CommentCreate(
if err = c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypePullrequesUpdated, pr); err != nil {
log.Ctx(ctx).Warn().Msg("failed to publish PR changed event")
}
// if it's a regular comment publish a comment create event
if !act.IsReply() && act.Type == enum.PullReqActivityTypeComment && act.Kind == enum.PullReqActivityKindComment {
c.eventReporter.CommentCreated(ctx, &events.CommentCreatedPayload{
Base: events.Base{
PullReqID: pr.ID,
SourceRepoID: pr.SourceRepoID,
TargetRepoID: pr.TargetRepoID,
PrincipalID: session.Principal.ID,
Number: pr.Number,
},
ActivityID: act.ID,
})
}
return act, nil
}

View File

@ -0,0 +1,54 @@
// 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/rs/zerolog/log"
)
const CommentCreatedEvent events.EventType = "comment-created"
type CommentCreatedPayload struct {
Base
ActivityID int64 `json:"activity_id"`
}
func (r *Reporter) CommentCreated(
ctx context.Context,
payload *CommentCreatedPayload,
) {
if payload == nil {
return
}
eventID, err := events.ReporterSendEvent(r.innerReporter, ctx, CommentCreatedEvent, payload)
if err != nil {
log.Ctx(ctx).Err(err).Msgf("failed to send pull request comment created event")
return
}
log.Ctx(ctx).Debug().Msgf("reported pull request comment created event with id '%s'", eventID)
}
func (r *Reader) RegisterCommentCreated(
fn events.HandlerFunc[*CommentCreatedPayload],
opts ...events.HandlerOption,
) error {
return events.ReaderRegisterEvent(r.innerReader, CommentCreatedEvent, fn, opts...)
}

View File

@ -16,6 +16,7 @@ package webhook
import (
"context"
"fmt"
pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/events"
@ -235,3 +236,55 @@ func (s *Service) handleEventPullReqClosed(ctx context.Context,
}, nil
})
}
// PullReqCommentPayload describes the body of the pullreq comment create trigger.
type PullReqCommentPayload struct {
BaseSegment
PullReqSegment
PullReqTargetReferenceSegment
ReferenceSegment
PullReqCommentSegment
}
func (s *Service) handleEventPullReqComment(
ctx context.Context,
event *events.Event[*pullreqevents.CommentCreatedPayload],
) error {
return s.triggerForEventWithPullReq(ctx, enum.WebhookTriggerPullReqCommentCreated,
event.ID, event.Payload.PrincipalID, event.Payload.PullReqID,
func(principal *types.Principal, pr *types.PullReq, targetRepo, sourceRepo *types.Repository) (any, error) {
targetRepoInfo := repositoryInfoFrom(targetRepo, s.urlProvider)
sourceRepoInfo := repositoryInfoFrom(sourceRepo, s.urlProvider)
activity, err := s.activityStore.Find(ctx, event.Payload.ActivityID)
if err != nil {
return nil, fmt.Errorf("failed to get activity by id for acitivity id %d: %w", event.Payload.ActivityID, err)
}
return &PullReqCommentPayload{
BaseSegment: BaseSegment{
Trigger: enum.WebhookTriggerPullReqCommentCreated,
Repo: targetRepoInfo,
Principal: principalInfoFrom(principal),
},
PullReqSegment: PullReqSegment{
PullReq: pullReqInfoFrom(pr),
},
PullReqTargetReferenceSegment: PullReqTargetReferenceSegment{
TargetRef: ReferenceInfo{
Name: gitReferenceNamePrefixBranch + pr.TargetBranch,
Repo: targetRepoInfo,
},
},
ReferenceSegment: ReferenceSegment{
Ref: ReferenceInfo{
Name: gitReferenceNamePrefixBranch + pr.SourceBranch,
Repo: sourceRepoInfo,
},
},
PullReqCommentSegment: PullReqCommentSegment{
CommentInfo: CommentInfo{
Text: activity.Text,
},
},
}, nil
})
}

View File

@ -83,6 +83,7 @@ type Service struct {
pullreqStore store.PullReqStore
principalStore store.PrincipalStore
gitRPCClient gitrpc.Interface
activityStore store.PullReqActivityStore
encrypter encrypt.Encrypter
secureHTTPClient *http.Client
@ -94,12 +95,20 @@ type Service struct {
config Config
}
func NewService(ctx context.Context, config Config,
func NewService(
ctx context.Context,
config Config,
gitReaderFactory *events.ReaderFactory[*gitevents.Reader],
prReaderFactory *events.ReaderFactory[*pullreqevents.Reader],
webhookStore store.WebhookStore, webhookExecutionStore store.WebhookExecutionStore,
repoStore store.RepoStore, pullreqStore store.PullReqStore, urlProvider url.Provider,
principalStore store.PrincipalStore, gitRPCClient gitrpc.Interface, encrypter encrypt.Encrypter,
webhookStore store.WebhookStore,
webhookExecutionStore store.WebhookExecutionStore,
repoStore store.RepoStore,
pullreqStore store.PullReqStore,
activityStore store.PullReqActivityStore,
urlProvider url.Provider,
principalStore store.PrincipalStore,
gitRPCClient gitrpc.Interface,
encrypter encrypt.Encrypter,
) (*Service, error) {
if err := config.Prepare(); err != nil {
return nil, fmt.Errorf("provided webhook service config is invalid: %w", err)
@ -109,6 +118,7 @@ func NewService(ctx context.Context, config Config,
webhookExecutionStore: webhookExecutionStore,
repoStore: repoStore,
pullreqStore: pullreqStore,
activityStore: activityStore,
urlProvider: urlProvider,
principalStore: principalStore,
gitRPCClient: gitRPCClient,
@ -163,6 +173,7 @@ func NewService(ctx context.Context, config Config,
_ = r.RegisterReopened(service.handleEventPullReqReopened)
_ = r.RegisterBranchUpdated(service.handleEventPullReqBranchUpdated)
_ = r.RegisterClosed(service.handleEventPullReqClosed)
_ = r.RegisterCommentCreated(service.handleEventPullReqComment)
return nil
})

View File

@ -63,6 +63,11 @@ type PullReqSegment struct {
PullReq PullReqInfo `json:"pull_req"`
}
// PullReqCommentSegment contains details for all pull req comment related payloads for webhooks.
type PullReqCommentSegment struct {
CommentInfo CommentInfo `json:"comment"`
}
// RepositoryInfo describes the repo related info for a webhook payload.
// NOTE: don't use types package as we want webhook payload to be independent from API calls.
type RepositoryInfo struct {
@ -193,3 +198,7 @@ type ReferenceInfo struct {
Name string `json:"name"`
Repo RepositoryInfo `json:"repo"`
}
type CommentInfo struct {
Text string `json:"text"`
}

View File

@ -33,13 +33,21 @@ var WireSet = wire.NewSet(
ProvideService,
)
func ProvideService(ctx context.Context, config Config,
func ProvideService(ctx context.Context,
config Config,
gitReaderFactory *events.ReaderFactory[*gitevents.Reader],
prReaderFactory *events.ReaderFactory[*pullreqevents.Reader],
webhookStore store.WebhookStore, webhookExecutionStore store.WebhookExecutionStore,
repoStore store.RepoStore, pullreqStore store.PullReqStore, urlProvider url.Provider,
principalStore store.PrincipalStore, gitRPCClient gitrpc.Interface, encrypter encrypt.Encrypter) (*Service, error) {
webhookStore store.WebhookStore,
webhookExecutionStore store.WebhookExecutionStore,
repoStore store.RepoStore,
pullreqStore store.PullReqStore,
activityStore store.PullReqActivityStore,
urlProvider url.Provider,
principalStore store.PrincipalStore,
gitRPCClient gitrpc.Interface,
encrypter encrypt.Encrypter,
) (*Service, error) {
return NewService(ctx, config, gitReaderFactory, prReaderFactory,
webhookStore, webhookExecutionStore, repoStore, pullreqStore,
webhookStore, webhookExecutionStore, repoStore, pullreqStore, activityStore,
urlProvider, principalStore, gitRPCClient, encrypter)
}

View File

@ -223,7 +223,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
webhookConfig := server.ProvideWebhookConfig(config)
webhookStore := database.ProvideWebhookStore(db)
webhookExecutionStore := database.ProvideWebhookExecutionStore(db)
webhookService, err := webhook.ProvideService(ctx, webhookConfig, readerFactory, eventsReaderFactory, webhookStore, webhookExecutionStore, repoStore, pullReqStore, provider, principalStore, gitrpcInterface, encrypter)
webhookService, err := webhook.ProvideService(ctx, webhookConfig, readerFactory, eventsReaderFactory, webhookStore, webhookExecutionStore, repoStore, pullReqStore, pullReqActivityStore, provider, principalStore, gitrpcInterface, encrypter)
if err != nil {
return nil, err
}

View File

@ -135,6 +135,8 @@ const (
WebhookTriggerPullReqBranchUpdated WebhookTrigger = "pullreq_branch_updated"
// WebhookTriggerPullReqClosed gets triggered when a pull request is closed.
WebhookTriggerPullReqClosed WebhookTrigger = "pullreq_closed"
// WebhookTriggerPullReqCommentCreated gets triggered when a pull request comment gets created.
WebhookTriggerPullReqCommentCreated WebhookTrigger = "pullreq_comment_created"
)
var webhookTriggers = sortEnum([]WebhookTrigger{
@ -148,4 +150,5 @@ var webhookTriggers = sortEnum([]WebhookTrigger{
WebhookTriggerPullReqReopened,
WebhookTriggerPullReqBranchUpdated,
WebhookTriggerPullReqClosed,
WebhookTriggerPullReqCommentCreated,
})

View File

@ -753,6 +753,7 @@ export interface StringsMap {
webhookListingContent: string
webhookPRBranchUpdated: string
webhookPRClosed: string
webhookPRCommentCreated: string
webhookPRCreated: string
webhookPRReopened: string
webhookSelectAllEvents: string

View File

@ -364,6 +364,7 @@ webhookPRBranchUpdated: PR branch updated
webhookPRCreated: PR created
webhookPRReopened: PR reopened
webhookPRClosed: PR closed
webhookPRCommentCreated: PR comment created
nameYourWebhook: Name your webhook
submitReview: Submit Review
approve: Approve

View File

@ -55,7 +55,8 @@ enum WebhookIndividualEvent {
PR_CREATED = 'pullreq_created',
PR_REOPENED = 'pullreq_reopened',
PR_BRANCH_UPDATED = 'pullreq_branch_updated',
PR_CLOSED = 'pullreq_closed'
PR_CLOSED = 'pullreq_closed',
PR_COMMENT_CREATED = 'pullreq_comment_created'
}
const SECRET_MASK = '********'
@ -78,6 +79,7 @@ interface FormData {
prReopened: boolean
prBranchUpdated: boolean
prClosed: boolean
prCommentCreated: boolean
}
interface WebHookFormProps extends Pick<GitInfoProps, 'repoMetadata'> {
@ -116,6 +118,7 @@ export function WehookForm({ repoMetadata, isEdit, webhook }: WebHookFormProps)
prReopened: webhook?.triggers?.includes(WebhookIndividualEvent.PR_REOPENED) || false,
prBranchUpdated: webhook?.triggers?.includes(WebhookIndividualEvent.PR_BRANCH_UPDATED) || false,
prClosed: webhook?.triggers?.includes(WebhookIndividualEvent.PR_CLOSED) || false,
prCommentCreated: webhook?.triggers?.includes(WebhookIndividualEvent.PR_COMMENT_CREATED) || false,
events: (webhook?.triggers?.length || 0) > 0 ? WebhookEventType.INDIVIDUAL : WebhookEventType.ALL
}}
formName="create-webhook-form"
@ -162,6 +165,9 @@ export function WehookForm({ repoMetadata, isEdit, webhook }: WebHookFormProps)
if (formData.prClosed) {
triggers.push(WebhookIndividualEvent.PR_CLOSED)
}
if (formData.prCommentCreated) {
triggers.push(WebhookIndividualEvent.PR_COMMENT_CREATED)
}
if (!triggers.length) {
return showError(getString('oneMustBeSelected'))
}
@ -295,6 +301,11 @@ export function WehookForm({ repoMetadata, isEdit, webhook }: WebHookFormProps)
name="prClosed"
className={css.checkbox}
/>
<FormInput.CheckBox
label={getString('webhookPRCommentCreated')}
name="prCommentCreated"
className={css.checkbox}
/>
</section>
</article>
) : null}

View File

@ -84,6 +84,7 @@ export type EnumWebhookTrigger =
| 'pullreq_branch_updated'
| 'pullreq_closed'
| 'pullreq_created'
| 'pullreq_comment_created'
| 'pullreq_reopened'
| 'tag_created'
| 'tag_deleted'

View File

@ -6381,6 +6381,7 @@ components:
- pullreq_branch_updated
- pullreq_closed
- pullreq_created
- pullreq_comment_created
- pullreq_reopened
- tag_created
- tag_deleted