From 3b2ed1de50d3f00abcfe1a8179264e53d6c9f214 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Mon, 23 Oct 2023 23:59:45 +0000 Subject: [PATCH] feat: [CODE-580]: code comment webhook (#706) --- app/api/controller/pullreq/comment_create.go | 15 +++++- app/events/pullreq/events_comment.go | 54 ++++++++++++++++++++ app/services/webhook/handler_pullreq.go | 53 +++++++++++++++++++ app/services/webhook/service.go | 19 +++++-- app/services/webhook/types.go | 9 ++++ app/services/webhook/wire.go | 18 +++++-- cmd/gitness/wire_gen.go | 2 +- types/enum/webhook.go | 3 ++ web/src/framework/strings/stringTypes.ts | 1 + web/src/i18n/strings.en.yaml | 1 + web/src/pages/WebhookNew/WehookForm.tsx | 13 ++++- web/src/services/code/index.tsx | 1 + web/src/services/code/swagger.yaml | 1 + 13 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 app/events/pullreq/events_comment.go diff --git a/app/api/controller/pullreq/comment_create.go b/app/api/controller/pullreq/comment_create.go index 8fd0d3384..252a02f11 100644 --- a/app/api/controller/pullreq/comment_create.go +++ b/app/api/controller/pullreq/comment_create.go @@ -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 } diff --git a/app/events/pullreq/events_comment.go b/app/events/pullreq/events_comment.go new file mode 100644 index 000000000..1c1df1a31 --- /dev/null +++ b/app/events/pullreq/events_comment.go @@ -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...) +} diff --git a/app/services/webhook/handler_pullreq.go b/app/services/webhook/handler_pullreq.go index 6a0b95bd7..72e02f18e 100644 --- a/app/services/webhook/handler_pullreq.go +++ b/app/services/webhook/handler_pullreq.go @@ -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 + }) +} diff --git a/app/services/webhook/service.go b/app/services/webhook/service.go index 27df4fca9..f379bc91a 100644 --- a/app/services/webhook/service.go +++ b/app/services/webhook/service.go @@ -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 }) diff --git a/app/services/webhook/types.go b/app/services/webhook/types.go index 247bdc89d..46255edcb 100644 --- a/app/services/webhook/types.go +++ b/app/services/webhook/types.go @@ -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"` +} diff --git a/app/services/webhook/wire.go b/app/services/webhook/wire.go index 807ebbe63..77d233857 100644 --- a/app/services/webhook/wire.go +++ b/app/services/webhook/wire.go @@ -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) } diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 5624f6085..a203c12d9 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -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 } diff --git a/types/enum/webhook.go b/types/enum/webhook.go index 2ee84c67b..46548a1ca 100644 --- a/types/enum/webhook.go +++ b/types/enum/webhook.go @@ -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, }) diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 5170211d5..6ca3b5b0f 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -753,6 +753,7 @@ export interface StringsMap { webhookListingContent: string webhookPRBranchUpdated: string webhookPRClosed: string + webhookPRCommentCreated: string webhookPRCreated: string webhookPRReopened: string webhookSelectAllEvents: string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index 5c1dd303e..a163ec315 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -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 diff --git a/web/src/pages/WebhookNew/WehookForm.tsx b/web/src/pages/WebhookNew/WehookForm.tsx index 05b9c2b52..5555fabd4 100644 --- a/web/src/pages/WebhookNew/WehookForm.tsx +++ b/web/src/pages/WebhookNew/WehookForm.tsx @@ -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 { @@ -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} /> + ) : null} diff --git a/web/src/services/code/index.tsx b/web/src/services/code/index.tsx index 8fb4e1a35..f21cd7d64 100644 --- a/web/src/services/code/index.tsx +++ b/web/src/services/code/index.tsx @@ -84,6 +84,7 @@ export type EnumWebhookTrigger = | 'pullreq_branch_updated' | 'pullreq_closed' | 'pullreq_created' + | 'pullreq_comment_created' | 'pullreq_reopened' | 'tag_created' | 'tag_deleted' diff --git a/web/src/services/code/swagger.yaml b/web/src/services/code/swagger.yaml index 393ad8fd0..0c8d31382 100644 --- a/web/src/services/code/swagger.yaml +++ b/web/src/services/code/swagger.yaml @@ -6381,6 +6381,7 @@ components: - pullreq_branch_updated - pullreq_closed - pullreq_created + - pullreq_comment_created - pullreq_reopened - tag_created - tag_deleted