diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 36fe011a7..9ac60c50a 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -22,7 +22,7 @@ import ( "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/plugin" "github.com/harness/gitness/internal/api/controller/principal" - "github.com/harness/gitness/internal/api/controller/pullreq" + pullreq2 "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" "github.com/harness/gitness/internal/api/controller/secret" "github.com/harness/gitness/internal/api/controller/service" @@ -52,7 +52,7 @@ import ( "github.com/harness/gitness/internal/services/exporter" "github.com/harness/gitness/internal/services/importer" "github.com/harness/gitness/internal/services/job" - pullreq2 "github.com/harness/gitness/internal/services/pullreq" + "github.com/harness/gitness/internal/services/pullreq" trigger2 "github.com/harness/gitness/internal/services/trigger" "github.com/harness/gitness/internal/services/webhook" "github.com/harness/gitness/internal/store" @@ -107,6 +107,10 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } + encrypter, err := encrypt.ProvideEncrypter(config) + if err != nil { + return nil, err + } jobStore := database.ProvideJobStore(db) pubsubConfig := pubsub.ProvideConfig(config) universalClient, err := server.ProvideRedis(config) @@ -121,7 +125,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } - repository, err := importer.ProvideRepoImporter(config, provider, gitrpcInterface, repoStore, jobScheduler, executor) + repository, err := importer.ProvideRepoImporter(config, provider, gitrpcInterface, repoStore, encrypter, jobScheduler, executor) if err != nil { return nil, err } @@ -149,14 +153,10 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } spaceController := space.ProvideController(db, provider, eventsStreamer, pathUID, authorizer, pathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository) - pipelineController := pipeline.ProvideController(db, pathUID, pathStore, repoStore, authorizer, pipelineStore) - encrypter, err := encrypt.ProvideEncrypter(config) - if err != nil { - return nil, err - } - secretController := secret.ProvideController(db, pathUID, pathStore, encrypter, secretStore, authorizer, spaceStore) triggerStore := database.ProvideTriggerStore(db) - triggerController := trigger.ProvideController(db, authorizer, triggerStore, pipelineStore, repoStore) + pipelineController := pipeline.ProvideController(db, pathUID, pathStore, repoStore, triggerStore, authorizer, pipelineStore) + secretController := secret.ProvideController(db, pathUID, pathStore, encrypter, secretStore, authorizer, spaceStore) + triggerController := trigger.ProvideController(db, authorizer, triggerStore, pathUID, pipelineStore, repoStore) connectorController := connector.ProvideController(db, pathUID, connectorStore, authorizer, spaceStore) templateController := template.ProvideController(db, pathUID, templateStore, authorizer, spaceStore) pluginStore := database.ProvidePluginStore(db) @@ -179,10 +179,6 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } migrator := codecomments.ProvideMigrator(gitrpcInterface) - pullreqController := pullreq.ProvideController(db, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, gitrpcInterface, reporter, mutexManager, migrator) - webhookConfig := server.ProvideWebhookConfig(config) - webhookStore := database.ProvideWebhookStore(db) - webhookExecutionStore := database.ProvideWebhookExecutionStore(db) readerFactory, err := events4.ProvideReaderFactory(eventsSystem) if err != nil { return nil, err @@ -191,6 +187,16 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } + repoGitInfoView := database.ProvideRepoGitInfoView(db) + repoGitInfoCache := cache.ProvideRepoGitInfoCache(repoGitInfoView) + pullreqService, err := pullreq.ProvideService(ctx, config, readerFactory, eventsReaderFactory, reporter, gitrpcInterface, db, repoGitInfoCache, repoStore, pullReqStore, pullReqActivityStore, codeCommentView, migrator, pubSub, provider) + if err != nil { + return nil, err + } + pullreqController := pullreq2.ProvideController(db, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, gitrpcInterface, reporter, mutexManager, migrator, pullreqService) + 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) if err != nil { return nil, err @@ -233,14 +239,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } cronManager := cron.ProvideManager(serverConfig) - repoGitInfoView := database.ProvideRepoGitInfoView(db) - repoGitInfoCache := cache.ProvideRepoGitInfoCache(repoGitInfoView) - pullreqService, err := pullreq2.ProvideService(ctx, config, readerFactory, eventsReaderFactory, reporter, gitrpcInterface, db, repoGitInfoCache, repoStore, pullReqStore, pullReqActivityStore, codeCommentView, migrator, pubSub, provider) - if err != nil { - return nil, err - } triggerConfig := server.ProvideTriggerConfig(config) - triggerService, err := trigger2.ProvideService(ctx, triggerConfig, readerFactory, eventsReaderFactory) + triggerService, err := trigger2.ProvideService(ctx, triggerConfig, triggerStore, commitService, pullReqStore, repoStore, pipelineStore, triggererTriggerer, readerFactory, eventsReaderFactory) if err != nil { return nil, err } diff --git a/internal/api/controller/execution/create.go b/internal/api/controller/execution/create.go index f529184bd..d8a1ab197 100644 --- a/internal/api/controller/execution/create.go +++ b/internal/api/controller/execution/create.go @@ -70,7 +70,6 @@ func (c *Controller) Create( Sender: session.Principal.UID, Source: branch, Target: branch, - Action: enum.TriggerEventCustom, Params: map[string]string{}, Timestamp: commit.Author.When.UnixMilli(), } diff --git a/internal/api/controller/pipeline/controller.go b/internal/api/controller/pipeline/controller.go index 5168d28d3..2d4b2248e 100644 --- a/internal/api/controller/pipeline/controller.go +++ b/internal/api/controller/pipeline/controller.go @@ -18,6 +18,7 @@ type Controller struct { uidCheck check.PathUID pathStore store.PathStore repoStore store.RepoStore + triggerStore store.TriggerStore authorizer authz.Authorizer pipelineStore store.PipelineStore } @@ -28,6 +29,7 @@ func NewController( authorizer authz.Authorizer, pathStore store.PathStore, repoStore store.RepoStore, + triggerStore store.TriggerStore, pipelineStore store.PipelineStore, ) *Controller { return &Controller{ @@ -35,6 +37,7 @@ func NewController( uidCheck: uidCheck, pathStore: pathStore, repoStore: repoStore, + triggerStore: triggerStore, authorizer: authorizer, pipelineStore: pipelineStore, } diff --git a/internal/api/controller/pipeline/create.go b/internal/api/controller/pipeline/create.go index 4405b0037..503e90155 100644 --- a/internal/api/controller/pipeline/create.go +++ b/internal/api/controller/pipeline/create.go @@ -16,6 +16,8 @@ import ( "github.com/harness/gitness/types" "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" ) var ( @@ -69,6 +71,27 @@ func (c *Controller) Create( return nil, fmt.Errorf("pipeline creation failed: %w", err) } + // Try to create a default trigger on pipeline creation. + // Default trigger operations are set on pull request created, reopened or updated. + // We log an error on failure but don't fail the op. + trigger := &types.Trigger{ + Description: "auto-created trigger on pipeline creation", + Created: now, + Updated: now, + PipelineID: pipeline.ID, + RepoID: pipeline.RepoID, + CreatedBy: session.Principal.ID, + UID: "default", + Actions: []enum.TriggerAction{enum.TriggerActionPullReqCreated, + enum.TriggerActionPullReqReopened, enum.TriggerActionPullReqBranchUpdated}, + Enabled: true, + Version: 0, + } + err = c.triggerStore.Create(ctx, trigger) + if err != nil { + log.Ctx(ctx).Err(err).Msg("failed to create auto trigger on pipeline creation") + } + return pipeline, nil } diff --git a/internal/api/controller/pipeline/wire.go b/internal/api/controller/pipeline/wire.go index 6dfcd0fc0..4d953f8d0 100644 --- a/internal/api/controller/pipeline/wire.go +++ b/internal/api/controller/pipeline/wire.go @@ -22,8 +22,10 @@ func ProvideController(db *sqlx.DB, uidCheck check.PathUID, pathStore store.PathStore, repoStore store.RepoStore, + triggerStore store.TriggerStore, authorizer authz.Authorizer, pipelineStore store.PipelineStore, ) *Controller { - return NewController(db, uidCheck, authorizer, pathStore, repoStore, pipelineStore) + return NewController(db, uidCheck, authorizer, pathStore, + repoStore, triggerStore, pipelineStore) } diff --git a/internal/api/controller/pullreq/controller.go b/internal/api/controller/pullreq/controller.go index e96c9c761..d7baa809b 100644 --- a/internal/api/controller/pullreq/controller.go +++ b/internal/api/controller/pullreq/controller.go @@ -16,6 +16,7 @@ import ( "github.com/harness/gitness/internal/auth/authz" pullreqevents "github.com/harness/gitness/internal/events/pullreq" "github.com/harness/gitness/internal/services/codecomments" + "github.com/harness/gitness/internal/services/pullreq" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/url" "github.com/harness/gitness/lock" @@ -40,6 +41,7 @@ type Controller struct { eventReporter *pullreqevents.Reporter mtxManager lock.MutexManager codeCommentMigrator *codecomments.Migrator + pullreqService *pullreq.Service } func NewController( @@ -57,6 +59,7 @@ func NewController( eventReporter *pullreqevents.Reporter, mtxManager lock.MutexManager, codeCommentMigrator *codecomments.Migrator, + pullreqService *pullreq.Service, ) *Controller { return &Controller{ db: db, @@ -73,6 +76,7 @@ func NewController( codeCommentMigrator: codeCommentMigrator, eventReporter: eventReporter, mtxManager: mtxManager, + pullreqService: pullreqService, } } diff --git a/internal/api/controller/pullreq/pr_recheck.go b/internal/api/controller/pullreq/pr_recheck.go new file mode 100644 index 000000000..f35848810 --- /dev/null +++ b/internal/api/controller/pullreq/pr_recheck.go @@ -0,0 +1,33 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pullreq + +import ( + "context" + "fmt" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types/enum" +) + +// Recheck re-checks all system PR checks (mergeability check, ...). +func (c *Controller) Recheck( + ctx context.Context, + session *auth.Session, + repoRef string, + prNum int64, +) error { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush) + if err != nil { + return fmt.Errorf("failed to acquire access to repo: %w", err) + } + + err = c.pullreqService.UpdateMergeDataIfRequired(ctx, repo.ID, prNum) + if err != nil { + return fmt.Errorf("failed to refresh merge data: %w", err) + } + + return nil +} diff --git a/internal/api/controller/pullreq/wire.go b/internal/api/controller/pullreq/wire.go index 8f6c3a757..e313c49bd 100644 --- a/internal/api/controller/pullreq/wire.go +++ b/internal/api/controller/pullreq/wire.go @@ -9,6 +9,7 @@ import ( "github.com/harness/gitness/internal/auth/authz" pullreqevents "github.com/harness/gitness/internal/events/pullreq" "github.com/harness/gitness/internal/services/codecomments" + "github.com/harness/gitness/internal/services/pullreq" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/url" "github.com/harness/gitness/lock" @@ -28,12 +29,14 @@ func ProvideController(db *sqlx.DB, urlProvider *url.Provider, authorizer authz. pullReqReviewStore store.PullReqReviewStore, pullReqReviewerStore store.PullReqReviewerStore, repoStore store.RepoStore, principalStore store.PrincipalStore, rpcClient gitrpc.Interface, eventReporter *pullreqevents.Reporter, - mtxManager lock.MutexManager, codeCommentMigrator *codecomments.Migrator) *Controller { + mtxManager lock.MutexManager, codeCommentMigrator *codecomments.Migrator, + pullreqService *pullreq.Service, +) *Controller { return NewController(db, urlProvider, authorizer, pullReqStore, pullReqActivityStore, codeCommentsView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, rpcClient, eventReporter, - mtxManager, codeCommentMigrator) + mtxManager, codeCommentMigrator, pullreqService) } diff --git a/internal/api/controller/repo/move.go b/internal/api/controller/repo/move.go index e4edbcfa8..635db3069 100644 --- a/internal/api/controller/repo/move.go +++ b/internal/api/controller/repo/move.go @@ -7,12 +7,12 @@ package repo import ( "context" "fmt" - "github.com/harness/gitness/internal/api/usererror" "strconv" "strings" "time" apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/api/usererror" "github.com/harness/gitness/internal/auth" "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/store/database/dbtx" diff --git a/internal/api/controller/space/events.go b/internal/api/controller/space/events.go index d3b2ee000..5094d82c4 100644 --- a/internal/api/controller/space/events.go +++ b/internal/api/controller/space/events.go @@ -15,6 +15,7 @@ import ( "github.com/harness/gitness/internal/auth" "github.com/harness/gitness/internal/writer" "github.com/harness/gitness/types/enum" + "github.com/rs/zerolog/log" ) diff --git a/internal/api/controller/trigger/common.go b/internal/api/controller/trigger/common.go new file mode 100644 index 000000000..95c71df0e --- /dev/null +++ b/internal/api/controller/trigger/common.go @@ -0,0 +1,58 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package trigger + +import ( + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +const ( + // triggerMaxSecretLength defines the max allowed length of a trigger secret. + // TODO: Check whether this is sufficient for other SCM providers once we + // add support. For now it's good to have a limit and increase if needed. + triggerMaxSecretLength = 4096 +) + +// checkSecret validates the secret of a trigger. +func checkSecret(secret string) error { + if len(secret) > triggerMaxSecretLength { + return check.NewValidationErrorf("The secret of a trigger can be at most %d characters long.", + triggerMaxSecretLength) + } + + return nil +} + +// checkActions validates the trigger actions. +func checkActions(actions []enum.TriggerAction) error { + // ignore duplicates here, should be deduplicated later + for _, action := range actions { + if _, ok := action.Sanitize(); !ok { + return check.NewValidationErrorf("The provided trigger action '%s' is invalid.", action) + } + } + + return nil +} + +// deduplicateActions de-duplicates the actions provided by in the trigger. +func deduplicateActions(in []enum.TriggerAction) []enum.TriggerAction { + if len(in) == 0 { + return []enum.TriggerAction{} + } + + actionSet := make(map[enum.TriggerAction]struct{}) + out := make([]enum.TriggerAction, 0, len(in)) + for _, action := range in { + if _, ok := actionSet[action]; ok { + continue + } + actionSet[action] = struct{}{} + out = append(out, action) + } + + return out +} diff --git a/internal/api/controller/trigger/controller.go b/internal/api/controller/trigger/controller.go index 304974bbc..601b45efe 100644 --- a/internal/api/controller/trigger/controller.go +++ b/internal/api/controller/trigger/controller.go @@ -7,6 +7,7 @@ package trigger import ( "github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" "github.com/jmoiron/sqlx" ) @@ -15,6 +16,7 @@ type Controller struct { db *sqlx.DB authorizer authz.Authorizer triggerStore store.TriggerStore + uidCheck check.PathUID pipelineStore store.PipelineStore repoStore store.RepoStore } @@ -23,6 +25,7 @@ func NewController( db *sqlx.DB, authorizer authz.Authorizer, triggerStore store.TriggerStore, + uidCheck check.PathUID, pipelineStore store.PipelineStore, repoStore store.RepoStore, ) *Controller { @@ -30,6 +33,7 @@ func NewController( db: db, authorizer: authorizer, triggerStore: triggerStore, + uidCheck: uidCheck, pipelineStore: pipelineStore, repoStore: repoStore, } diff --git a/internal/api/controller/trigger/create.go b/internal/api/controller/trigger/create.go index 8d95174a5..60a8451d4 100644 --- a/internal/api/controller/trigger/create.go +++ b/internal/api/controller/trigger/create.go @@ -12,13 +12,17 @@ import ( apiauth "github.com/harness/gitness/internal/api/auth" "github.com/harness/gitness/internal/auth" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" ) // TODO: Add more as needed. type CreateInput struct { - Description string `json:"description"` - UID string `json:"uid"` + Description string `json:"description"` + UID string `json:"uid"` + Secret string `json:"secret"` + Enabled bool `json:"enabled"` + Actions []enum.TriggerAction `json:"actions"` } func (c *Controller) Create( @@ -39,6 +43,11 @@ func (c *Controller) Create( return nil, fmt.Errorf("failed to authorize pipeline: %w", err) } + err = c.checkCreateInput(in) + if err != nil { + return nil, fmt.Errorf("invalid input: %w", err) + } + pipeline, err := c.pipelineStore.FindByUID(ctx, repo.ID, pipelineUID) if err != nil { return nil, fmt.Errorf("failed to find pipeline: %w", err) @@ -47,6 +56,11 @@ func (c *Controller) Create( now := time.Now().UnixMilli() trigger := &types.Trigger{ Description: in.Description, + Enabled: in.Enabled, + Secret: in.Secret, + CreatedBy: session.Principal.ID, + RepoID: repo.ID, + Actions: deduplicateActions(in.Actions), UID: in.UID, PipelineID: pipeline.ID, Created: now, @@ -60,3 +74,20 @@ func (c *Controller) Create( return trigger, nil } + +func (c *Controller) checkCreateInput(in *CreateInput) error { + if err := check.Description(in.Description); err != nil { + return err + } + if err := checkSecret(in.Secret); err != nil { + return err + } + if err := checkActions(in.Actions); err != nil { + return err + } + if err := c.uidCheck(in.UID, false); err != nil { + return err + } + + return nil +} diff --git a/internal/api/controller/trigger/update.go b/internal/api/controller/trigger/update.go index adef2f668..282d2978c 100644 --- a/internal/api/controller/trigger/update.go +++ b/internal/api/controller/trigger/update.go @@ -11,13 +11,17 @@ import ( apiauth "github.com/harness/gitness/internal/api/auth" "github.com/harness/gitness/internal/auth" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" ) // UpdateInput is used for updating a trigger. type UpdateInput struct { - Description string `json:"description"` - UID string `json:"uid"` + Description *string `json:"description"` + UID *string `json:"uid"` + Actions []enum.TriggerAction `json:"actions"` + Secret *string `json:"secret"` + Enabled *bool `json:"enabled"` // can be nil, so keeping it a pointer } func (c *Controller) Update( @@ -38,6 +42,11 @@ func (c *Controller) Update( return nil, fmt.Errorf("failed to authorize pipeline: %w", err) } + err = c.checkUpdateInput(in) + if err != nil { + return nil, fmt.Errorf("invalid input: %w", err) + } + pipeline, err := c.pipelineStore.FindByUID(ctx, repo.ID, pipelineUID) if err != nil { return nil, fmt.Errorf("failed to find pipeline: %w", err) @@ -51,12 +60,48 @@ func (c *Controller) Update( return c.triggerStore.UpdateOptLock(ctx, trigger, func(original *types.Trigger) error { // update values only if provided - if in.Description != "" { - original.Description = in.Description + if in.Description != nil { + original.Description = *in.Description } - if in.UID != "" { - original.UID = in.UID + if in.UID != nil { + original.UID = *in.UID } + if in.Actions != nil { + original.Actions = deduplicateActions(in.Actions) + } + if in.Secret != nil { + original.Secret = *in.Secret + } + if in.Enabled != nil { + original.Enabled = *in.Enabled + } + return nil }) } + +func (c *Controller) checkUpdateInput(in *UpdateInput) error { + if in.Description != nil { + if err := check.Description(*in.Description); err != nil { + return err + } + } + if in.Secret != nil { + if err := checkSecret(*in.Secret); err != nil { + return err + } + } + if in.Actions != nil { + if err := checkActions(in.Actions); err != nil { + return err + } + } + + if in.UID != nil { + if err := c.uidCheck(*in.UID, false); err != nil { + return err + } + } + + return nil +} diff --git a/internal/api/controller/trigger/wire.go b/internal/api/controller/trigger/wire.go index 8fb3ab6f7..09a04b578 100644 --- a/internal/api/controller/trigger/wire.go +++ b/internal/api/controller/trigger/wire.go @@ -7,6 +7,7 @@ package trigger import ( "github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" "github.com/google/wire" "github.com/jmoiron/sqlx" @@ -20,8 +21,9 @@ var WireSet = wire.NewSet( func ProvideController(db *sqlx.DB, authorizer authz.Authorizer, triggerStore store.TriggerStore, + uidCheck check.PathUID, pipelineStore store.PipelineStore, repoStore store.RepoStore, ) *Controller { - return NewController(db, authorizer, triggerStore, pipelineStore, repoStore) + return NewController(db, authorizer, triggerStore, uidCheck, pipelineStore, repoStore) } diff --git a/internal/api/handler/pullreq/pr_recheck.go b/internal/api/handler/pullreq/pr_recheck.go new file mode 100644 index 000000000..212e37f12 --- /dev/null +++ b/internal/api/handler/pullreq/pr_recheck.go @@ -0,0 +1,41 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pullreq + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/pullreq" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +// HandleRecheck handles API that re-checks all system PR checks (mergeability check, ...). +func HandleRecheck(pullreqCtrl *pullreq.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + pullreqNumber, err := request.GetPullReqNumberFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + err = pullreqCtrl.Recheck(ctx, session, repoRef, pullreqNumber) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/internal/api/handler/space/events_stream.go b/internal/api/handler/space/events_stream.go index 1af9e053d..0abebbd33 100644 --- a/internal/api/handler/space/events_stream.go +++ b/internal/api/handler/space/events_stream.go @@ -11,6 +11,7 @@ import ( "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/writer" + "github.com/rs/zerolog/log" ) diff --git a/internal/api/openapi/pullreq.go b/internal/api/openapi/pullreq.go index fdff3d37f..693da8daa 100644 --- a/internal/api/openapi/pullreq.go +++ b/internal/api/openapi/pullreq.go @@ -469,4 +469,15 @@ func pullReqOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opMetaData, new(usererror.Error), http.StatusForbidden) _ = reflector.SetJSONResponse(&opMetaData, new(usererror.Error), http.StatusNotFound) _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/pullreq/{pullreq_number}/metadata", opMetaData) + + recheckPullReq := openapi3.Operation{} + recheckPullReq.WithTags("pullreq") + recheckPullReq.WithMapOfAnything(map[string]interface{}{"operationId": "recheckPullReq"}) + _ = reflector.SetRequest(&recheckPullReq, nil, http.MethodPost) + _ = reflector.SetJSONResponse(&recheckPullReq, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&recheckPullReq, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&recheckPullReq, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&recheckPullReq, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&recheckPullReq, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodPost, "/repos/{repo_ref}/pullreq/{pullreq_number}/recheck", recheckPullReq) } diff --git a/internal/pipeline/commit/gitness.go b/internal/pipeline/commit/gitness.go index 7701b8906..83400ff8d 100644 --- a/internal/pipeline/commit/gitness.go +++ b/internal/pipeline/commit/gitness.go @@ -42,3 +42,24 @@ func (f *service) FindRef( // convert the RPC commit output to a types.Commit. return controller.MapCommit(branchOutput.Branch.Commit) } + +// FindCommit finds information about a commit in gitness for the git SHA +func (f *service) FindCommit( + ctx context.Context, + repo *types.Repository, + sha string, +) (*types.Commit, error) { + readParams := gitrpc.ReadParams{ + RepoUID: repo.GitUID, + } + commitOutput, err := f.gitRPCClient.GetCommit(ctx, &gitrpc.GetCommitParams{ + ReadParams: readParams, + SHA: sha, + }) + if err != nil { + return nil, err + } + + // convert the RPC commit output to a types.Commit. + return controller.MapCommit(&commitOutput.Commit) +} diff --git a/internal/pipeline/commit/service.go b/internal/pipeline/commit/service.go index 37359ce00..bcadbe829 100644 --- a/internal/pipeline/commit/service.go +++ b/internal/pipeline/commit/service.go @@ -17,5 +17,8 @@ type ( CommitService interface { // ref is the ref to fetch the commit from, eg refs/heads/master FindRef(ctx context.Context, repo *types.Repository, ref string) (*types.Commit, error) + + // FindCommit returns information about a commit in a repo. + FindCommit(ctx context.Context, repo *types.Repository, sha string) (*types.Commit, error) } ) diff --git a/internal/pipeline/manager/manager.go b/internal/pipeline/manager/manager.go index 182529fdf..bb4919349 100644 --- a/internal/pipeline/manager/manager.go +++ b/internal/pipeline/manager/manager.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "time" "github.com/harness/gitness/internal/pipeline/events" "github.com/harness/gitness/internal/pipeline/file" @@ -202,7 +201,6 @@ func (m *Manager) Accept(ctx context.Context, id int64, machine string) (*types. stage.Machine = machine stage.Status = enum.CIStatusPending - stage.Updated = time.Now().Unix() err = m.Stages.Update(noContext, stage) if errors.Is(err, gitness_store.ErrVersionConflict) { log.Debug().Err(err).Msg("manager: stage processed by another agent") diff --git a/internal/pipeline/triggerer/skip.go b/internal/pipeline/triggerer/skip.go index 1821abf38..c952bb831 100644 --- a/internal/pipeline/triggerer/skip.go +++ b/internal/pipeline/triggerer/skip.go @@ -7,8 +7,6 @@ package triggerer import ( "strings" - "github.com/harness/gitness/types/enum" - "github.com/drone/drone-yaml/yaml" ) @@ -44,27 +42,6 @@ func skipCron(document *yaml.Pipeline, cron string) bool { return !document.Trigger.Cron.Match(cron) } -func skipMessage(hook *Hook) bool { - switch { - case hook.Event == enum.TriggerEventTag: - return false - case hook.Event == enum.TriggerEventCron: - return false - case hook.Event == enum.TriggerEventCustom: - return false - case hook.Event == enum.TriggerEventPromote: - return false - case hook.Event == enum.TriggerEventRollback: - return false - case skipMessageEval(hook.Message): - return true - case skipMessageEval(hook.Title): - return true - default: - return false - } -} - func skipMessageEval(str string) bool { lower := strings.ToLower(str) switch { diff --git a/internal/pipeline/triggerer/trigger.go b/internal/pipeline/triggerer/trigger.go index 8ef3e425b..e8c1c70ad 100644 --- a/internal/pipeline/triggerer/trigger.go +++ b/internal/pipeline/triggerer/trigger.go @@ -23,42 +23,35 @@ import ( "github.com/rs/zerolog/log" ) -// Trigger types -const ( - TriggerHook = "@hook" - TriggerCron = "@cron" -) - var _ Triggerer = (*triggerer)(nil) // Hook represents the payload of a post-commit hook. type Hook struct { - Parent int64 `json:"parent"` - Trigger string `json:"trigger"` - Event string `json:"event"` - Action string `json:"action"` - Link string `json:"link"` - Timestamp int64 `json:"timestamp"` - Title string `json:"title"` - Message string `json:"message"` - Before string `json:"before"` - After string `json:"after"` - Ref string `json:"ref"` - Fork string `json:"fork"` - Source string `json:"source"` - Target string `json:"target"` - AuthorLogin string `json:"author_login"` - AuthorName string `json:"author_name"` - AuthorEmail string `json:"author_email"` - AuthorAvatar string `json:"author_avatar"` - Debug bool `json:"debug"` - Cron string `json:"cron"` - Sender string `json:"sender"` - Params map[string]string `json:"params"` + Parent int64 `json:"parent"` + Trigger string `json:"trigger"` + Action enum.TriggerAction `json:"action"` + Link string `json:"link"` + Timestamp int64 `json:"timestamp"` + Title string `json:"title"` + Message string `json:"message"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + Fork string `json:"fork"` + Source string `json:"source"` + Target string `json:"target"` + AuthorLogin string `json:"author_login"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + AuthorAvatar string `json:"author_avatar"` + Debug bool `json:"debug"` + Cron string `json:"cron"` + Sender string `json:"sender"` + Params map[string]string `json:"params"` } // Triggerer is responsible for triggering a Execution from an -// incoming drone. If an execution is skipped a nil value is +// incoming hook (could be manual or webhook). If an execution is skipped a nil value is // returned. type Triggerer interface { Trigger(ctx context.Context, pipeline *types.Pipeline, hook *Hook) (*types.Execution, error) @@ -115,16 +108,14 @@ func (t *triggerer) Trigger( } }() + event := string(base.Action.GetTriggerEvent()) + repo, err := t.repoStore.Find(ctx, pipeline.RepoID) if err != nil { log.Error().Err(err).Msg("could not find repo") return nil, err } - // if skipMessage(base) { - // logger.Infoln("trigger: skipping hook. found skip directive") - // return nil, nil - // } // if base.Event == core.TriggerEventPullRequest { // if repo.IgnorePulls { // logger.Infoln("trigger: skipping hook. project ignores pull requests") @@ -251,9 +242,9 @@ func (t *triggerer) Trigger( if skipBranch(pipeline, base.Target) { log.Info().Str("pipeline", pipeline.Name).Msg("trigger: skipping pipeline, does not match branch") - } else if skipEvent(pipeline, base.Event) { + } else if skipEvent(pipeline, event) { log.Info().Str("pipeline", pipeline.Name).Msg("trigger: skipping pipeline, does not match event") - } else if skipAction(pipeline, base.Action) { + } else if skipAction(pipeline, string(base.Action)) { log.Info().Str("pipeline", pipeline.Name).Msg("trigger: skipping pipeline, does not match action") } else if skipRef(pipeline, base.Ref) { log.Info().Str("pipeline", pipeline.Name).Msg("trigger: skipping pipeline, does not match ref") @@ -282,6 +273,8 @@ func (t *triggerer) Trigger( return nil, err } + now := time.Now().UnixMilli() + execution := &types.Execution{ RepoID: repo.ID, PipelineID: pipeline.ID, @@ -289,8 +282,8 @@ func (t *triggerer) Trigger( Number: pipeline.Seq, Parent: base.Parent, Status: enum.CIStatusPending, - Event: base.Event, - Action: base.Action, + Event: event, + Action: string(base.Action), Link: base.Link, // Timestamp: base.Timestamp, Title: trunc(base.Title, 2000), @@ -309,8 +302,8 @@ func (t *triggerer) Trigger( Debug: base.Debug, Sender: base.Sender, Cron: base.Cron, - Created: time.Now().Unix(), - Updated: time.Now().Unix(), + Created: now, + Updated: now, } stages := make([]*types.Stage, len(matched)) @@ -321,6 +314,8 @@ func (t *triggerer) Trigger( onFailure = false } + now := time.Now().UnixMilli() + stage := &types.Stage{ RepoID: repo.ID, Number: int64(i + 1), @@ -337,8 +332,8 @@ func (t *triggerer) Trigger( OnSuccess: onSuccess, OnFailure: onFailure, Labels: match.Node, - Created: time.Now().Unix(), - Updated: time.Now().Unix(), + Created: now, + Updated: now, } if stage.Kind == "pipeline" && stage.Type == "" { stage.Type = "docker" @@ -454,14 +449,16 @@ func (t *triggerer) createExecutionWithError( return nil, err } + now := time.Now().UnixMilli() + execution := &types.Execution{ RepoID: pipeline.RepoID, Number: pipeline.Seq, Parent: base.Parent, Status: enum.CIStatusError, Error: message, - Event: base.Event, - Action: base.Action, + Event: string(base.Action.GetTriggerEvent()), + Action: string(base.Action), Link: base.Link, Title: base.Title, Message: base.Message, @@ -477,10 +474,10 @@ func (t *triggerer) createExecutionWithError( AuthorAvatar: base.AuthorAvatar, Debug: base.Debug, Sender: base.Sender, - Created: time.Now().Unix(), - Updated: time.Now().Unix(), - Started: time.Now().Unix(), - Finished: time.Now().Unix(), + Created: now, + Updated: now, + Started: now, + Finished: now, } err = t.executionStore.Create(ctx, execution) diff --git a/internal/router/api.go b/internal/router/api.go index fa0fc70fa..933bab4e4 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -462,6 +462,7 @@ func SetupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) { r.Get("/", handlerpullreq.HandleFind(pullreqCtrl)) r.Patch("/", handlerpullreq.HandleUpdate(pullreqCtrl)) r.Post("/state", handlerpullreq.HandleState(pullreqCtrl)) + r.Post("/recheck", handlerpullreq.HandleRecheck(pullreqCtrl)) r.Get("/activities", handlerpullreq.HandleListActivities(pullreqCtrl)) r.Route("/comments", func(r chi.Router) { r.Post("/", handlerpullreq.HandleCommentCreate(pullreqCtrl)) diff --git a/internal/services/importer/provider.go b/internal/services/importer/provider.go index 3a50d25b9..6fcc675c3 100644 --- a/internal/services/importer/provider.go +++ b/internal/services/importer/provider.go @@ -23,18 +23,14 @@ import ( type ProviderType string const ( - ProviderTypeGitHub ProviderType = "github" - ProviderTypeGitHubEnterprise ProviderType = "github-enterprise" - ProviderTypeGitLab ProviderType = "gitlab" - ProviderTypeGitLabEnterprise ProviderType = "gitlab-enterprise" + ProviderTypeGitHub ProviderType = "github" + ProviderTypeGitLab ProviderType = "gitlab" ) func (p ProviderType) Enum() []any { return []any{ ProviderTypeGitHub, - ProviderTypeGitHubEnterprise, ProviderTypeGitLab, - ProviderTypeGitLabEnterprise, } } @@ -83,67 +79,48 @@ func (r *RepositoryInfo) ToRepo( } func getClient(provider Provider) (*scm.Client, error) { + if provider.Username == "" || provider.Password == "" { + return nil, usererror.BadRequest("scm provider authentication credentials missing") + } + + var c *scm.Client + var err error + switch provider.Type { case "": - return nil, usererror.BadRequest("provider can not be empty") + return nil, usererror.BadRequest("scm provider can not be empty") case ProviderTypeGitHub: - c := github.NewDefault() - if provider.Password != "" { - c.Client = &http.Client{ - Transport: &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}), - }, + if provider.Host != "" { + c, err = github.New(provider.Host) + if err != nil { + return nil, usererror.BadRequestf("scm provider Host invalid: %s", err.Error()) } + } else { + c = github.NewDefault() } - return c, nil - - case ProviderTypeGitHubEnterprise: - c, err := github.New(provider.Host) - if err != nil { - return nil, usererror.BadRequestf("provider Host invalid: %s", err.Error()) - } - - if provider.Password != "" { - c.Client = &http.Client{ - Transport: &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}), - }, - } - } - return c, nil case ProviderTypeGitLab: - c := gitlab.NewDefault() - if provider.Password != "" { - c.Client = &http.Client{ - Transport: &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}), - }, + if provider.Host != "" { + c, err = gitlab.New(provider.Host) + if err != nil { + return nil, usererror.BadRequestf("scm provider Host invalid: %s", err.Error()) } + } else { + c = gitlab.NewDefault() } - return c, nil - - case ProviderTypeGitLabEnterprise: - c, err := gitlab.New(provider.Host) - if err != nil { - return nil, usererror.BadRequestf("provider Host invalid: %s", err.Error()) - } - - if provider.Password != "" { - c.Client = &http.Client{ - Transport: &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}), - }, - } - } - - return c, nil - default: - return nil, usererror.BadRequestf("unsupported provider: %s", provider) + return nil, usererror.BadRequestf("unsupported scm provider: %s", provider) } + + c.Client = &http.Client{ + Transport: &oauth2.Transport{ + Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}), + }, + } + + return c, nil } func LoadRepositoryFromProvider(ctx context.Context, provider Provider, repoSlug string) (RepositoryInfo, error) { scmClient, err := getClient(provider) @@ -155,18 +132,24 @@ func LoadRepositoryFromProvider(ctx context.Context, provider Provider, repoSlug return RepositoryInfo{}, usererror.BadRequest("provider repository identifier is missing") } - scmRepo, _, err := scmClient.Repositories.Find(ctx, repoSlug) - if errors.Is(err, scm.ErrNotFound) { + var statusCode int + scmRepo, scmResp, err := scmClient.Repositories.Find(ctx, repoSlug) + if scmResp != nil { + statusCode = scmResp.Status + } + + if errors.Is(err, scm.ErrNotFound) || statusCode == http.StatusNotFound { return RepositoryInfo{}, usererror.BadRequestf("repository %s not found at %s", repoSlug, provider.Type) } - if errors.Is(err, scm.ErrNotAuthorized) { + if errors.Is(err, scm.ErrNotAuthorized) || statusCode == http.StatusUnauthorized { return RepositoryInfo{}, usererror.BadRequestf("bad credentials provided for %s at %s", repoSlug, provider.Type) } - if err != nil { + if err != nil || statusCode > 299 { return RepositoryInfo{}, - fmt.Errorf("failed to fetch repository %s from %s: %w", repoSlug, provider.Type, err) + fmt.Errorf("failed to fetch repository %s from %s, status=%d: %w", + repoSlug, provider.Type, statusCode, err) } return RepositoryInfo{ @@ -188,33 +171,38 @@ func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, s return nil, usererror.BadRequest("provider space identifier is missing") } - repos := make([]RepositoryInfo, 0) + const pageSize = 100 + opts := scm.ListOptions{Page: 0, Size: pageSize} - const pageSize = 50 - page := 1 + repos := make([]RepositoryInfo, 0) for { - scmRepos, scmResponse, err := scmClient.Repositories.ListV2(ctx, scm.RepoListOptions{ - ListOptions: scm.ListOptions{ - URL: "", - Page: page, - Size: pageSize, - }, - RepoSearchTerm: scm.RepoSearchTerm{ - RepoName: "", - User: spaceSlug, - }, - }) - if errors.Is(err, scm.ErrNotFound) { + opts.Page++ + + var statusCode int + scmRepos, scmResp, err := scmClient.Repositories.List(ctx, opts) + if scmResp != nil { + statusCode = scmResp.Status + } + + if errors.Is(err, scm.ErrNotFound) || statusCode == http.StatusNotFound { return nil, usererror.BadRequestf("space %s not found at %s", spaceSlug, provider.Type) } - if errors.Is(err, scm.ErrNotAuthorized) { + if errors.Is(err, scm.ErrNotAuthorized) || statusCode == http.StatusUnauthorized { return nil, usererror.BadRequestf("bad credentials provided for %s at %s", spaceSlug, provider.Type) } - if err != nil { - return nil, fmt.Errorf("failed to fetch space %s from %s: %w", spaceSlug, provider.Type, err) + if err != nil || statusCode > 299 { + return nil, fmt.Errorf("failed to fetch space %s from %s, status=%d: %w", + spaceSlug, provider.Type, statusCode, err) + } + + if len(scmRepos) == 0 { + break } for _, scmRepo := range scmRepos { + if scmRepo.Namespace != spaceSlug { + continue + } repos = append(repos, RepositoryInfo{ Space: scmRepo.Namespace, UID: scmRepo.Name, @@ -223,12 +211,6 @@ func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, s DefaultBranch: scmRepo.Branch, }) } - - if len(scmRepos) == 0 || page == scmResponse.Page.Last { - break - } - - page++ } return repos, nil diff --git a/internal/services/importer/repository.go b/internal/services/importer/repository.go index a6e9add4a..028fd175e 100644 --- a/internal/services/importer/repository.go +++ b/internal/services/importer/repository.go @@ -6,6 +6,7 @@ package importer import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -13,6 +14,7 @@ import ( "strings" "time" + "github.com/harness/gitness/encrypt" "github.com/harness/gitness/gitrpc" "github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/githook" @@ -35,6 +37,7 @@ type Repository struct { urlProvider *gitnessurl.Provider git gitrpc.Interface repoStore store.RepoStore + encrypter encrypt.Encrypter scheduler *job.Scheduler } @@ -55,27 +58,17 @@ func (r *Repository) Register(executor *job.Executor) error { // Run starts a background job that imports the provided repository from the provided clone URL. func (r *Repository) Run(ctx context.Context, provider Provider, repo *types.Repository, cloneURL string) error { - input := Input{ + jobDef, err := r.getJobDef(*repo.ImportingJobUID, Input{ RepoID: repo.ID, GitUser: provider.Username, GitPass: provider.Password, CloneURL: cloneURL, - } - - data, err := json.Marshal(input) - if err != nil { - return fmt.Errorf("failed to marshal job input json: %w", err) - } - - strData := strings.TrimSpace(string(data)) - - return r.scheduler.RunJob(ctx, job.Definition{ - UID: *repo.ImportingJobUID, - Type: jobType, - MaxRetries: importJobMaxRetries, - Timeout: importJobMaxDuration, - Data: strData, }) + if err != nil { + return err + } + + return r.scheduler.RunJob(ctx, jobDef) } // RunMany starts background jobs that import the provided repositories from the provided clone URLs. @@ -91,34 +84,23 @@ func (r *Repository) RunMany(ctx context.Context, } n := len(repos) - defs := make([]job.Definition, n) for k := 0; k < n; k++ { repo := repos[k] cloneURL := cloneURLs[k] - input := Input{ + jobDef, err := r.getJobDef(*repo.ImportingJobUID, Input{ RepoID: repo.ID, GitUser: provider.Username, GitPass: provider.Password, CloneURL: cloneURL, - } - - data, err := json.Marshal(input) + }) if err != nil { - return fmt.Errorf("failed to marshal job input json: %w", err) + return err } - strData := strings.TrimSpace(string(data)) - - defs[k] = job.Definition{ - UID: *repo.ImportingJobUID, - Type: jobType, - MaxRetries: importJobMaxRetries, - Timeout: importJobMaxDuration, - Data: strData, - } + defs[k] = jobDef } err := r.scheduler.RunJobs(ctx, groupID, defs) @@ -129,13 +111,56 @@ func (r *Repository) RunMany(ctx context.Context, return nil } +func (r *Repository) getJobDef(jobUID string, input Input) (job.Definition, error) { + data, err := json.Marshal(input) + if err != nil { + return job.Definition{}, fmt.Errorf("failed to marshal job input json: %w", err) + } + + strData := strings.TrimSpace(string(data)) + + encryptedData, err := r.encrypter.Encrypt(strData) + if err != nil { + return job.Definition{}, fmt.Errorf("failed to encrypt job input: %w", err) + } + + return job.Definition{ + UID: jobUID, + Type: jobType, + MaxRetries: importJobMaxRetries, + Timeout: importJobMaxDuration, + Data: base64.StdEncoding.EncodeToString(encryptedData), + }, nil +} + +func (r *Repository) getJobInput(data string) (Input, error) { + encrypted, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return Input{}, fmt.Errorf("failed to base64 decode job input: %w", err) + } + + decrypted, err := r.encrypter.Decrypt(encrypted) + if err != nil { + return Input{}, fmt.Errorf("failed to decrypt job input: %w", err) + } + + var input Input + + err = json.NewDecoder(strings.NewReader(decrypted)).Decode(&input) + if err != nil { + return Input{}, fmt.Errorf("failed to unmarshal job input json: %w", err) + } + + return input, nil +} + // Handle is repository import background job handler. func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressReporter) (string, error) { systemPrincipal := bootstrap.NewSystemServiceSession().Principal - var input Input - if err := json.NewDecoder(strings.NewReader(data)).Decode(&input); err != nil { - return "", fmt.Errorf("failed to unmarshal job input: %w", err) + input, err := r.getJobInput(data) + if err != nil { + return "", err } if input.CloneURL == "" { diff --git a/internal/services/importer/wire.go b/internal/services/importer/wire.go index 44328b4bf..cdeb81623 100644 --- a/internal/services/importer/wire.go +++ b/internal/services/importer/wire.go @@ -5,6 +5,7 @@ package importer import ( + "github.com/harness/gitness/encrypt" "github.com/harness/gitness/gitrpc" "github.com/harness/gitness/internal/services/job" "github.com/harness/gitness/internal/store" @@ -23,6 +24,7 @@ func ProvideRepoImporter( urlProvider *url.Provider, git gitrpc.Interface, repoStore store.RepoStore, + encrypter encrypt.Encrypter, scheduler *job.Scheduler, executor *job.Executor, ) (*Repository, error) { @@ -31,6 +33,7 @@ func ProvideRepoImporter( urlProvider: urlProvider, git: git, repoStore: repoStore, + encrypter: encrypter, scheduler: scheduler, } diff --git a/internal/services/pullreq/handlers_mergeable.go b/internal/services/pullreq/handlers_mergeable.go index 4fe14c6ca..54262e350 100644 --- a/internal/services/pullreq/handlers_mergeable.go +++ b/internal/services/pullreq/handlers_mergeable.go @@ -106,6 +106,28 @@ func (s *Service) deleteMergeRef(ctx context.Context, repoID int64, prNum int64) return nil } +// UpdateMergeDataIfRequired rechecks the merge data of a PR. +// TODO: This is a temporary solution - doesn't fix changed merge-base or other things. +func (s *Service) UpdateMergeDataIfRequired( + ctx context.Context, + repoID int64, + prNum int64, +) error { + pr, err := s.pullreqStore.FindByNumber(ctx, repoID, prNum) + if err != nil { + return fmt.Errorf("failed to get pull request number %d: %w", prNum, err) + } + + // nothing to-do if check was already performed + if pr.MergeCheckStatus != enum.MergeCheckStatusUnchecked { + return nil + } + + // WARNING: This CAN lead to two (or more) merge-checks on the same SHA + // running on different machines at the same time. + return s.updateMergeDataInner(ctx, pr, "", pr.SourceSHA) +} + //nolint:funlen // refactor if required. func (s *Service) updateMergeData( ctx context.Context, @@ -114,21 +136,31 @@ func (s *Service) updateMergeData( oldSHA string, newSHA string, ) error { - // TODO: Merge check should not update the merge base. - // TODO: Instead it should accept it as an argument and fail if it doesn't match. - // Then is would not longer be necessary to cancel already active mergeability checks. - pr, err := s.pullreqStore.FindByNumber(ctx, repoID, prNum) if err != nil { return fmt.Errorf("failed to get pull request number %d: %w", prNum, err) } + return s.updateMergeDataInner(ctx, pr, oldSHA, newSHA) +} + +//nolint:funlen // refactor if required. +func (s *Service) updateMergeDataInner( + ctx context.Context, + pr *types.PullReq, + oldSHA string, + newSHA string, +) error { + // TODO: Merge check should not update the merge base. + // TODO: Instead it should accept it as an argument and fail if it doesn't match. + // Then is would not longer be necessary to cancel already active mergeability checks. + if pr.State != enum.PullReqStateOpen { - return fmt.Errorf("cannot do mergability check on closed PR %d", prNum) + return fmt.Errorf("cannot do mergability check on closed PR %d", pr.Number) } // cancel all previous mergability work for this PR based on oldSHA - if err = s.pubsub.Publish(ctx, cancelMergeCheckKey, []byte(oldSHA), + if err := s.pubsub.Publish(ctx, cancelMergeCheckKey, []byte(oldSHA), pubsub.WithPublishNamespace("pullreq")); err != nil { return err } @@ -137,6 +169,13 @@ func (s *Service) updateMergeData( ctx, cancel = context.WithCancel(ctx) s.cancelMutex.Lock() + // NOTE: Temporary workaround to avoid overwriting existing cancel method on same machine. + // This doesn't avoid same SHA running on multiple machines + if _, ok := s.cancelMergeability[newSHA]; ok { + s.cancelMutex.Unlock() + cancel() + return nil + } s.cancelMergeability[newSHA] = cancel s.cancelMutex.Unlock() @@ -175,7 +214,7 @@ func (s *Service) updateMergeData( HeadRepoUID: sourceRepo.GitUID, HeadBranch: pr.SourceBranch, RefType: gitrpcenum.RefTypePullReqMerge, - RefName: strconv.Itoa(int(prNum)), + RefName: strconv.Itoa(int(pr.Number)), HeadExpectedSHA: newSHA, Force: true, diff --git a/internal/services/trigger/handler_branch.go b/internal/services/trigger/handler_branch.go index ff9207ef5..07b54dcd0 100644 --- a/internal/services/trigger/handler_branch.go +++ b/internal/services/trigger/handler_branch.go @@ -6,17 +6,76 @@ package trigger import ( "context" + "fmt" + "strings" "github.com/harness/gitness/events" gitevents "github.com/harness/gitness/internal/events/git" + "github.com/harness/gitness/internal/pipeline/triggerer" + "github.com/harness/gitness/types/enum" ) +// TODO: This can be moved to SCM library +func ExtractBranch(ref string) string { + return strings.TrimPrefix(ref, "refs/heads/") +} + func (s *Service) handleEventBranchCreated(ctx context.Context, event *events.Event[*gitevents.BranchCreatedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionBranchCreated, + Ref: event.Payload.Ref, + Source: ExtractBranch(event.Payload.Ref), + Target: ExtractBranch(event.Payload.Ref), + After: event.Payload.SHA, + } + err := s.augmentCommitInfo(ctx, hook, event.Payload.RepoID, event.Payload.SHA) + if err != nil { + return fmt.Errorf("could not augment commit info: %w", err) + } + return s.trigger(ctx, event.Payload.RepoID, enum.TriggerActionBranchCreated, hook) } func (s *Service) handleEventBranchUpdated(ctx context.Context, event *events.Event[*gitevents.BranchUpdatedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionBranchUpdated, + Ref: event.Payload.Ref, + Before: event.Payload.OldSHA, + After: event.Payload.NewSHA, + Source: ExtractBranch(event.Payload.Ref), + Target: ExtractBranch(event.Payload.Ref), + } + err := s.augmentCommitInfo(ctx, hook, event.Payload.RepoID, event.Payload.NewSHA) + if err != nil { + return fmt.Errorf("could not augment commit info: %w", err) + } + return s.trigger(ctx, event.Payload.RepoID, enum.TriggerActionBranchUpdated, hook) +} + +// augmentCommitInfo adds information about the commit to the hook by interacting with +// the commit service. +func (s *Service) augmentCommitInfo( + ctx context.Context, + hook *triggerer.Hook, + repoID int64, + sha string, +) error { + repo, err := s.repoStore.Find(ctx, repoID) + if err != nil { + return fmt.Errorf("could not find repo: %w", err) + } + commit, err := s.commitSvc.FindCommit(ctx, repo, sha) + if err != nil { + return fmt.Errorf("could not find commit info") + } + hook.AuthorName = commit.Author.Identity.Name + hook.Title = commit.Title + hook.Timestamp = commit.Committer.When.UnixMilli() + hook.AuthorLogin = commit.Author.Identity.Name + hook.AuthorEmail = commit.Author.Identity.Email + hook.Message = commit.Message + return nil } diff --git a/internal/services/trigger/handler_pullreq.go b/internal/services/trigger/handler_pullreq.go index 7f209d85a..fd65444de 100644 --- a/internal/services/trigger/handler_pullreq.go +++ b/internal/services/trigger/handler_pullreq.go @@ -6,22 +6,79 @@ package trigger import ( "context" + "fmt" "github.com/harness/gitness/events" pullreqevents "github.com/harness/gitness/internal/events/pullreq" + "github.com/harness/gitness/internal/pipeline/triggerer" + "github.com/harness/gitness/types/enum" + + "github.com/drone/go-scm/scm" ) func (s *Service) handleEventPullReqCreated(ctx context.Context, event *events.Event[*pullreqevents.CreatedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionPullReqCreated, + After: event.Payload.SourceSHA, + } + err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID) + if err != nil { + return fmt.Errorf("could not augment pull request info: %w", err) + } + return s.trigger(ctx, event.Payload.SourceRepoID, enum.TriggerActionPullReqCreated, hook) } func (s *Service) handleEventPullReqReopened(ctx context.Context, event *events.Event[*pullreqevents.ReopenedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionPullReqReopened, + After: event.Payload.SourceSHA, + } + err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID) + if err != nil { + return fmt.Errorf("could not augment pull request info: %w", err) + } + return s.trigger(ctx, event.Payload.SourceRepoID, enum.TriggerActionPullReqReopened, hook) } func (s *Service) handleEventPullReqBranchUpdated(ctx context.Context, event *events.Event[*pullreqevents.BranchUpdatedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionPullReqBranchUpdated, + After: event.Payload.NewSHA, + } + err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID) + if err != nil { + return fmt.Errorf("could not augment pull request info: %w", err) + } + return s.trigger(ctx, event.Payload.SourceRepoID, enum.TriggerActionPullReqBranchUpdated, hook) +} + +// augmentPullReqInfo adds in information into the hook pertaining to the pull request +// by querying the database. +func (s *Service) augmentPullReqInfo( + ctx context.Context, + hook *triggerer.Hook, + pullReqID int64, +) error { + pullreq, err := s.pullReqStore.Find(ctx, pullReqID) + if err != nil { + return fmt.Errorf("could not find pull request: %w", err) + } + hook.Title = pullreq.Title + hook.Timestamp = pullreq.Created + hook.AuthorLogin = pullreq.Author.UID + hook.AuthorName = pullreq.Author.DisplayName + hook.AuthorEmail = pullreq.Author.Email + hook.Message = pullreq.Description + hook.Before = pullreq.MergeBaseSHA + hook.Target = pullreq.TargetBranch + hook.Source = pullreq.SourceBranch + // expand the branch to a git reference. + hook.Ref = scm.ExpandRef(pullreq.SourceBranch, "refs/heads") + return nil } diff --git a/internal/services/trigger/handler_tag.go b/internal/services/trigger/handler_tag.go index c69c76809..b96c523fb 100644 --- a/internal/services/trigger/handler_tag.go +++ b/internal/services/trigger/handler_tag.go @@ -6,17 +6,46 @@ package trigger import ( "context" + "fmt" "github.com/harness/gitness/events" gitevents "github.com/harness/gitness/internal/events/git" + "github.com/harness/gitness/internal/pipeline/triggerer" + "github.com/harness/gitness/types/enum" ) func (s *Service) handleEventTagCreated(ctx context.Context, event *events.Event[*gitevents.TagCreatedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionTagCreated, + Ref: event.Payload.Ref, + Before: event.Payload.SHA, + After: event.Payload.SHA, + Source: event.Payload.Ref, + Target: event.Payload.Ref, + } + err := s.augmentCommitInfo(ctx, hook, event.Payload.RepoID, event.Payload.SHA) + if err != nil { + return fmt.Errorf("could not augment commit info: %w", err) + } + return s.trigger(ctx, event.Payload.RepoID, enum.TriggerActionTagCreated, hook) } func (s *Service) handleEventTagUpdated(ctx context.Context, event *events.Event[*gitevents.TagUpdatedPayload]) error { - return events.NewDiscardEventErrorf("not implemented") + hook := &triggerer.Hook{ + Trigger: enum.TriggerHook, + Action: enum.TriggerActionTagUpdated, + Ref: event.Payload.Ref, + Before: event.Payload.OldSHA, + After: event.Payload.NewSHA, + Source: event.Payload.Ref, + Target: event.Payload.Ref, + } + err := s.augmentCommitInfo(ctx, hook, event.Payload.RepoID, event.Payload.NewSHA) + if err != nil { + return fmt.Errorf("could not augment commit info: %w", err) + } + return s.trigger(ctx, event.Payload.RepoID, enum.TriggerActionTagUpdated, hook) } diff --git a/internal/services/trigger/service.go b/internal/services/trigger/service.go index c2e58a5a6..4a89155a9 100644 --- a/internal/services/trigger/service.go +++ b/internal/services/trigger/service.go @@ -13,7 +13,14 @@ import ( "github.com/harness/gitness/events" gitevents "github.com/harness/gitness/internal/events/git" pullreqevents "github.com/harness/gitness/internal/events/pullreq" + "github.com/harness/gitness/internal/pipeline/commit" + "github.com/harness/gitness/internal/pipeline/triggerer" + "github.com/harness/gitness/internal/store" "github.com/harness/gitness/stream" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/hashicorp/go-multierror" ) const ( @@ -43,11 +50,24 @@ func (c *Config) Prepare() error { return nil } -type Service struct{} +type Service struct { + triggerStore store.TriggerStore + pullReqStore store.PullReqStore + repoStore store.RepoStore + pipelineStore store.PipelineStore + triggerSvc triggerer.Triggerer + commitSvc commit.CommitService +} func New( ctx context.Context, config Config, + triggerStore store.TriggerStore, + pullReqStore store.PullReqStore, + repoStore store.RepoStore, + pipelineStore store.PipelineStore, + triggerSvc triggerer.Triggerer, + commitSvc commit.CommitService, gitReaderFactory *events.ReaderFactory[*gitevents.Reader], pullreqEvReaderFactory *events.ReaderFactory[*pullreqevents.Reader], ) (*Service, error) { @@ -55,7 +75,14 @@ func New( return nil, fmt.Errorf("provided trigger service config is invalid: %w", err) } - service := &Service{} + service := &Service{ + triggerStore: triggerStore, + pullReqStore: pullReqStore, + repoStore: repoStore, + commitSvc: commitSvc, + pipelineStore: pipelineStore, + triggerSvc: triggerSvc, + } _, err := gitReaderFactory.Launch(ctx, eventsReaderGroupName, config.EventReaderName, func(r *gitevents.Reader) error { @@ -101,3 +128,40 @@ func New( return service, nil } + +// trigger a build given an action on a repo and a hook. +// It tries to find all enabled triggers, see if the action is the same +// as the trigger action - and if so, find the pipeline for the trigger +// and fire an execution. +func (s *Service) trigger(ctx context.Context, repoID int64, + action enum.TriggerAction, hook *triggerer.Hook) error { + // Get all enabled triggers for a repo. + ret, err := s.triggerStore.ListAllEnabled(ctx, repoID) + if err != nil { + return fmt.Errorf("failed to list all enabled triggers: %w", err) + } + validTriggers := []*types.Trigger{} + // Check which triggers are eligible to be fired + for _, t := range ret { + for _, a := range t.Actions { + if a == action { + validTriggers = append(validTriggers, t) + break + } + } + } + + var errs error + for _, t := range validTriggers { + pipeline, err := s.pipelineStore.Find(ctx, t.PipelineID) + if err != nil { + errs = multierror.Append(errs, err) + } + + _, err = s.triggerSvc.Trigger(ctx, pipeline, hook) + if err != nil { + errs = multierror.Append(errs, err) + } + } + return errs +} diff --git a/internal/services/trigger/wire.go b/internal/services/trigger/wire.go index fab566b95..3c11aface 100644 --- a/internal/services/trigger/wire.go +++ b/internal/services/trigger/wire.go @@ -10,6 +10,9 @@ import ( "github.com/harness/gitness/events" gitevents "github.com/harness/gitness/internal/events/git" pullreqevents "github.com/harness/gitness/internal/events/pullreq" + "github.com/harness/gitness/internal/pipeline/commit" + "github.com/harness/gitness/internal/pipeline/triggerer" + "github.com/harness/gitness/internal/store" "github.com/google/wire" ) @@ -21,8 +24,15 @@ var WireSet = wire.NewSet( func ProvideService( ctx context.Context, config Config, + triggerStore store.TriggerStore, + commitSvc commit.CommitService, + pullReqStore store.PullReqStore, + repoStore store.RepoStore, + pipelineStore store.PipelineStore, + triggerSvc triggerer.Triggerer, gitReaderFactory *events.ReaderFactory[*gitevents.Reader], pullReqEvFactory *events.ReaderFactory[*pullreqevents.Reader], ) (*Service, error) { - return New(ctx, config, gitReaderFactory, pullReqEvFactory) + return New(ctx, config, triggerStore, pullReqStore, repoStore, pipelineStore, triggerSvc, + commitSvc, gitReaderFactory, pullReqEvFactory) } diff --git a/internal/store/database.go b/internal/store/database.go index 954c5b0dc..d9058cc0e 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -694,6 +694,10 @@ type ( // Count the number of triggers in a pipeline. Count(ctx context.Context, pipelineID int64, filter types.ListQueryFilter) (int64, error) + + // ListAllEnabled lists all enabled triggers for a given repo without pagination. + // It's used only internally to trigger builds. + ListAllEnabled(ctx context.Context, repoID int64) ([]*types.Trigger, error) } PluginStore interface { diff --git a/internal/store/database/migrate/ci/ci_migrations.sql b/internal/store/database/migrate/ci/ci_migrations.sql index c580d0059..8ccd773d3 100644 --- a/internal/store/database/migrate/ci/ci_migrations.sql +++ b/internal/store/database/migrate/ci/ci_migrations.sql @@ -261,8 +261,13 @@ CREATE TABLE templates ( CREATE TABLE triggers ( trigger_id INTEGER PRIMARY KEY AUTOINCREMENT ,trigger_uid TEXT NOT NULL - ,trigger_description TEXT NOT NULL ,trigger_pipeline_id INTEGER NOT NULL + ,trigger_repo_id INTEGER NOT NULL + ,trigger_secret TEXT NOT NULL + ,trigger_description TEXT NOT NULL + ,trigger_enabled BOOLEAN NOT NULL + ,trigger_created_by INTEGER NOT NULL + ,trigger_actions TEXT NOT NULL ,trigger_created INTEGER NOT NULL ,trigger_updated INTEGER NOT NULL ,trigger_version INTEGER NOT NULL @@ -270,11 +275,17 @@ CREATE TABLE triggers ( -- Ensure unique combination of pipeline ID and UID ,UNIQUE (trigger_pipeline_id, trigger_uid) - -- Foreign key to spaces table + -- Foreign key to pipelines table ,CONSTRAINT fk_triggers_pipeline_id FOREIGN KEY (trigger_pipeline_id) REFERENCES pipelines (pipeline_id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE + + -- Foreign key to repositories table + ,CONSTRAINT fk_triggers_repo_id FOREIGN KEY (trigger_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE ); CREATE TABLE plugins ( diff --git a/internal/store/database/trigger.go b/internal/store/database/trigger.go index f7a3164e8..b0c61c39a 100644 --- a/internal/store/database/trigger.go +++ b/internal/store/database/trigger.go @@ -6,6 +6,7 @@ package database import ( "context" + "encoding/json" "fmt" "strings" "time" @@ -15,13 +16,82 @@ import ( "github.com/harness/gitness/store/database" "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" "github.com/jmoiron/sqlx" + sqlxtypes "github.com/jmoiron/sqlx/types" "github.com/pkg/errors" ) var _ store.TriggerStore = (*triggerStore)(nil) +type trigger struct { + ID int64 `db:"trigger_id"` + UID string `db:"trigger_uid"` + Description string `db:"trigger_description"` + Secret string `db:"trigger_secret"` + PipelineID int64 `db:"trigger_pipeline_id"` + RepoID int64 `db:"trigger_repo_id"` + CreatedBy int64 `db:"trigger_created_by"` + Enabled bool `db:"trigger_enabled"` + Actions sqlxtypes.JSONText `db:"trigger_actions"` + Created int64 `db:"trigger_created"` + Updated int64 `db:"trigger_updated"` + Version int64 `db:"trigger_version"` +} + +func mapInternalToTrigger(trigger *trigger) (*types.Trigger, error) { + var actions []enum.TriggerAction + err := json.Unmarshal(trigger.Actions, &actions) + if err != nil { + return nil, errors.Wrap(err, "could not unmarshal trigger.actions") + } + + return &types.Trigger{ + ID: trigger.ID, + Description: trigger.Description, + Secret: trigger.Secret, + PipelineID: trigger.PipelineID, + RepoID: trigger.RepoID, + CreatedBy: trigger.CreatedBy, + Enabled: trigger.Enabled, + Actions: actions, + UID: trigger.UID, + Created: trigger.Created, + Updated: trigger.Updated, + Version: trigger.Version, + }, nil +} + +func mapInternalToTriggerList(triggers []*trigger) ([]*types.Trigger, error) { + ret := make([]*types.Trigger, len(triggers)) + for i, t := range triggers { + trigger, err := mapInternalToTrigger(t) + if err != nil { + return nil, err + } + ret[i] = trigger + } + return ret, nil +} + +func mapTriggerToInternal(t *types.Trigger) *trigger { + return &trigger{ + ID: t.ID, + UID: t.UID, + Description: t.Description, + PipelineID: t.PipelineID, + Secret: t.Secret, + RepoID: t.RepoID, + CreatedBy: t.CreatedBy, + Enabled: t.Enabled, + Actions: EncodeToSQLXJSON(t.Actions), + Created: t.Created, + Updated: t.Updated, + Version: t.Version, + } +} + // NewTriggerStore returns a new TriggerStore. func NewTriggerStore(db *sqlx.DB) *triggerStore { return &triggerStore{ @@ -37,6 +107,8 @@ const ( triggerColumns = ` trigger_id ,trigger_uid + ,trigger_enabled + ,trigger_actions ,trigger_description ,trigger_pipeline_id ,trigger_created @@ -53,33 +125,44 @@ func (s *triggerStore) FindByUID(ctx context.Context, pipelineID int64, uid stri WHERE trigger_pipeline_id = $1 AND trigger_uid = $2` db := dbtx.GetAccessor(ctx, s.db) - dst := new(types.Trigger) + dst := new(trigger) if err := db.GetContext(ctx, dst, findQueryStmt, pipelineID, uid); err != nil { return nil, database.ProcessSQLErrorf(err, "Failed to find trigger") } - return dst, nil + return mapInternalToTrigger(dst) } // Create creates a new trigger in the datastore. -func (s *triggerStore) Create(ctx context.Context, trigger *types.Trigger) error { +func (s *triggerStore) Create(ctx context.Context, t *types.Trigger) error { const triggerInsertStmt = ` INSERT INTO triggers ( trigger_uid ,trigger_description + ,trigger_actions + ,trigger_enabled + ,trigger_secret + ,trigger_created_by ,trigger_pipeline_id + ,trigger_repo_id ,trigger_created ,trigger_updated ,trigger_version ) VALUES ( :trigger_uid ,:trigger_description + ,:trigger_actions + ,:trigger_enabled + ,:trigger_secret + ,:trigger_created_by ,:trigger_pipeline_id + ,:trigger_repo_id ,:trigger_created ,:trigger_updated ,:trigger_version ) RETURNING trigger_id` db := dbtx.GetAccessor(ctx, s.db) + trigger := mapTriggerToInternal(t) query, arg, err := db.BindNamed(triggerInsertStmt, trigger) if err != nil { return database.ProcessSQLErrorf(err, "Failed to bind trigger object") @@ -93,17 +176,18 @@ func (s *triggerStore) Create(ctx context.Context, trigger *types.Trigger) error } // Update tries to update an trigger in the datastore with optimistic locking. -func (s *triggerStore) Update(ctx context.Context, e *types.Trigger) error { +func (s *triggerStore) Update(ctx context.Context, t *types.Trigger) error { const triggerUpdateStmt = ` UPDATE triggers SET trigger_uid = :trigger_uid ,trigger_description = :trigger_description ,trigger_updated = :trigger_updated + ,trigger_actions = :trigger_actions ,trigger_version = :trigger_version WHERE trigger_id = :trigger_id AND trigger_version = :trigger_version - 1` updatedAt := time.Now() - trigger := *e + trigger := mapTriggerToInternal(t) trigger.Version++ trigger.Updated = updatedAt.UnixMilli() @@ -129,8 +213,8 @@ func (s *triggerStore) Update(ctx context.Context, e *types.Trigger) error { return gitness_store.ErrVersionConflict } - e.Version = trigger.Version - e.Updated = trigger.Updated + t.Version = trigger.Version + t.Updated = trigger.Updated return nil } @@ -186,12 +270,37 @@ func (s *triggerStore) List( db := dbtx.GetAccessor(ctx, s.db) - dst := []*types.Trigger{} + dst := []*trigger{} if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") } - return dst, nil + return mapInternalToTriggerList(dst) +} + +// ListAllEnabled lists all enabled triggers for a given repo without pagination +func (s *triggerStore) ListAllEnabled( + ctx context.Context, + repoID int64, +) ([]*types.Trigger, error) { + stmt := database.Builder. + Select(triggerColumns). + From("triggers"). + Where("trigger_repo_id = ? AND trigger_enabled = true", fmt.Sprint(repoID)) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + dst := []*trigger{} + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") + } + + return mapInternalToTriggerList(dst) } // Count of triggers under a given pipeline. diff --git a/types/enum/trigger_actions.go b/types/enum/trigger_actions.go new file mode 100644 index 000000000..2ab492275 --- /dev/null +++ b/types/enum/trigger_actions.go @@ -0,0 +1,60 @@ +package enum + +// TriggerAction defines the different actions on triggers will fire. +type TriggerAction string + +// These are similar to enums defined in webhook enum but can diverge +// as these are different entities. +const ( + // TriggerActionBranchCreated gets triggered when a branch gets created. + TriggerActionBranchCreated TriggerAction = "branch_created" + // TriggerActionBranchUpdated gets triggered when a branch gets updated. + TriggerActionBranchUpdated TriggerAction = "branch_updated" + + // TriggerActionTagCreated gets triggered when a tag gets created. + TriggerActionTagCreated TriggerAction = "tag_created" + // TriggerActionTagUpdated gets triggered when a tag gets updated. + TriggerActionTagUpdated TriggerAction = "tag_updated" + + // TriggerActionPullReqCreated gets triggered when a pull request gets created. + TriggerActionPullReqCreated TriggerAction = "pullreq_created" + // TriggerActionPullReqReopened gets triggered when a pull request gets reopened. + TriggerActionPullReqReopened TriggerAction = "pullreq_reopened" + // TriggerActionPullReqBranchUpdated gets triggered when a pull request source branch gets updated. + TriggerActionPullReqBranchUpdated TriggerAction = "pullreq_branch_updated" +) + +func (TriggerAction) Enum() []interface{} { return toInterfaceSlice(triggerActions) } +func (s TriggerAction) Sanitize() (TriggerAction, bool) { return Sanitize(s, GetAllTriggerActions) } +func (t TriggerAction) GetTriggerEvent() TriggerEvent { + if t == TriggerActionPullReqCreated || + t == TriggerActionPullReqBranchUpdated || + t == TriggerActionPullReqReopened { + return TriggerEventPullRequest + } else if t == TriggerActionTagCreated || t == TriggerActionTagUpdated { + return TriggerEventTag + } else if t == "" { + return TriggerEventManual + } + return TriggerEventPush +} + +func GetAllTriggerActions() ([]TriggerAction, TriggerAction) { + return triggerActions, "" // No default value +} + +var triggerActions = sortEnum([]TriggerAction{ + TriggerActionBranchCreated, + TriggerActionBranchUpdated, + TriggerActionTagCreated, + TriggerActionTagUpdated, + TriggerActionPullReqCreated, + TriggerActionPullReqReopened, + TriggerActionPullReqBranchUpdated, +}) + +// Trigger types +const ( + TriggerHook = "@hook" + TriggerCron = "@cron" +) diff --git a/types/enum/trigger_events.go b/types/enum/trigger_events.go index df1090c50..a00f84f81 100644 --- a/types/enum/trigger_events.go +++ b/types/enum/trigger_events.go @@ -4,13 +4,14 @@ package enum +// TriggerEvent defines the different kinds of events in triggers. +type TriggerEvent string + // Hook event constants. const ( TriggerEventCron = "cron" - TriggerEventCustom = "custom" + TriggerEventManual = "manual" TriggerEventPush = "push" TriggerEventPullRequest = "pull_request" TriggerEventTag = "tag" - TriggerEventPromote = "promote" - TriggerEventRollback = "rollback" ) diff --git a/types/trigger.go b/types/trigger.go index 81a6ebccc..c6c2ac28c 100644 --- a/types/trigger.go +++ b/types/trigger.go @@ -4,12 +4,19 @@ package types +import "github.com/harness/gitness/types/enum" + type Trigger struct { - ID int64 `db:"trigger_id" json:"id"` - Description string `db:"trigger_description" json:"description"` - PipelineID int64 `db:"trigger_pipeline_id" json:"pipeline_id"` - UID string `db:"trigger_uid" json:"uid"` - Created int64 `db:"trigger_created" json:"created"` - Updated int64 `db:"trigger_updated" json:"updated"` - Version int64 `db:"trigger_version" json:"-"` + ID int64 `json:"id"` + Description string `json:"description"` + PipelineID int64 `json:"pipeline_id"` + Secret string `json:"-"` + RepoID int64 `json:"repo_id"` + CreatedBy int64 `json:"created_by"` + Enabled bool `json:"enabled"` + Actions []enum.TriggerAction `json:"actions"` + UID string `json:"uid"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + Version int64 `json:"-"` } diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js index 5148930c1..919873364 100644 --- a/web/config/moduleFederation.config.js +++ b/web/config/moduleFederation.config.js @@ -35,6 +35,7 @@ module.exports = { './Settings': './src/pages/RepositorySettings/RepositorySettings.tsx', './Webhooks': './src/pages/Webhooks/Webhooks.tsx', './WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx', + './Search': './src/pages/Search/Search.tsx', './WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx', './NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx' }, diff --git a/web/config/webpack.common.js b/web/config/webpack.common.js index e9e6cd9e8..b4561a0e6 100644 --- a/web/config/webpack.common.js +++ b/web/config/webpack.common.js @@ -221,6 +221,18 @@ module.exports = { 'vb', 'xml', 'yaml' + ], + globalAPI: true, + filename: '[name].worker.[contenthash:6].js', + customLanguages: [ + { + label: 'yaml', + entry: 'monaco-yaml', + worker: { + id: 'monaco-yaml/yamlWorker', + entry: 'monaco-yaml/yaml.worker' + } + } ] }) ] diff --git a/web/package.json b/web/package.json index 78e9e44be..adab8327c 100644 --- a/web/package.json +++ b/web/package.json @@ -65,6 +65,7 @@ "moment": "^2.25.3", "monaco-editor": "^0.40.0", "monaco-editor-webpack-plugin": "^7.1.0", + "monaco-yaml": "^4.0.4", "qs": "^6.9.4", "react": "^17.0.2", "react-complex-tree": "^1.1.11", diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts index 65d79cc0b..1a7000138 100644 --- a/web/src/RouteDefinitions.ts +++ b/web/src/RouteDefinitions.ts @@ -46,7 +46,7 @@ export interface CODERoutes { toCODESpaceAccessControl: (args: Required>) => string toCODESpaceSettings: (args: Required>) => string toCODEPipelines: (args: Required>) => string - toCODEPipelinesNew: (args: Required>) => string + toCODEPipelineEdit: (args: Required>) => string toCODESecrets: (args: Required>) => string toCODEGlobalSettings: () => string @@ -73,7 +73,7 @@ export interface CODERoutes { toCODEWebhookNew: (args: Required>) => string toCODEWebhookDetails: (args: Required>) => string toCODESettings: (args: Required>) => string - + toCODESearch: (args: Required>) => string toCODEExecutions: (args: Required>) => string toCODEExecution: (args: Required>) => string toCODESecret: (args: Required>) => string @@ -97,7 +97,7 @@ export const routes: CODERoutes = { toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`, toCODESpaceSettings: ({ space }) => `/settings/${space}`, toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`, - toCODEPipelinesNew: ({ space }) => `/pipelines/${space}/new`, + toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`, toCODESecrets: ({ space }) => `/secrets/${space}`, toCODEGlobalSettings: () => '/settings', @@ -126,6 +126,7 @@ export const routes: CODERoutes = { toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`, toCODETags: ({ repoPath }) => `/${repoPath}/tags`, toCODESettings: ({ repoPath }) => `/${repoPath}/settings`, + toCODESearch: ({ repoPath }) => `/${repoPath}/search`, toCODEWebhooks: ({ repoPath }) => `/${repoPath}/webhooks`, toCODEWebhookNew: ({ repoPath }) => `/${repoPath}/webhooks/new`, toCODEWebhookDetails: ({ repoPath, webhookId }) => `/${repoPath}/webhook/${webhookId}`, diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index d7cef09bb..60ec8dd21 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -27,16 +27,17 @@ import ChangePassword from 'pages/ChangePassword/ChangePassword' import SpaceAccessControl from 'pages/SpaceAccessControl/SpaceAccessControl' import SpaceSettings from 'pages/SpaceSettings/SpaceSettings' import { useStrings } from 'framework/strings' -import { useFeatureFlag } from 'hooks/useFeatureFlag' import ExecutionList from 'pages/ExecutionList/ExecutionList' import Execution from 'pages/Execution/Execution' import Secret from 'pages/Secret/Secret' -import NewPipeline from 'pages/NewPipeline/NewPipeline' +import Search from 'pages/Search/Search' +import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline' +import { useAppContext } from 'AppContext' export const RouteDestinations: React.FC = React.memo(function RouteDestinations() { const { getString } = useStrings() const repoPath = `${pathProps.space}/${pathProps.repoName}` - const { OPEN_SOURCE_PIPELINES, OPEN_SOURCE_SECRETS } = useFeatureFlag() + const { standalone } = useAppContext() return ( @@ -163,7 +164,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations - {OPEN_SOURCE_PIPELINES && ( + {standalone && ( )} - {OPEN_SOURCE_PIPELINES && ( + {standalone && ( @@ -185,15 +186,15 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations )} - {OPEN_SOURCE_PIPELINES && ( - + {standalone && ( + - + )} - {OPEN_SOURCE_PIPELINES && ( + {standalone && ( @@ -201,7 +202,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations )} - {OPEN_SOURCE_SECRETS && ( + {standalone && ( @@ -209,7 +210,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations )} - {OPEN_SOURCE_SECRETS && ( + {standalone && ( @@ -249,6 +250,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations + + + + + + = ({ stage, repoPath }) => { return (
+ {stage?.error && ( + + + {stage?.error} + + + )} diff --git a/web/src/components/ConsoleStep/ConsoleStep.module.scss b/web/src/components/ConsoleStep/ConsoleStep.module.scss index 465505cc1..3bf78fd03 100644 --- a/web/src/components/ConsoleStep/ConsoleStep.module.scss +++ b/web/src/components/ConsoleStep/ConsoleStep.module.scss @@ -20,3 +20,16 @@ transform: rotate(360deg); } } + +.timeoutIcon { + svg { + g { + circle { + fill: black !important; + } + path { + fill: white !important; + } + } + } +} diff --git a/web/src/components/ConsoleStep/ConsoleStep.module.scss.d.ts b/web/src/components/ConsoleStep/ConsoleStep.module.scss.d.ts index eb9e0ef6e..d4984fd99 100644 --- a/web/src/components/ConsoleStep/ConsoleStep.module.scss.d.ts +++ b/web/src/components/ConsoleStep/ConsoleStep.module.scss.d.ts @@ -3,3 +3,4 @@ export declare const loading: string export declare const spin: string export declare const stepLayout: string +export declare const timeoutIcon: string diff --git a/web/src/components/ConsoleStep/ConsoleStep.tsx b/web/src/components/ConsoleStep/ConsoleStep.tsx index 5742248f0..7f34b3df7 100644 --- a/web/src/components/ConsoleStep/ConsoleStep.tsx +++ b/web/src/components/ConsoleStep/ConsoleStep.tsx @@ -70,6 +70,10 @@ const ConsoleStep: FC = ({ step, stageNumber, repoPath, pipeli icon = } else if (step?.status === ExecutionState.RUNNING) { icon = + } else if (step?.status === ExecutionState.FAILURE) { + icon = + } else if (step?.status === ExecutionState.SKIPPED) { + icon = } else { icon = // Default icon in case of other statuses or unknown status } @@ -93,7 +97,7 @@ const ConsoleStep: FC = ({ step, stageNumber, repoPath, pipeli className={css.stepLayout} spacing="medium" onClick={() => { - if (!isPending) { + if (!isPending && step?.status !== ExecutionState.SKIPPED) { setIsOpened(!isOpened) if (shouldUseGet && !isOpened) refetch() } diff --git a/web/src/components/Editor/Editor.tsx b/web/src/components/Editor/Editor.tsx index c0d94bf0d..5c2b5ff59 100644 --- a/web/src/components/Editor/Editor.tsx +++ b/web/src/components/Editor/Editor.tsx @@ -11,7 +11,7 @@ import { EditorView, keymap, placeholder as placeholderExtension } from '@codemi import { Compartment, EditorState, Extension } from '@codemirror/state' import { color } from '@uiw/codemirror-extensions-color' import { hyperLink } from '@uiw/codemirror-extensions-hyper-link' -import { githubLight as theme } from '@uiw/codemirror-themes-all' +import { githubLight, githubDark } from '@uiw/codemirror-themes-all' import css from './Editor.module.scss' export interface EditorProps { @@ -28,6 +28,7 @@ export interface EditorProps { setDirty?: React.Dispatch> onChange?: (doc: Text, viewUpdate: ViewUpdate, isDirty: boolean) => void onViewUpdate?: (viewUpdate: ViewUpdate) => void + darkTheme?: boolean } export const Editor = React.memo(function CodeMirrorReactEditor({ @@ -43,8 +44,10 @@ export const Editor = React.memo(function CodeMirrorReactEditor({ viewRef, setDirty, onChange, - onViewUpdate + onViewUpdate, + darkTheme }: EditorProps) { + const contentRef = useRef(content) const view = useRef() const ref = useRef() const languageConfig = useMemo(() => new Compartment(), []) @@ -70,7 +73,7 @@ export const Editor = React.memo(function CodeMirrorReactEditor({ color, hyperLink, - theme, + darkTheme ? githubDark : githubLight, EditorView.lineWrapping, @@ -136,5 +139,14 @@ export const Editor = React.memo(function CodeMirrorReactEditor({ } }, [filename, forMarkdown, view, languageConfig, markdownLanguageSupport]) + useEffect(() => { + if (contentRef.current !== content) { + contentRef.current = content + viewRef?.current?.dispatch({ + changes: { from: 0, to: viewRef?.current?.state.doc.length, insert: content } + }) + } + }, [content, viewRef]) + return }) diff --git a/web/src/components/ExecutionPageHeader/ExecutionPageHeader.tsx b/web/src/components/ExecutionPageHeader/ExecutionPageHeader.tsx index 44cd71ba4..d5a30db3e 100644 --- a/web/src/components/ExecutionPageHeader/ExecutionPageHeader.tsx +++ b/web/src/components/ExecutionPageHeader/ExecutionPageHeader.tsx @@ -79,7 +79,7 @@ export function ExecutionPageHeader({ content={ executionInfo && ( - + {executionInfo.message} diff --git a/web/src/components/ExecutionStageList/ExecutionStageList.tsx b/web/src/components/ExecutionStageList/ExecutionStageList.tsx index 2b5090586..3a5098ea3 100644 --- a/web/src/components/ExecutionStageList/ExecutionStageList.tsx +++ b/web/src/components/ExecutionStageList/ExecutionStageList.tsx @@ -34,6 +34,7 @@ const ExecutionStage: FC = ({ stage, isSelected = false, se noBackground iconSize={18} className={css.statusIcon} + isCi /> {stage.name} diff --git a/web/src/components/ExecutionStatus/ExecutionStatus.module.scss b/web/src/components/ExecutionStatus/ExecutionStatus.module.scss index 01efc8f29..c11a459e8 100644 --- a/web/src/components/ExecutionStatus/ExecutionStatus.module.scss +++ b/web/src/components/ExecutionStatus/ExecutionStatus.module.scss @@ -32,6 +32,14 @@ --bgColor: transparent !important; } + &.waiting { + svg { + > circle { + fill: var(--orange-700) !important; + } + } + } + &.pending { --fgColor: var(--grey-600); --bgColor: var(--grey-100); @@ -48,6 +56,14 @@ } } + &.runningBlue { + svg { + > circle { + fill: var(--primary-6) !important; + } + } + } + &.success { --fgColor: var(--green-800); --bgColor: var(--green-50); diff --git a/web/src/components/ExecutionStatus/ExecutionStatus.module.scss.d.ts b/web/src/components/ExecutionStatus/ExecutionStatus.module.scss.d.ts index 40f8b815e..7cbd3420c 100644 --- a/web/src/components/ExecutionStatus/ExecutionStatus.module.scss.d.ts +++ b/web/src/components/ExecutionStatus/ExecutionStatus.module.scss.d.ts @@ -7,4 +7,6 @@ export declare const main: string export declare const noBackground: string export declare const pending: string export declare const running: string +export declare const runningBlue: string export declare const success: string +export declare const waiting: string diff --git a/web/src/components/ExecutionStatus/ExecutionStatus.tsx b/web/src/components/ExecutionStatus/ExecutionStatus.tsx index 40f1c3128..07f8f7b90 100644 --- a/web/src/components/ExecutionStatus/ExecutionStatus.tsx +++ b/web/src/components/ExecutionStatus/ExecutionStatus.tsx @@ -10,7 +10,8 @@ export enum ExecutionState { RUNNING = 'running', SUCCESS = 'success', FAILURE = 'failure', - ERROR = 'error' + ERROR = 'error', + SKIPPED = 'skipped' } interface ExecutionStatusProps { @@ -19,6 +20,7 @@ interface ExecutionStatusProps { noBackground?: boolean iconSize?: number className?: string + isCi?: boolean } export const ExecutionStatus: React.FC = ({ @@ -26,19 +28,20 @@ export const ExecutionStatus: React.FC = ({ iconSize = 20, iconOnly = false, noBackground = false, - className + className, + isCi = false }) => { const { getString } = useStrings() const maps = useMemo( () => ({ [ExecutionState.PENDING]: { - icon: 'ci-pending-build', - css: css.pending, + icon: isCi ? 'execution-waiting' : 'ci-pending-build', + css: isCi ? css.waiting : css.pending, title: getString('pending').toLocaleUpperCase() }, [ExecutionState.RUNNING]: { icon: 'running-filled', - css: css.running, + css: isCi ? css.runningBlue : css.running, title: getString('running').toLocaleUpperCase() }, [ExecutionState.SUCCESS]: { @@ -55,9 +58,14 @@ export const ExecutionStatus: React.FC = ({ icon: 'solid-error', css: css.error, title: getString('error').toLocaleUpperCase() + }, + [ExecutionState.SKIPPED]: { + icon: 'execution-timeout', + css: null, + title: getString('skipped').toLocaleUpperCase() } }), - [getString] + [getString, isCi] ) const map = useMemo(() => maps[status], [maps, status]) diff --git a/web/src/components/ExecutionText/ExecutionText.module.scss b/web/src/components/ExecutionText/ExecutionText.module.scss new file mode 100644 index 000000000..d716efe36 --- /dev/null +++ b/web/src/components/ExecutionText/ExecutionText.module.scss @@ -0,0 +1,21 @@ +.author { + color: var(--grey-500) !important; + font-weight: 600 !important; +} + +.hash { + color: var(--primary-7) !important; + font-family: Roboto Mono !important; + font-size: var(--font-size-small) !important; + font-weight: 500 !important; +} + +.pillContainer { + background-color: var(--grey-100) !important; + color: var(--grey-500) !important; + padding: 2px 4px !important; +} + +.pillText { + font-weight: 600 !important; +} diff --git a/web/src/components/ExecutionText/ExecutionText.module.scss.d.ts b/web/src/components/ExecutionText/ExecutionText.module.scss.d.ts new file mode 100644 index 000000000..89c04ec14 --- /dev/null +++ b/web/src/components/ExecutionText/ExecutionText.module.scss.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +// This is an auto-generated file +export declare const author: string +export declare const hash: string +export declare const pillContainer: string +export declare const pillText: string diff --git a/web/src/components/ExecutionText/ExecutionText.tsx b/web/src/components/ExecutionText/ExecutionText.tsx new file mode 100644 index 000000000..31a44e426 --- /dev/null +++ b/web/src/components/ExecutionText/ExecutionText.tsx @@ -0,0 +1,162 @@ +import React from 'react' +import { Avatar, Layout, Text, Utils } from '@harnessio/uicore' +import { Link } from 'react-router-dom' +import { GitCommit, GitFork, Label } from 'iconoir-react' +import { Color } from '@harnessio/design-system' +import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator' +import { useAppContext } from 'AppContext' +import type { EnumTriggerAction } from 'services/code' +import css from './ExecutionText.module.scss' + +export enum ExecutionTrigger { + CRON = 'cron', + MANUAL = 'manual', + PUSH = 'push', + PULL = 'pull_request', + TAG = 'tag' +} + +enum PillType { + BRANCH = 'branch', + TAG = 'tag', + COMMIT = 'commit' +} + +interface PillProps { + type: PillType + text: string +} + +interface ExecutionTextProps { + authorName: string + authorEmail: string + repoPath: string + commitRef: string + event: ExecutionTrigger + target: string + beforeRef: string + source: string + action: EnumTriggerAction | undefined +} + +const Pill: React.FC = ({ type, text }) => { + let Icon + + switch (type) { + case PillType.BRANCH: + Icon = GitFork + break + case PillType.TAG: + Icon = Label + break + case PillType.COMMIT: + Icon = GitCommit + break + default: + Icon = GitCommit + } + + return ( + + + + {text} + + + ) +} + +export const ExecutionText: React.FC = ({ + authorName, + authorEmail, + repoPath, + commitRef, + event, + target, + beforeRef, + source, + action +}) => { + const { routes } = useAppContext() + + let componentToRender + + switch (event) { + case ExecutionTrigger.CRON: + componentToRender = ( + + Triggered by CRON job + + ) + break + case ExecutionTrigger.MANUAL: + componentToRender = ( + {`${authorName} triggered manually`} + ) + break + case ExecutionTrigger.PUSH: + componentToRender = ( + <> + {`${authorName} pushed`} + + + to + + + + ) + break + case ExecutionTrigger.PULL: + componentToRender = ( + <> + {`${authorName} ${ + action === 'pullreq_reopened' ? 'reopened' : action === 'pullreq_branch_updated' ? 'updated' : 'created' + } pull request`} + + + to + + + + ) + break + case ExecutionTrigger.TAG: + componentToRender = ( + <> + {`${authorName} ${ + action === 'branch_updated' ? 'updated' : 'created' + }`} + + + ) + break + default: + componentToRender = ( + + Unknown trigger + + ) + } + + return ( + + + {componentToRender} + + { + e.stopPropagation() + }}> + {commitRef?.slice(0, 6)} + + + ) +} diff --git a/web/src/components/NewPipelineModal/NewPipelineModal.module.scss b/web/src/components/NewPipelineModal/NewPipelineModal.module.scss new file mode 100644 index 000000000..23d8222cf --- /dev/null +++ b/web/src/components/NewPipelineModal/NewPipelineModal.module.scss @@ -0,0 +1,17 @@ +.branchSelect { + :global { + .bp3-popover-wrapper { + width: 100% !important; + .bp3-popover-target { + width: 100% !important; + } + } + .bp3-button { + justify-content: start; + width: 100%; + } + .bp3-icon-chevron-down { + margin-left: auto; + } + } +} diff --git a/web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts b/web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts new file mode 100644 index 000000000..f9139a9e7 --- /dev/null +++ b/web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts @@ -0,0 +1,3 @@ +/* eslint-disable */ +// This is an auto-generated file +export declare const branchSelect: string diff --git a/web/src/components/NewPipelineModal/NewPipelineModal.tsx b/web/src/components/NewPipelineModal/NewPipelineModal.tsx new file mode 100644 index 000000000..99685a42b --- /dev/null +++ b/web/src/components/NewPipelineModal/NewPipelineModal.tsx @@ -0,0 +1,162 @@ +import React, { useMemo, useState } from 'react' +import { useHistory } from 'react-router-dom' +import { useMutate } from 'restful-react' +import * as yup from 'yup' +import { capitalize } from 'lodash-es' +import { + Button, + ButtonVariation, + Container, + Dialog, + FormInput, + Formik, + FormikForm, + Layout, + Text, + useToaster +} from '@harnessio/uicore' +import { FontVariation } from '@harnessio/design-system' +import { useModalHook } from 'hooks/useModalHook' +import type { OpenapiCreatePipelineRequest, TypesPipeline, TypesRepository } from 'services/code' +import { useStrings } from 'framework/strings' +import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect' +import { useAppContext } from 'AppContext' +import { getErrorMessage } from 'utils/Utils' +import { DEFAULT_YAML_PATH_PREFIX, DEFAULT_YAML_PATH_SUFFIX } from '../../pages/AddUpdatePipeline/Constants' + +import css from './NewPipelineModal.module.scss' + +interface FormData { + name: string + branch: string + yamlPath: string +} + +const useNewPipelineModal = () => { + const { routes } = useAppContext() + const { getString } = useStrings() + const history = useHistory() + const { showError } = useToaster() + const [repo, setRepo] = useState() + const repoPath = useMemo(() => repo?.path || '', [repo]) + + const { mutate: savePipeline } = useMutate({ + verb: 'POST', + path: `/api/v1/repos/${repoPath}/+/pipelines` + }) + + const handleCreatePipeline = (formData: FormData): void => { + const { name, branch, yamlPath } = formData + try { + const payload: OpenapiCreatePipelineRequest = { + config_path: yamlPath, + default_branch: branch, + uid: name + } + savePipeline(payload, { pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines` } }) + .then(() => { + hideModal() + history.push(routes.toCODEPipelineEdit({ repoPath, pipeline: name })) + }) + .catch(error => { + showError(getErrorMessage(error), 0, 'pipelines.failedToCreatePipeline') + }) + } catch (exception) { + showError(getErrorMessage(exception), 0, 'pipelines.failedToCreatePipeline') + } + } + + const [openModal, hideModal] = useModalHook(() => { + const onClose = () => { + hideModal() + } + return ( + + + initialValues={{ name: '', branch: repo?.default_branch || '', yamlPath: '' }} + formName="createNewPipeline" + enableReinitialize={true} + validationSchema={yup.object().shape({ + name: yup + .string() + .trim() + .required(`${getString('name')} ${getString('isRequired')}`), + branch: yup + .string() + .trim() + .required(`${getString('branch')} ${getString('isRequired')}`), + yamlPath: yup + .string() + .trim() + .required(`${getString('pipelines.yamlPath')} ${getString('isRequired')}`) + })} + validateOnChange + validateOnBlur + onSubmit={handleCreatePipeline}> + {formik => { + return ( + + + + { + const input = (event.target as HTMLInputElement)?.value + formik?.setFieldValue('name', input) + if (input) { + // Keeping minimal validation for now, this could be much more exhaustive + const path = input.trim().replace(/\s/g, '') + formik?.setFieldValue( + 'yamlPath', + DEFAULT_YAML_PATH_PREFIX.concat(path).concat(DEFAULT_YAML_PATH_SUFFIX) + ) + } + }} + /> + + {capitalize(getString('branch'))} + + { + formik?.setFieldValue('branch', ref) + }} + repoMetadata={repo || {}} + disableBranchCreation + disableViewAllBranches + forBranchesOnly + /> + + + + + + + ) + }, [repo]) + + return { + openModal: ({ repoMetadata }: { repoMetadata?: TypesRepository }) => { + setRepo(repoMetadata) + openModal() + }, + hideModal + } +} + +export default useNewPipelineModal diff --git a/web/src/components/PluginsPanel/PluginsPanel.module.scss b/web/src/components/PluginsPanel/PluginsPanel.module.scss new file mode 100644 index 000000000..d5d2232cd --- /dev/null +++ b/web/src/components/PluginsPanel/PluginsPanel.module.scss @@ -0,0 +1,53 @@ +.main { + height: 100%; + :global { + .bp3-tabs { + width: 100%; + height: 100%; + } + } +} + +.mainTabPanel { + &:global(.bp3-tab-panel[role='tabpanel']) { + height: calc(100% - 30px); + margin-top: 0 !important; + margin-bottom: 0 !important; + } +} + +.pluginDetailsPanel { + height: 100%; + border-top: 1px solid var(--grey-100); +} + +.pluginIcon { + background: var(--teal-200) !important; + border-radius: 5px; +} + +.plugin { + border: 1px solid var(--grey-100); + &:hover { + cursor: pointer; + } +} + +.form { + height: 100%; + width: 100%; + :global { + .FormikForm--main { + height: 100%; + & > div { + height: 100%; + } + } + } +} + +.arrow { + &:hover { + cursor: pointer; + } +} diff --git a/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts b/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts new file mode 100644 index 000000000..cc220df9e --- /dev/null +++ b/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +// This is an auto-generated file +export declare const arrow: string +export declare const form: string +export declare const main: string +export declare const mainTabPanel: string +export declare const plugin: string +export declare const pluginDetailsPanel: string +export declare const pluginIcon: string diff --git a/web/src/components/PluginsPanel/PluginsPanel.tsx b/web/src/components/PluginsPanel/PluginsPanel.tsx new file mode 100644 index 000000000..82002b53a --- /dev/null +++ b/web/src/components/PluginsPanel/PluginsPanel.tsx @@ -0,0 +1,301 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { Formik } from 'formik' +import { capitalize, get } from 'lodash-es' +import { useGet } from 'restful-react' +import { Color, FontVariation } from '@harnessio/design-system' +import { Icon, type IconName } from '@harnessio/icons' +import { Button, ButtonVariation, Container, FormInput, FormikForm, Layout, Tab, Tabs, Text } from '@harnessio/uicore' +import { useStrings } from 'framework/strings' +import { LIST_FETCHING_LIMIT } from 'utils/Utils' +import type { TypesPlugin } from 'services/code' +import { YamlVersion } from 'pages/AddUpdatePipeline/Constants' + +import css from './PluginsPanel.module.scss' + +enum PluginCategory { + Harness, + Drone +} + +enum PluginPanelView { + Category, + Listing, + Configuration +} + +interface PluginInterface { + category: PluginCategory + name: string + description: string + icon: IconName +} + +const PluginCategories: PluginInterface[] = [ + { + category: PluginCategory.Harness, + name: 'Run', + description: 'Run a script on macOS, Linux, or Windows', + icon: 'run-step' + }, + { category: PluginCategory.Drone, name: 'Drone', description: 'Run Drone plugins', icon: 'ci-infra' } +] + +const dronePluginSpecMockData = { + inputs: { + channel: { + type: 'string' + }, + token: { + type: 'string' + } + }, + steps: [ + { + type: 'script', + spec: { + image: 'plugins/slack' + }, + envs: { + PLUGIN_CHANNEL: '<+inputs.channel>' + } + } + ] +} + +const runStepSpec = { + inputs: { + script: { + type: 'string' + } + } +} + +export interface PluginsPanelInterface { + version?: YamlVersion + onPluginAddUpdate: (isUpdate: boolean, pluginFormData: Record) => void +} + +export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: PluginsPanelInterface): JSX.Element => { + const { getString } = useStrings() + const [category, setCategory] = useState() + const [panelView, setPanelView] = useState(PluginPanelView.Category) + const [plugin, setPlugin] = useState() + + const { + data: plugins, + loading, + refetch: fetchPlugins + } = useGet({ + path: `/api/v1/plugins`, + queryParams: { + limit: LIST_FETCHING_LIMIT, + page: 1 + }, + lazy: true + }) + + useEffect(() => { + if (category === PluginCategory.Drone) { + fetchPlugins() + } + }, [category]) + + const renderPluginCategories = (): JSX.Element => { + return ( + <> + {PluginCategories.map((item: PluginInterface) => { + const { name, category: pluginCategory, description, icon } = item + return ( + { + setCategory(pluginCategory) + if (pluginCategory === PluginCategory.Drone) { + setPanelView(PluginPanelView.Listing) + } else if (pluginCategory === PluginCategory.Harness) { + setPlugin({ uid: getString('run') }) + setPanelView(PluginPanelView.Configuration) + } + }} + key={pluginCategory} + padding={{ left: 'medium', right: 'medium', top: 'medium', bottom: 'medium' }} + flex={{ justifyContent: 'flex-start' }} + className={css.plugin}> + + + + + + {name} + + {description} + + + ) + })} + + ) + } + + const renderPlugins = useCallback((): JSX.Element => { + return loading ? ( + + + + ) : ( + + + { + setPanelView(PluginPanelView.Category) + }} + className={css.arrow} + /> + + {getString('plugins.addAPlugin', { category: PluginCategory[category as PluginCategory] })} + + + + {plugins?.map((_plugin: TypesPlugin) => { + const { uid, description } = _plugin + return ( + { + setPanelView(PluginPanelView.Configuration) + setPlugin(_plugin) + }} + key={uid}> + + + + {uid} + + {description} + + + ) + })} + + + ) + }, [loading, plugins]) + + const renderPluginFormField = ({ name, type }: { name: string; type: 'string' }): JSX.Element => { + return type === 'string' ? ( + {capitalize(name)}} + style={{ width: '100%' }} + key={name} + /> + ) : ( + <> + ) + } + + const constructPayloadForYAMLInsertion = (isUpdate: boolean, pluginFormData: Record) => { + let constructedPayload = { ...pluginFormData } + switch (category) { + case PluginCategory.Drone: + case PluginCategory.Harness: + constructedPayload = + version === YamlVersion.V1 + ? { type: 'script', spec: constructedPayload } + : { name: 'run step', commands: [get(constructedPayload, 'script', '')] } + } + onPluginAddUpdate?.(isUpdate, constructedPayload) + } + + const renderPluginConfigForm = useCallback((): JSX.Element => { + // TODO obtain plugin input spec by parsing YAML + const inputs = get(category === PluginCategory.Drone ? dronePluginSpecMockData : runStepSpec, 'inputs', {}) + return ( + + + { + setPlugin(undefined) + if (category === PluginCategory.Drone) { + setPanelView(PluginPanelView.Listing) + } else if (category === PluginCategory.Harness) { + setPanelView(PluginPanelView.Category) + } + }} + className={css.arrow} + /> + {plugin?.uid ? ( + + {getString('addLabel')} {plugin.uid} {getString('plugins.stepLabel')} + + ) : ( + <> + )} + + + { + constructPayloadForYAMLInsertion(false, formData) + }}> + + + + {Object.keys(inputs).map((field: string) => { + const fieldType = get(inputs, `${field}.type`, '') as 'string' + return renderPluginFormField({ name: field, type: fieldType }) + })} + + + openModal({ repoMetadata }) + }} + disabled={loading} + /> ) const columns: Column[] = useMemo( () => [ { Header: getString('pipelines.name'), - width: 'calc(50% - 90px)', + width: 'calc(100% - 210px)', Cell: ({ row }: CellProps) => { const record = row.original return ( @@ -109,6 +114,7 @@ const PipelineList = () => { noBackground iconSize={24} className={css.statusIcon} + isCi /> {record.uid} @@ -128,7 +134,7 @@ const PipelineList = () => { {`#${record.number}`} - {record.message} + {record.title || record.message} { /> {/* TODO need logic here for different trigger types */} {record.author_name} - - - {record.source} + {record.target && ( + <> + + + {record.target.split('/').pop()} + + )} { ) }, disableSortBy: true + }, + { + Header: ' ', + width: '30px', + Cell: ({ row }: CellProps) => { + const [menuOpen, setMenuOpen] = useState(false) + const record = row.original + const { uid } = record + return ( + { + setMenuOpen(nextOpenState) + }} + className={Classes.DARK} + position={Position.BOTTOM_RIGHT}> +