mirror of
https://github.com/harness/drone.git
synced 2025-05-09 03:22:25 +08:00
Merge remote-tracking branch 'origin' into abhinav/CODE-830
This commit is contained in:
commit
3ec8d18fbf
@ -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
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
33
internal/api/controller/pullreq/pr_recheck.go
Normal file
33
internal/api/controller/pullreq/pr_recheck.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
|
58
internal/api/controller/trigger/common.go
Normal file
58
internal/api/controller/trigger/common.go
Normal file
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
41
internal/api/handler/pullreq/pr_recheck.go
Normal file
41
internal/api/handler/pullreq/pr_recheck.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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 == "" {
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 (
|
||||
|
@ -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.
|
||||
|
60
types/enum/trigger_actions.go
Normal file
60
types/enum/trigger_actions.go
Normal file
@ -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"
|
||||
)
|
@ -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"
|
||||
)
|
||||
|
@ -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:"-"`
|
||||
}
|
||||
|
@ -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'
|
||||
},
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
|
@ -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",
|
||||
|
@ -46,7 +46,7 @@ export interface CODERoutes {
|
||||
toCODESpaceAccessControl: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
toCODESpaceSettings: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
toCODEPipelinesNew: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||
toCODESecrets: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
|
||||
toCODEGlobalSettings: () => string
|
||||
@ -73,7 +73,7 @@ export interface CODERoutes {
|
||||
toCODEWebhookNew: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
toCODEWebhookDetails: (args: Required<Pick<CODEProps, 'repoPath' | 'webhookId'>>) => string
|
||||
toCODESettings: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
|
||||
toCODESearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||
toCODEExecution: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline' | 'execution'>>) => string
|
||||
toCODESecret: (args: Required<Pick<CODEProps, 'space' | 'secret'>>) => 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}`,
|
||||
|
@ -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 (
|
||||
<BrowserRouter>
|
||||
@ -163,7 +164,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
|
||||
{OPEN_SOURCE_PIPELINES && (
|
||||
{standalone && (
|
||||
<Route
|
||||
path={routes.toCODEExecution({
|
||||
repoPath,
|
||||
@ -177,7 +178,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{OPEN_SOURCE_PIPELINES && (
|
||||
{standalone && (
|
||||
<Route path={routes.toCODEExecutions({ repoPath, pipeline: pathProps.pipeline })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.executions')}>
|
||||
<ExecutionList />
|
||||
@ -185,15 +186,15 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{OPEN_SOURCE_PIPELINES && (
|
||||
<Route path={routes.toCODEPipelinesNew({ space: pathProps.space })} exact>
|
||||
{standalone && (
|
||||
<Route path={routes.toCODEPipelineEdit({ repoPath, pipeline: pathProps.pipeline })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.pipelines')}>
|
||||
<NewPipeline />
|
||||
<AddUpdatePipeline />
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{OPEN_SOURCE_PIPELINES && (
|
||||
{standalone && (
|
||||
<Route path={routes.toCODEPipelines({ repoPath })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.pipelines')}>
|
||||
<PipelineList />
|
||||
@ -201,7 +202,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{OPEN_SOURCE_SECRETS && (
|
||||
{standalone && (
|
||||
<Route path={routes.toCODESecret({ space: pathProps.space, secret: pathProps.secret })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.secrets')}>
|
||||
<Secret />
|
||||
@ -209,7 +210,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{OPEN_SOURCE_SECRETS && (
|
||||
{standalone && (
|
||||
<Route path={routes.toCODESecrets({ space: pathProps.space })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.secrets')}>
|
||||
<SecretList />
|
||||
@ -249,6 +250,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
|
||||
<Route path={routes.toCODESearch({ repoPath })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.search')}>
|
||||
<Search />
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={routes.toCODEFileEdit({
|
||||
repoPath,
|
||||
|
@ -26,6 +26,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
background-color: var(--red) !important;
|
||||
height: var(--log-content-header-height) !important;
|
||||
border-bottom: 1px solid var(--grey-800) !important;
|
||||
padding: var(--spacing-medium) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.steps {
|
||||
padding: var(--spacing-medium) !important;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const container: string
|
||||
export declare const error: string
|
||||
export declare const header: string
|
||||
export declare const headerLayout: string
|
||||
export declare const log: string
|
||||
|
@ -20,6 +20,13 @@ const Console: FC<ConsoleProps> = ({ stage, repoPath }) => {
|
||||
|
||||
return (
|
||||
<div className={css.container}>
|
||||
{stage?.error && (
|
||||
<Container className={css.error}>
|
||||
<Text font={{ variation: FontVariation.BODY }} color={Color.WHITE}>
|
||||
{stage?.error}
|
||||
</Text>
|
||||
</Container>
|
||||
)}
|
||||
<Container className={css.header}>
|
||||
<Layout.Horizontal className={css.headerLayout} spacing="small">
|
||||
<Text font={{ variation: FontVariation.H4 }} color={Color.WHITE} padding={{ left: 'large', right: 'large' }}>
|
||||
|
@ -20,3 +20,16 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.timeoutIcon {
|
||||
svg {
|
||||
g {
|
||||
circle {
|
||||
fill: black !important;
|
||||
}
|
||||
path {
|
||||
fill: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,3 +3,4 @@
|
||||
export declare const loading: string
|
||||
export declare const spin: string
|
||||
export declare const stepLayout: string
|
||||
export declare const timeoutIcon: string
|
||||
|
@ -70,6 +70,10 @@ const ConsoleStep: FC<ConsoleStepProps> = ({ step, stageNumber, repoPath, pipeli
|
||||
icon = <Icon name="circle" />
|
||||
} else if (step?.status === ExecutionState.RUNNING) {
|
||||
icon = <Icon className={css.spin} name="pending" />
|
||||
} else if (step?.status === ExecutionState.FAILURE) {
|
||||
icon = <Icon name="danger-icon" />
|
||||
} else if (step?.status === ExecutionState.SKIPPED) {
|
||||
icon = <Icon className={css.timeoutIcon} name="execution-timeout" />
|
||||
} else {
|
||||
icon = <Icon name="circle" /> // Default icon in case of other statuses or unknown status
|
||||
}
|
||||
@ -93,7 +97,7 @@ const ConsoleStep: FC<ConsoleStepProps> = ({ step, stageNumber, repoPath, pipeli
|
||||
className={css.stepLayout}
|
||||
spacing="medium"
|
||||
onClick={() => {
|
||||
if (!isPending) {
|
||||
if (!isPending && step?.status !== ExecutionState.SKIPPED) {
|
||||
setIsOpened(!isOpened)
|
||||
if (shouldUseGet && !isOpened) refetch()
|
||||
}
|
||||
|
@ -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<React.SetStateAction<boolean>>
|
||||
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<EditorView>()
|
||||
const ref = useRef<HTMLDivElement>()
|
||||
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 <Container ref={ref} className={cx(css.editor, className)} style={style} />
|
||||
})
|
||||
|
@ -79,7 +79,7 @@ export function ExecutionPageHeader({
|
||||
content={
|
||||
executionInfo && (
|
||||
<Container className={css.executionInfo}>
|
||||
<ExecutionStatus status={getStatus(executionInfo.status)} iconOnly noBackground iconSize={18} />
|
||||
<ExecutionStatus status={getStatus(executionInfo.status)} iconOnly noBackground iconSize={18} isCi />
|
||||
<Text inline color={Color.GREY_800} font={{ size: 'small' }}>
|
||||
{executionInfo.message}
|
||||
</Text>
|
||||
|
@ -34,6 +34,7 @@ const ExecutionStage: FC<ExecutionStageProps> = ({ stage, isSelected = false, se
|
||||
noBackground
|
||||
iconSize={18}
|
||||
className={css.statusIcon}
|
||||
isCi
|
||||
/>
|
||||
<Text className={css.uid} lineClamp={1}>
|
||||
{stage.name}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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<ExecutionStatusProps> = ({
|
||||
@ -26,19 +28,20 @@ export const ExecutionStatus: React.FC<ExecutionStatusProps> = ({
|
||||
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<ExecutionStatusProps> = ({
|
||||
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])
|
||||
|
||||
|
21
web/src/components/ExecutionText/ExecutionText.module.scss
Normal file
21
web/src/components/ExecutionText/ExecutionText.module.scss
Normal file
@ -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;
|
||||
}
|
6
web/src/components/ExecutionText/ExecutionText.module.scss.d.ts
vendored
Normal file
6
web/src/components/ExecutionText/ExecutionText.module.scss.d.ts
vendored
Normal file
@ -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
|
162
web/src/components/ExecutionText/ExecutionText.tsx
Normal file
162
web/src/components/ExecutionText/ExecutionText.tsx
Normal file
@ -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<PillProps> = ({ 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 (
|
||||
<Layout.Horizontal
|
||||
spacing={'xsmall'}
|
||||
style={{ alignItems: 'center', borderRadius: '4px' }}
|
||||
className={css.pillContainer}>
|
||||
<Icon height={12} width={12} color={Utils.getRealCSSColor(Color.GREY_500)} />
|
||||
<Text className={css.pillText} font={{ size: 'xsmall' }}>
|
||||
{text}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
|
||||
export const ExecutionText: React.FC<ExecutionTextProps> = ({
|
||||
authorName,
|
||||
authorEmail,
|
||||
repoPath,
|
||||
commitRef,
|
||||
event,
|
||||
target,
|
||||
beforeRef,
|
||||
source,
|
||||
action
|
||||
}) => {
|
||||
const { routes } = useAppContext()
|
||||
|
||||
let componentToRender
|
||||
|
||||
switch (event) {
|
||||
case ExecutionTrigger.CRON:
|
||||
componentToRender = (
|
||||
<Text font={{ size: 'small' }} className={css.author}>
|
||||
Triggered by CRON job
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
case ExecutionTrigger.MANUAL:
|
||||
componentToRender = (
|
||||
<Text font={{ size: 'small' }} className={css.author}>{`${authorName} triggered manually`}</Text>
|
||||
)
|
||||
break
|
||||
case ExecutionTrigger.PUSH:
|
||||
componentToRender = (
|
||||
<>
|
||||
<Text font={{ size: 'small' }} className={css.author}>{`${authorName} pushed`}</Text>
|
||||
<Pill type={PillType.COMMIT} text={beforeRef.slice(0, 6)} />
|
||||
<Text font={{ size: 'small' }} className={css.author}>
|
||||
to
|
||||
</Text>
|
||||
<Pill type={PillType.BRANCH} text={target} />
|
||||
</>
|
||||
)
|
||||
break
|
||||
case ExecutionTrigger.PULL:
|
||||
componentToRender = (
|
||||
<>
|
||||
<Text font={{ size: 'small' }} className={css.author}>{`${authorName} ${
|
||||
action === 'pullreq_reopened' ? 'reopened' : action === 'pullreq_branch_updated' ? 'updated' : 'created'
|
||||
} pull request`}</Text>
|
||||
<Pill type={PillType.BRANCH} text={source} />
|
||||
<Text font={{ size: 'small' }} className={css.author}>
|
||||
to
|
||||
</Text>
|
||||
<Pill type={PillType.BRANCH} text={target} />
|
||||
</>
|
||||
)
|
||||
break
|
||||
case ExecutionTrigger.TAG:
|
||||
componentToRender = (
|
||||
<>
|
||||
<Text font={{ size: 'small' }} className={css.author}>{`${authorName} ${
|
||||
action === 'branch_updated' ? 'updated' : 'created'
|
||||
}`}</Text>
|
||||
<Pill type={PillType.TAG} text={target.split('/').pop() as string} />
|
||||
</>
|
||||
)
|
||||
break
|
||||
default:
|
||||
componentToRender = (
|
||||
<Text font={{ size: 'small' }} className={css.author}>
|
||||
Unknown trigger
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center', marginLeft: '1.2rem' }}>
|
||||
<Avatar email={authorEmail} name={authorName} size="small" hoverCard={false} />
|
||||
{componentToRender}
|
||||
<PipeSeparator height={7} />
|
||||
<Link
|
||||
to={routes.toCODECommit({
|
||||
repoPath: repoPath,
|
||||
commitRef: commitRef
|
||||
})}
|
||||
className={css.hash}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
{commitRef?.slice(0, 6)}
|
||||
</Link>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
3
web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts
vendored
Normal file
3
web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const branchSelect: string
|
162
web/src/components/NewPipelineModal/NewPipelineModal.tsx
Normal file
162
web/src/components/NewPipelineModal/NewPipelineModal.tsx
Normal file
@ -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<TypesRepository | undefined>()
|
||||
const repoPath = useMemo(() => repo?.path || '', [repo])
|
||||
|
||||
const { mutate: savePipeline } = useMutate<TypesPipeline>({
|
||||
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 (
|
||||
<Dialog isOpen enforceFocus={false} onClose={onClose} title={getString('pipelines.createNewPipeline')}>
|
||||
<Formik<FormData>
|
||||
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 (
|
||||
<FormikForm>
|
||||
<Layout.Vertical spacing="small">
|
||||
<Layout.Vertical spacing="small">
|
||||
<FormInput.Text
|
||||
name="name"
|
||||
label={getString('name')}
|
||||
placeholder={getString('pipelines.enterPipelineName')}
|
||||
inputGroup={{ autoFocus: true }}
|
||||
onChange={event => {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Layout.Vertical spacing="xsmall" padding={{ bottom: 'medium' }}>
|
||||
<Text font={{ variation: FontVariation.BODY }}>{capitalize(getString('branch'))}</Text>
|
||||
<Container className={css.branchSelect}>
|
||||
<BranchTagSelect
|
||||
gitRef={formik?.values?.branch || repo?.default_branch || ''}
|
||||
onSelect={(ref: string) => {
|
||||
formik?.setFieldValue('branch', ref)
|
||||
}}
|
||||
repoMetadata={repo || {}}
|
||||
disableBranchCreation
|
||||
disableViewAllBranches
|
||||
forBranchesOnly
|
||||
/>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
<FormInput.Text
|
||||
name="yamlPath"
|
||||
label={getString('pipelines.yamlPath')}
|
||||
placeholder={getString('pipelines.enterYAMLPath')}
|
||||
/>
|
||||
</Layout.Vertical>
|
||||
<Layout.Horizontal spacing="medium" width="100%">
|
||||
<Button variation={ButtonVariation.PRIMARY} text={getString('create')} type="submit" />
|
||||
<Button variation={ButtonVariation.SECONDARY} text={getString('cancel')} onClick={onClose} />
|
||||
</Layout.Horizontal>
|
||||
</Layout.Vertical>
|
||||
</FormikForm>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
)
|
||||
}, [repo])
|
||||
|
||||
return {
|
||||
openModal: ({ repoMetadata }: { repoMetadata?: TypesRepository }) => {
|
||||
setRepo(repoMetadata)
|
||||
openModal()
|
||||
},
|
||||
hideModal
|
||||
}
|
||||
}
|
||||
|
||||
export default useNewPipelineModal
|
53
web/src/components/PluginsPanel/PluginsPanel.module.scss
Normal file
53
web/src/components/PluginsPanel/PluginsPanel.module.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
9
web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts
vendored
Normal file
9
web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts
vendored
Normal file
@ -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
|
301
web/src/components/PluginsPanel/PluginsPanel.tsx
Normal file
301
web/src/components/PluginsPanel/PluginsPanel.tsx
Normal file
@ -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<string, any>) => void
|
||||
}
|
||||
|
||||
export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: PluginsPanelInterface): JSX.Element => {
|
||||
const { getString } = useStrings()
|
||||
const [category, setCategory] = useState<PluginCategory>()
|
||||
const [panelView, setPanelView] = useState<PluginPanelView>(PluginPanelView.Category)
|
||||
const [plugin, setPlugin] = useState<TypesPlugin>()
|
||||
|
||||
const {
|
||||
data: plugins,
|
||||
loading,
|
||||
refetch: fetchPlugins
|
||||
} = useGet<TypesPlugin[]>({
|
||||
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 (
|
||||
<Layout.Horizontal
|
||||
onClick={() => {
|
||||
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}>
|
||||
<Container padding="small" className={css.pluginIcon}>
|
||||
<Icon name={icon} />
|
||||
</Container>
|
||||
<Layout.Vertical padding={{ left: 'small' }}>
|
||||
<Text color={Color.PRIMARY_7} font={{ variation: FontVariation.BODY2 }}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text font={{ variation: FontVariation.SMALL }}>{description}</Text>
|
||||
</Layout.Vertical>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPlugins = useCallback((): JSX.Element => {
|
||||
return loading ? (
|
||||
<Container flex={{ justifyContent: 'center' }} padding="large">
|
||||
<Icon name="steps-spinner" color={Color.PRIMARY_7} size={25} />
|
||||
</Container>
|
||||
) : (
|
||||
<Layout.Vertical spacing="small" padding={{ top: 'small' }}>
|
||||
<Layout.Horizontal
|
||||
flex={{ justifyContent: 'flex-start', alignItems: 'center' }}
|
||||
spacing="small"
|
||||
padding={{ left: 'medium' }}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
size={18}
|
||||
onClick={() => {
|
||||
setPanelView(PluginPanelView.Category)
|
||||
}}
|
||||
className={css.arrow}
|
||||
/>
|
||||
<Text font={{ variation: FontVariation.H5 }} flex={{ justifyContent: 'center' }}>
|
||||
{getString('plugins.addAPlugin', { category: PluginCategory[category as PluginCategory] })}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
<Container>
|
||||
{plugins?.map((_plugin: TypesPlugin) => {
|
||||
const { uid, description } = _plugin
|
||||
return (
|
||||
<Layout.Horizontal
|
||||
flex={{ justifyContent: 'flex-start' }}
|
||||
padding={{ left: 'large', top: 'medium', bottom: 'medium', right: 'large' }}
|
||||
className={css.plugin}
|
||||
onClick={() => {
|
||||
setPanelView(PluginPanelView.Configuration)
|
||||
setPlugin(_plugin)
|
||||
}}
|
||||
key={uid}>
|
||||
<Icon name={'gear'} size={25} />
|
||||
<Layout.Vertical padding={{ left: 'small' }}>
|
||||
<Text font={{ variation: FontVariation.BODY2 }} color={Color.PRIMARY_7}>
|
||||
{uid}
|
||||
</Text>
|
||||
<Text font={{ variation: FontVariation.SMALL }}>{description}</Text>
|
||||
</Layout.Vertical>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
)
|
||||
}, [loading, plugins])
|
||||
|
||||
const renderPluginFormField = ({ name, type }: { name: string; type: 'string' }): JSX.Element => {
|
||||
return type === 'string' ? (
|
||||
<FormInput.Text
|
||||
name={name}
|
||||
label={<Text font={{ variation: FontVariation.FORM_INPUT_TEXT }}>{capitalize(name)}</Text>}
|
||||
style={{ width: '100%' }}
|
||||
key={name}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
const constructPayloadForYAMLInsertion = (isUpdate: boolean, pluginFormData: Record<string, any>) => {
|
||||
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 (
|
||||
<Layout.Vertical
|
||||
spacing="medium"
|
||||
margin={{ left: 'xxlarge', top: 'large', right: 'xxlarge', bottom: 'xxlarge' }}
|
||||
height="95%">
|
||||
<Layout.Horizontal spacing="small" flex={{ justifyContent: 'flex-start' }}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
size={18}
|
||||
onClick={() => {
|
||||
setPlugin(undefined)
|
||||
if (category === PluginCategory.Drone) {
|
||||
setPanelView(PluginPanelView.Listing)
|
||||
} else if (category === PluginCategory.Harness) {
|
||||
setPanelView(PluginPanelView.Category)
|
||||
}
|
||||
}}
|
||||
className={css.arrow}
|
||||
/>
|
||||
{plugin?.uid ? (
|
||||
<Text font={{ variation: FontVariation.H5 }}>
|
||||
{getString('addLabel')} {plugin.uid} {getString('plugins.stepLabel')}
|
||||
</Text>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Layout.Horizontal>
|
||||
<Container className={css.form}>
|
||||
<Formik
|
||||
initialValues={{}}
|
||||
onSubmit={formData => {
|
||||
constructPayloadForYAMLInsertion(false, formData)
|
||||
}}>
|
||||
<FormikForm>
|
||||
<Layout.Vertical flex={{ alignItems: 'flex-start' }} height="100%">
|
||||
<Layout.Vertical width="100%">
|
||||
{Object.keys(inputs).map((field: string) => {
|
||||
const fieldType = get(inputs, `${field}.type`, '') as 'string'
|
||||
return renderPluginFormField({ name: field, type: fieldType })
|
||||
})}
|
||||
</Layout.Vertical>
|
||||
<Button variation={ButtonVariation.PRIMARY} text={getString('addLabel')} type="submit" />
|
||||
</Layout.Vertical>
|
||||
</FormikForm>
|
||||
</Formik>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
)
|
||||
}, [plugin, category])
|
||||
|
||||
const renderPluginsPanel = useCallback((): JSX.Element => {
|
||||
switch (panelView) {
|
||||
case PluginPanelView.Category:
|
||||
return renderPluginCategories()
|
||||
case PluginPanelView.Listing:
|
||||
return renderPlugins()
|
||||
case PluginPanelView.Configuration:
|
||||
return renderPluginConfigForm()
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}, [loading, plugins, panelView, category])
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<Tabs id={'pluginsPanel'} defaultSelectedTabId={'plugins'}>
|
||||
<Tab
|
||||
panelClassName={css.mainTabPanel}
|
||||
id="plugins"
|
||||
title={
|
||||
<Text
|
||||
font={{ variation: FontVariation.BODY2 }}
|
||||
padding={{ left: 'small', bottom: 'xsmall', top: 'xsmall' }}
|
||||
color={Color.PRIMARY_7}>
|
||||
{getString('plugins.title')}
|
||||
</Text>
|
||||
}
|
||||
panel={<Container className={css.pluginDetailsPanel}>{renderPluginsPanel()}</Container>}
|
||||
/>
|
||||
</Tabs>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import { Container, Layout, Text, PageHeader } from '@harnessio/uicore'
|
||||
import { Container, Layout, Text, PageHeader, PageHeaderProps } from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
@ -19,33 +19,35 @@ interface RepositoryPageHeaderProps extends Optional<Pick<GitInfoProps, 'repoMet
|
||||
title: string | JSX.Element
|
||||
dataTooltipId: string
|
||||
extraBreadcrumbLinks?: BreadcrumbLink[]
|
||||
className?: string
|
||||
content?: PageHeaderProps['content']
|
||||
}
|
||||
|
||||
export function RepositoryPageHeader({
|
||||
repoMetadata,
|
||||
title,
|
||||
dataTooltipId,
|
||||
extraBreadcrumbLinks = []
|
||||
extraBreadcrumbLinks = [],
|
||||
className,
|
||||
content
|
||||
}: RepositoryPageHeaderProps) {
|
||||
const { gitRef } = useParams<CODEProps>()
|
||||
const { getString } = useStrings()
|
||||
const space = useGetSpaceParam()
|
||||
const { routes } = useAppContext()
|
||||
|
||||
if (!repoMetadata) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PageHeader
|
||||
className={className}
|
||||
content={content}
|
||||
title=""
|
||||
breadcrumbs={
|
||||
<Container className={css.header}>
|
||||
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
||||
<Link to={routes.toCODERepositories({ space })}>{getString('repositories')}</Link>
|
||||
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
|
||||
<Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })}>
|
||||
{repoMetadata.uid}
|
||||
<Link to={routes.toCODERepository({ repoPath: (repoMetadata?.path as string) || '', gitRef })}>
|
||||
{repoMetadata?.uid || ''}
|
||||
</Link>
|
||||
{extraBreadcrumbLinks.map(link => (
|
||||
<Fragment key={link.url}>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
3
web/src/components/RunPipelineModal/RunPipelineModal.module.scss.d.ts
vendored
Normal file
3
web/src/components/RunPipelineModal/RunPipelineModal.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const branchSelect: string
|
130
web/src/components/RunPipelineModal/RunPipelineModal.tsx
Normal file
130
web/src/components/RunPipelineModal/RunPipelineModal.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useHistory } from 'react-router'
|
||||
import { useMutate } from 'restful-react'
|
||||
import * as yup from 'yup'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { FontVariation } from '@harnessio/design-system'
|
||||
import {
|
||||
Button,
|
||||
ButtonVariation,
|
||||
Container,
|
||||
Dialog,
|
||||
Formik,
|
||||
FormikForm,
|
||||
Layout,
|
||||
Text,
|
||||
useToaster
|
||||
} from '@harnessio/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useModalHook } from 'hooks/useModalHook'
|
||||
import type { CreateExecutionQueryParams, TypesExecution, TypesRepository } from 'services/code'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||
|
||||
import css from './RunPipelineModal.module.scss'
|
||||
|
||||
interface FormData {
|
||||
branch: string
|
||||
}
|
||||
|
||||
const useRunPipelineModal = () => {
|
||||
const { routes } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
const { showSuccess, showError, clear: clearToaster } = useToaster()
|
||||
const history = useHistory()
|
||||
const [repo, setRepo] = useState<TypesRepository>()
|
||||
const [pipeline, setPipeline] = useState<string>('')
|
||||
const repoPath = useMemo(() => repo?.path || '', [repo])
|
||||
|
||||
const { mutate: startExecution } = useMutate<TypesExecution>({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/executions`
|
||||
})
|
||||
|
||||
const runPipeline = (formData: FormData): void => {
|
||||
const { branch } = formData
|
||||
try {
|
||||
startExecution(
|
||||
{},
|
||||
{
|
||||
pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/executions` },
|
||||
queryParams: { branch } as CreateExecutionQueryParams
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
clearToaster()
|
||||
showSuccess(getString('pipelines.executionStarted'))
|
||||
if (response?.number && !isNaN(response.number)) {
|
||||
history.push(routes.toCODEExecution({ repoPath, pipeline, execution: response.number.toString() }))
|
||||
}
|
||||
hideModal()
|
||||
})
|
||||
.catch(error => {
|
||||
showError(getErrorMessage(error), 0, 'pipelines.executionCouldNotStart')
|
||||
})
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception), 0, 'pipelines.executionCouldNotStart')
|
||||
}
|
||||
}
|
||||
|
||||
const [openModal, hideModal] = useModalHook(() => {
|
||||
const onClose = () => {
|
||||
hideModal()
|
||||
}
|
||||
return (
|
||||
<Dialog isOpen enforceFocus={false} onClose={onClose} title={getString('pipelines.run')}>
|
||||
<Formik
|
||||
formName="run-pipeline-form"
|
||||
initialValues={{ branch: repo?.default_branch || '' }}
|
||||
validationSchema={yup.object().shape({
|
||||
branch: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required(`${getString('branch')} ${getString('isRequired')}`)
|
||||
})}
|
||||
onSubmit={runPipeline}
|
||||
enableReinitialize>
|
||||
{formik => {
|
||||
return (
|
||||
<FormikForm>
|
||||
<Layout.Vertical spacing="medium">
|
||||
<Layout.Vertical spacing="xsmall" padding={{ bottom: 'medium' }}>
|
||||
<Text font={{ variation: FontVariation.BODY }}>{capitalize(getString('branch'))}</Text>
|
||||
<Container className={css.branchSelect}>
|
||||
<BranchTagSelect
|
||||
gitRef={formik?.values?.branch || repo?.default_branch || ''}
|
||||
onSelect={(ref: string) => {
|
||||
formik?.setFieldValue('branch', ref)
|
||||
}}
|
||||
repoMetadata={repo || {}}
|
||||
disableBranchCreation
|
||||
disableViewAllBranches
|
||||
forBranchesOnly
|
||||
/>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
<Layout.Horizontal spacing="medium">
|
||||
<Button variation={ButtonVariation.PRIMARY} type="submit" text={getString('pipelines.run')} />
|
||||
<Button variation={ButtonVariation.SECONDARY} text={getString('cancel')} onClick={onClose} />
|
||||
</Layout.Horizontal>
|
||||
</Layout.Vertical>
|
||||
</FormikForm>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
)
|
||||
}, [repo?.default_branch, pipeline])
|
||||
|
||||
return {
|
||||
openModal: ({ repoMetadata, pipeline: pipelineUid }: { repoMetadata: TypesRepository; pipeline: string }) => {
|
||||
setRepo(repoMetadata)
|
||||
setPipeline(pipelineUid)
|
||||
openModal()
|
||||
},
|
||||
hideModal
|
||||
}
|
||||
}
|
||||
|
||||
export default useRunPipelineModal
|
@ -16,6 +16,7 @@ interface SearchInputWithSpinnerProps {
|
||||
icon?: IconName
|
||||
spinnerIcon?: IconName
|
||||
spinnerPosition?: 'left' | 'right'
|
||||
onSearch?: (searchTerm: string) => void
|
||||
}
|
||||
|
||||
export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
@ -26,7 +27,8 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
placeholder,
|
||||
icon = 'search',
|
||||
spinnerIcon = 'steps-spinner',
|
||||
spinnerPosition = 'left'
|
||||
spinnerPosition = 'left',
|
||||
onSearch
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const spinner = <Icon name={spinnerIcon as IconName} color={Color.PRIMARY_7} />
|
||||
@ -37,6 +39,7 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
<Layout.Horizontal className={css.layout}>
|
||||
<Render when={loading && !spinnerOnRight}>{spinner}</Render>
|
||||
<TextInput
|
||||
type="search"
|
||||
value={query}
|
||||
wrapperClassName={cx(css.wrapper, { [css.spinnerOnRight]: spinnerOnRight })}
|
||||
className={css.input}
|
||||
@ -45,7 +48,14 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
style={{ width }}
|
||||
autoFocus
|
||||
onFocus={event => event.target.select()}
|
||||
onInput={event => setQuery(event.currentTarget.value || '')}
|
||||
onInput={event => {
|
||||
setQuery(event.currentTarget.value || '')
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSearch?.((e as unknown as React.FormEvent<HTMLInputElement>).currentTarget.value || '')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Render when={loading && spinnerOnRight}>{spinner}</Render>
|
||||
</Layout.Horizontal>
|
||||
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import type monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
import MonacoEditor, { MonacoDiffEditor } from 'react-monaco-editor'
|
||||
import { setDiagnosticsOptions } from 'monaco-yaml'
|
||||
import { noop } from 'lodash-es'
|
||||
import { SourceCodeEditorProps, PLAIN_TEXT } from 'utils/Utils'
|
||||
import { useEventListener } from 'hooks/useEventListener'
|
||||
@ -39,7 +40,8 @@ export default function MonacoSourceCodeEditor({
|
||||
height,
|
||||
autoHeight,
|
||||
wordWrap = true,
|
||||
onChange = noop
|
||||
onChange = noop,
|
||||
schema
|
||||
}: SourceCodeEditorProps) {
|
||||
const [editor, setEditor] = useState<monacoEditor.editor.IStandaloneCodeEditor>()
|
||||
const scrollbar = autoHeight ? 'hidden' : 'auto'
|
||||
@ -50,6 +52,24 @@ export default function MonacoSourceCodeEditor({
|
||||
monaco.languages.typescript?.typescriptDefaults?.setCompilerOptions?.(compilerOptions)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (language === 'yaml' && schema) {
|
||||
setDiagnosticsOptions({
|
||||
validate: true,
|
||||
enableSchemaRequest: false,
|
||||
hover: true,
|
||||
completion: true,
|
||||
schemas: [
|
||||
{
|
||||
fileMatch: ['*'],
|
||||
schema,
|
||||
uri: 'https://github.com/harness/harness-schema'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}, [language, schema])
|
||||
|
||||
useEventListener('resize', () => {
|
||||
editor?.layout({ width: 0, height: 0 })
|
||||
window.requestAnimationFrame(() => editor?.layout())
|
||||
|
@ -48,7 +48,7 @@
|
||||
width: min(calc(100vw - var(--nav-menu-width)), 840px) !important;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 5px;
|
||||
left: 241px;
|
||||
top: -5px;
|
||||
|
||||
> div {
|
||||
@ -79,6 +79,13 @@
|
||||
background-color: rgba(26, 26, 26, 0.4);
|
||||
}
|
||||
|
||||
:global {
|
||||
.bp3-popover-arrow {
|
||||
left: -11px !important;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
48
web/src/components/Split/Split.module.scss
Normal file
48
web/src/components/Split/Split.module.scss
Normal file
@ -0,0 +1,48 @@
|
||||
.main {
|
||||
:global {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const layout: string
|
||||
export declare const main: string
|
8
web/src/components/Split/Split.tsx
Normal file
8
web/src/components/Split/Split.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import SplitPane, { type SplitPaneProps } from 'react-split-pane'
|
||||
import css from './Split.module.scss'
|
||||
|
||||
export const Split: React.FC<SplitPaneProps> = ({ className, ...props }) => (
|
||||
<SplitPane className={cx(css.main, className)} {...props} />
|
||||
)
|
@ -11,11 +11,13 @@ export interface StringsMap {
|
||||
add: string
|
||||
addComment: string
|
||||
addGitIgnore: string
|
||||
addLabel: string
|
||||
addLicense: string
|
||||
addMember: string
|
||||
addNewFile: string
|
||||
addReadMe: string
|
||||
admin: string
|
||||
aiSearch: string
|
||||
all: string
|
||||
allBranches: string
|
||||
allComments: string
|
||||
@ -64,6 +66,7 @@ export interface StringsMap {
|
||||
cloneText: string
|
||||
close: string
|
||||
closed: string
|
||||
codeSearch: string
|
||||
comment: string
|
||||
commentDeleted: string
|
||||
commit: string
|
||||
@ -190,6 +193,7 @@ export interface StringsMap {
|
||||
failedToCreateSpace: string
|
||||
failedToDeleteBranch: string
|
||||
failedToDeleteWebhook: string
|
||||
failedToSavePipeline: string
|
||||
fileDeleted: string
|
||||
fileTooLarge: string
|
||||
files: string
|
||||
@ -212,6 +216,7 @@ export interface StringsMap {
|
||||
'homepage.welcomeText': string
|
||||
in: string
|
||||
inactiveBranches: string
|
||||
isRequired: string
|
||||
leaveAComment: string
|
||||
license: string
|
||||
lineBreaks: string
|
||||
@ -304,6 +309,7 @@ export interface StringsMap {
|
||||
'pageTitle.repositories': string
|
||||
'pageTitle.repository': string
|
||||
'pageTitle.repositorySettings': string
|
||||
'pageTitle.search': string
|
||||
'pageTitle.secrets': string
|
||||
'pageTitle.signin': string
|
||||
'pageTitle.spaceSettings': string
|
||||
@ -318,11 +324,27 @@ export interface StringsMap {
|
||||
payloadUrl: string
|
||||
payloadUrlLabel: string
|
||||
pending: string
|
||||
'pipelines.createNewPipeline': string
|
||||
'pipelines.created': string
|
||||
'pipelines.editPipeline': string
|
||||
'pipelines.enterPipelineName': string
|
||||
'pipelines.enterYAMLPath': string
|
||||
'pipelines.executionCouldNotStart': string
|
||||
'pipelines.executionStarted': string
|
||||
'pipelines.failedToCreatePipeline': string
|
||||
'pipelines.lastExecution': string
|
||||
'pipelines.name': string
|
||||
'pipelines.newPipelineButton': string
|
||||
'pipelines.noData': string
|
||||
'pipelines.run': string
|
||||
'pipelines.saveAndRun': string
|
||||
'pipelines.time': string
|
||||
poweredByAI: string
|
||||
'pipelines.updated': string
|
||||
'pipelines.yamlPath': string
|
||||
'plugins.addAPlugin': string
|
||||
'plugins.stepLabel': string
|
||||
'plugins.title': string
|
||||
'pr.ableToMerge': string
|
||||
'pr.addDescription': string
|
||||
'pr.authorCommentedPR': string
|
||||
@ -391,6 +413,7 @@ export interface StringsMap {
|
||||
'prChecks.notFound': string
|
||||
'prChecks.pending': string
|
||||
'prChecks.running': string
|
||||
'prChecks.skipped': string
|
||||
'prChecks.success': string
|
||||
'prChecks.viewExternal': string
|
||||
prMustSelectSourceAndTargetBranches: string
|
||||
@ -455,6 +478,7 @@ export interface StringsMap {
|
||||
reviewerNotFound: string
|
||||
reviewers: string
|
||||
role: string
|
||||
run: string
|
||||
running: string
|
||||
samplePayloadUrl: string
|
||||
save: string
|
||||
@ -462,6 +486,7 @@ export interface StringsMap {
|
||||
scrollToTop: string
|
||||
search: string
|
||||
searchBranches: string
|
||||
searchResult: string
|
||||
secret: string
|
||||
'secrets.createSecret': string
|
||||
'secrets.createSuccess': string
|
||||
@ -488,6 +513,7 @@ export interface StringsMap {
|
||||
showMore: string
|
||||
signIn: string
|
||||
signUp: string
|
||||
skipped: string
|
||||
space: string
|
||||
'spaceMemberships.addMember': string
|
||||
'spaceMemberships.changeRole': string
|
||||
@ -506,6 +532,7 @@ export interface StringsMap {
|
||||
'spaceSetting.setting': string
|
||||
spaces: string
|
||||
sslVerificationLabel: string
|
||||
startSearching: string
|
||||
status: string
|
||||
submitReview: string
|
||||
success: string
|
||||
@ -565,6 +592,7 @@ export interface StringsMap {
|
||||
viewAllTags: string
|
||||
viewCommitDetails: string
|
||||
viewFile: string
|
||||
viewFileHistory: string
|
||||
viewFiles: string
|
||||
viewRaw: string
|
||||
viewRepo: string
|
||||
|
@ -1,8 +0,0 @@
|
||||
// temp file to hide open source pipelines and secrets - can be extended if needs be
|
||||
|
||||
const featureFlags = {
|
||||
OPEN_SOURCE_PIPELINES: false,
|
||||
OPEN_SOURCE_SECRETS: false
|
||||
}
|
||||
|
||||
export const useFeatureFlag = (): Record<keyof typeof featureFlags, boolean> => featureFlags
|
@ -63,6 +63,11 @@ export function usePRChecksDecision({
|
||||
setColor(Color.GREY_600)
|
||||
setBackground(Color.GREY_100)
|
||||
setMessage(stringSubstitute(getString('prChecks.pending'), { count: _count.pending, total }) as string)
|
||||
} else if (_count.skipped) {
|
||||
_status = ExecutionState.SKIPPED
|
||||
setColor(Color.GREY_600)
|
||||
setBackground(Color.GREY_100)
|
||||
setMessage(stringSubstitute(getString('prChecks.skipped'), { count: _count.pending, total }) as string)
|
||||
} else if (_count.success) {
|
||||
_status = ExecutionState.SUCCESS
|
||||
setColor(Color.GREEN_800)
|
||||
@ -110,5 +115,6 @@ const DEFAULT_COUNTS = {
|
||||
failure: 0,
|
||||
pending: 0,
|
||||
running: 0,
|
||||
success: 0
|
||||
success: 0,
|
||||
skipped: 0
|
||||
}
|
||||
|
@ -96,6 +96,7 @@ pageTitle:
|
||||
pipelines: Pipelines
|
||||
secrets: Secrets
|
||||
executions: Executions
|
||||
search: Search powered by AI
|
||||
repos:
|
||||
name: Repo Name
|
||||
data: Repo Data
|
||||
@ -298,6 +299,7 @@ prChecks:
|
||||
failure: '{count}/{total} {count|1:check,checks} failed.'
|
||||
running: '{count}/{total} {count|1:check,checks} running.'
|
||||
pending: '{count}/{total} {count|1:check,checks} pending.'
|
||||
skipped: '{count}/{total} {count|1:check,checks} skipped.'
|
||||
success: '{count}/{total} {count|1:check,checks} succeeded.'
|
||||
notFound: No pipelines or external checks found for this repository.
|
||||
viewExternal: View Details
|
||||
@ -416,6 +418,7 @@ makeRequired: Make Required
|
||||
makeOptional: Make Optional
|
||||
remove: Remove
|
||||
required: Required
|
||||
isRequired: is required
|
||||
noneYet: None Yet
|
||||
noOptionalReviewers: No Optional Reviewers
|
||||
noRequiredReviewers: No Required Reviewers
|
||||
@ -480,13 +483,14 @@ newTag: New Tag
|
||||
overview: Overview
|
||||
fileTooLarge: File is too large to open. {download}
|
||||
clickHereToDownload: Click here to download.
|
||||
viewFile: View the file at this point in the history
|
||||
viewFileHistory: View the file at this point in the history
|
||||
viewRepo: View the repository at this point in the history
|
||||
hideCommitHistory: Renamed from {file} - Hide History
|
||||
showCommitHistory: Renamed from {file} - Show History
|
||||
noReviewers: No Reviewers
|
||||
assignPeople: Assign people
|
||||
add: Add +
|
||||
addLabel: Add
|
||||
users: Users
|
||||
findAUser: Find a user
|
||||
reviewerNotFound: 'Reviewer <strong>{{reviewer}}</strong> not found.'
|
||||
@ -586,6 +590,7 @@ spaceMemberships:
|
||||
memberAdded: Member added successfully.
|
||||
failedToCreateSpace: Failed to create Space. Please try again.
|
||||
failedToCreatePipeline: Failed to create Pipeline. Please try again.
|
||||
failedToSavePipeline: Failed to save Pipeline. Please try again.
|
||||
enterName: Enter the name
|
||||
createASpace: Create a space
|
||||
createSpace: Create Space
|
||||
@ -613,6 +618,7 @@ running: Running
|
||||
success: Success
|
||||
failed: Failed
|
||||
error: Error
|
||||
skipped: Skipped
|
||||
repoDelete:
|
||||
title: Delete Repository
|
||||
deleteConfirm1: This will permanently delete the "{{repo}}" repository, wiki, issues, comments, packages, secrets, workflow runs, and remove all collaborator associations.
|
||||
@ -624,8 +630,20 @@ pipelines:
|
||||
noData: There are no pipelines :(
|
||||
newPipelineButton: New Pipeline
|
||||
name: Pipeline Name
|
||||
createNewPipeline: Create New Pipeline
|
||||
enterPipelineName: Enter pipeline name
|
||||
yamlPath: YAML Path
|
||||
enterYAMLPath: Enter YAML path
|
||||
failedToCreatePipeline: Failed to create pipeline
|
||||
saveAndRun: Save and Run
|
||||
editPipeline: Edit pipeline {{pipeline}}
|
||||
run: Run pipeline
|
||||
time: Time
|
||||
lastExecution: Last Execution
|
||||
created: Pipeline created successfully
|
||||
updated: Pipeline updated successfully
|
||||
executionStarted: Pipeline execution started successfully
|
||||
executionCouldNotStart: Failure while starting Pipeline execution
|
||||
executions:
|
||||
noData: There are no executions :(
|
||||
newExecutionButton: Run Pipeline
|
||||
@ -652,3 +670,14 @@ secrets:
|
||||
failedToDeleteSecret: Failed to delete Secret. Please try again.
|
||||
deleteSecret: Delete Secrets
|
||||
userUpdateSuccess: 'User updated successfully'
|
||||
viewFile: View File
|
||||
searchResult: 'Search Result {count}'
|
||||
aiSearch: AI Search
|
||||
codeSearch: Code Search
|
||||
startSearching: Begin search by describing what you are looking for.
|
||||
poweredByAI: Unlock the power of AI with Semantic Code search. Try phrases like "Locate the code for authentication".
|
||||
run: Run
|
||||
plugins:
|
||||
title: Plugins
|
||||
addAPlugin: Add a {{category}} plugin
|
||||
stepLabel: step
|
||||
|
@ -11,6 +11,7 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
.settings {
|
||||
margin: 0 var(--spacing-medium);
|
||||
|
@ -7,13 +7,12 @@ import { useStrings } from 'framework/strings'
|
||||
import type { TypesSpace } from 'services/code'
|
||||
import { SpaceSelector } from 'components/SpaceSelector/SpaceSelector'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useFeatureFlag } from 'hooks/useFeatureFlag'
|
||||
import { NavMenuItem } from './NavMenuItem'
|
||||
import css from './DefaultMenu.module.scss'
|
||||
|
||||
export const DefaultMenu: React.FC = () => {
|
||||
const history = useHistory()
|
||||
const { routes } = useAppContext()
|
||||
const { routes, standalone } = useAppContext()
|
||||
const [selectedSpace, setSelectedSpace] = useState<TypesSpace | undefined>()
|
||||
const { repoMetadata, gitRef, commitRef } = useGetRepositoryMetadata()
|
||||
const { getString } = useStrings()
|
||||
@ -25,7 +24,6 @@ export const DefaultMenu: React.FC = () => {
|
||||
)
|
||||
const isCommitSelected = useMemo(() => routeMatch.path === '/:space*/:repoName/commit/:commitRef*', [routeMatch])
|
||||
|
||||
const { OPEN_SOURCE_PIPELINES, OPEN_SOURCE_SECRETS } = useFeatureFlag()
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<Layout.Vertical spacing="small">
|
||||
@ -118,7 +116,7 @@ export const DefaultMenu: React.FC = () => {
|
||||
})}
|
||||
/>
|
||||
|
||||
{OPEN_SOURCE_PIPELINES && (
|
||||
{standalone && (
|
||||
<NavMenuItem
|
||||
data-code-repo-section="pipelines"
|
||||
isSubLink
|
||||
@ -128,20 +126,30 @@ export const DefaultMenu: React.FC = () => {
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavMenuItem
|
||||
data-code-repo-section="settings"
|
||||
data-code-repo-section="pipelines"
|
||||
isSubLink
|
||||
label={getString('settings')}
|
||||
to={routes.toCODESettings({
|
||||
label={getString('pageTitle.pipelines')}
|
||||
to={routes.toCODEPipelines({
|
||||
repoPath
|
||||
})}
|
||||
/>
|
||||
|
||||
{!standalone && (
|
||||
<NavMenuItem
|
||||
data-code-repo-section="search"
|
||||
isSubLink
|
||||
label={getString('search')}
|
||||
to={routes.toCODESearch({
|
||||
repoPath
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
</Render>
|
||||
|
||||
{OPEN_SOURCE_SECRETS && (
|
||||
{standalone && (
|
||||
<Render when={selectedSpace}>
|
||||
{/* icon is placeholder */}
|
||||
<NavMenuItem
|
||||
|
@ -0,0 +1,34 @@
|
||||
.main {
|
||||
--header-height: 96px;
|
||||
--heading-height: 58px;
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
.layout {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
width: calc(100% - 30vw);
|
||||
height: calc(100vh - var(--header-height)) !important;
|
||||
}
|
||||
|
||||
.pluginsContainer {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.header {
|
||||
a {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--primary-7);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
height: calc(100% - 40px);
|
||||
}
|
9
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.module.scss.d.ts
vendored
Normal file
9
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const breadcrumb: string
|
||||
export declare const drawer: string
|
||||
export declare const editorContainer: string
|
||||
export declare const header: string
|
||||
export declare const layout: string
|
||||
export declare const main: string
|
||||
export declare const pluginsContainer: string
|
327
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.tsx
Normal file
327
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useGet, useMutate } from 'restful-react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { get, isEmpty, isUndefined, set } from 'lodash-es'
|
||||
import { stringify } from 'yaml'
|
||||
import { Menu, PopoverPosition } from '@blueprintjs/core'
|
||||
import {
|
||||
Container,
|
||||
PageHeader,
|
||||
PageBody,
|
||||
Layout,
|
||||
ButtonVariation,
|
||||
Text,
|
||||
useToaster,
|
||||
SplitButton,
|
||||
Button
|
||||
} from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import type { OpenapiCommitFilesRequest, RepoCommitFilesResponse, RepoFileContent, TypesPipeline } from 'services/code'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useGetResourceContent } from 'hooks/useGetResourceContent'
|
||||
import MonacoSourceCodeEditor from 'components/SourceCodeEditor/MonacoSourceCodeEditor'
|
||||
import { PluginsPanel } from 'components/PluginsPanel/PluginsPanel'
|
||||
import useRunPipelineModal from 'components/RunPipelineModal/RunPipelineModal'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import type { CODEProps } from 'RouteDefinitions'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { decodeGitContent } from 'utils/GitUtils'
|
||||
import pipelineSchemaV1 from './schema/pipeline-schema-v1.json'
|
||||
import pipelineSchemaV0 from './schema/pipeline-schema-v0.json'
|
||||
import { YamlVersion } from './Constants'
|
||||
|
||||
import css from './AddUpdatePipeline.module.scss'
|
||||
|
||||
const StarterPipelineV1: Record<string, any> = {
|
||||
version: 1,
|
||||
stages: [
|
||||
{
|
||||
type: 'ci',
|
||||
spec: {
|
||||
steps: [
|
||||
{
|
||||
type: 'script',
|
||||
spec: {
|
||||
run: 'echo hello world'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const StarterPipelineV0: Record<string, any> = {
|
||||
kind: 'pipeline',
|
||||
type: 'docker',
|
||||
name: 'default',
|
||||
steps: [
|
||||
{
|
||||
name: 'test',
|
||||
image: 'alpine',
|
||||
commands: ['echo hello world']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
enum PipelineSaveAndRunAction {
|
||||
SAVE,
|
||||
RUN,
|
||||
SAVE_AND_RUN
|
||||
}
|
||||
|
||||
interface PipelineSaveAndRunOption {
|
||||
title: string
|
||||
action: PipelineSaveAndRunAction
|
||||
}
|
||||
|
||||
const AddUpdatePipeline = (): JSX.Element => {
|
||||
const version = YamlVersion.V0
|
||||
const { routes } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
const { pipeline } = useParams<CODEProps>()
|
||||
const { repoMetadata } = useGetRepositoryMetadata()
|
||||
const { showError, showSuccess, clear: clearToaster } = useToaster()
|
||||
const [pipelineAsObj, setPipelineAsObj] = useState<Record<string, any>>(
|
||||
version === YamlVersion.V0 ? StarterPipelineV0 : StarterPipelineV1
|
||||
)
|
||||
const [pipelineAsYAML, setPipelineAsYaml] = useState<string>('')
|
||||
const { openModal: openRunPipelineModal } = useRunPipelineModal()
|
||||
const repoPath = useMemo(() => repoMetadata?.path || '', [repoMetadata])
|
||||
const [isExistingPipeline, setIsExistingPipeline] = useState<boolean>(false)
|
||||
const [isDirty, setIsDirty] = useState<boolean>(false)
|
||||
|
||||
const pipelineSaveOption: PipelineSaveAndRunOption = {
|
||||
title: getString('save'),
|
||||
action: PipelineSaveAndRunAction.SAVE
|
||||
}
|
||||
|
||||
const pipelineRunOption: PipelineSaveAndRunOption = {
|
||||
title: getString('run'),
|
||||
action: PipelineSaveAndRunAction.RUN
|
||||
}
|
||||
|
||||
const pipelineSaveAndRunOption: PipelineSaveAndRunOption = {
|
||||
title: getString('pipelines.saveAndRun'),
|
||||
action: PipelineSaveAndRunAction.SAVE_AND_RUN
|
||||
}
|
||||
|
||||
const pipelineSaveAndRunOptions: PipelineSaveAndRunOption[] = [pipelineSaveAndRunOption, pipelineSaveOption]
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<PipelineSaveAndRunOption>()
|
||||
|
||||
const { mutate, loading } = useMutate<RepoCommitFilesResponse>({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoPath}/+/commits`
|
||||
})
|
||||
|
||||
// Fetch pipeline metadata to fetch pipeline YAML file content
|
||||
const { data: pipelineData, loading: fetchingPipeline } = useGet<TypesPipeline>({
|
||||
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`,
|
||||
lazy: !repoMetadata
|
||||
})
|
||||
|
||||
const {
|
||||
data: pipelineYAMLFileContent,
|
||||
loading: fetchingPipelineYAMLFileContent,
|
||||
refetch: fetchPipelineYAMLFileContent
|
||||
} = useGetResourceContent({
|
||||
repoMetadata,
|
||||
gitRef: pipelineData?.default_branch || '',
|
||||
resourcePath: pipelineData?.config_path || ''
|
||||
})
|
||||
|
||||
const originalPipelineYAMLFileContent = useMemo(
|
||||
(): string => decodeGitContent((pipelineYAMLFileContent?.content as RepoFileContent)?.data),
|
||||
[pipelineYAMLFileContent?.content]
|
||||
)
|
||||
|
||||
// check if file already exists and has some content
|
||||
useEffect(() => {
|
||||
setIsExistingPipeline(!isEmpty(originalPipelineYAMLFileContent) && !isUndefined(originalPipelineYAMLFileContent))
|
||||
}, [originalPipelineYAMLFileContent])
|
||||
|
||||
// load initial content on the editor
|
||||
useEffect(() => {
|
||||
if (isExistingPipeline) {
|
||||
setPipelineAsYaml(originalPipelineYAMLFileContent)
|
||||
} else {
|
||||
// load with starter pipeline
|
||||
try {
|
||||
setPipelineAsYaml(stringify(pipelineAsObj))
|
||||
} catch (ex) {
|
||||
// ignore exception
|
||||
}
|
||||
}
|
||||
}, [isExistingPipeline, originalPipelineYAMLFileContent, pipelineAsObj])
|
||||
|
||||
// find if editor content was modified
|
||||
useEffect(() => {
|
||||
setIsDirty(originalPipelineYAMLFileContent !== pipelineAsYAML)
|
||||
}, [originalPipelineYAMLFileContent, pipelineAsYAML])
|
||||
|
||||
// set initial CTA title
|
||||
useEffect(() => {
|
||||
setSelectedOption(isDirty ? pipelineSaveAndRunOption : pipelineRunOption)
|
||||
}, [isDirty])
|
||||
|
||||
const handleSaveAndRun = (option: PipelineSaveAndRunOption): void => {
|
||||
if ([PipelineSaveAndRunAction.SAVE_AND_RUN, PipelineSaveAndRunAction.SAVE].includes(option?.action)) {
|
||||
try {
|
||||
const data: OpenapiCommitFilesRequest = {
|
||||
actions: [
|
||||
{
|
||||
action: isExistingPipeline ? 'UPDATE' : 'CREATE',
|
||||
path: pipelineData?.config_path,
|
||||
payload: pipelineAsYAML,
|
||||
sha: isExistingPipeline ? pipelineYAMLFileContent?.sha : ''
|
||||
}
|
||||
],
|
||||
branch: pipelineData?.default_branch || '',
|
||||
title: `${isExistingPipeline ? getString('updated') : getString('created')} pipeline ${pipeline}`,
|
||||
message: ''
|
||||
}
|
||||
|
||||
mutate(data)
|
||||
.then(() => {
|
||||
fetchPipelineYAMLFileContent()
|
||||
clearToaster()
|
||||
showSuccess(getString(isExistingPipeline ? 'pipelines.updated' : 'pipelines.created'))
|
||||
if (option?.action === PipelineSaveAndRunAction.SAVE_AND_RUN && repoMetadata && pipeline) {
|
||||
openRunPipelineModal({ repoMetadata, pipeline })
|
||||
}
|
||||
setSelectedOption(pipelineRunOption)
|
||||
})
|
||||
.catch(error => {
|
||||
showError(getErrorMessage(error), 0, 'pipelines.failedToSavePipeline')
|
||||
})
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception), 0, 'pipelines.failedToSavePipeline')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatePipeline = (payload: Record<string, any>): Record<string, any> => {
|
||||
const pipelineAsObjClone = { ...pipelineAsObj }
|
||||
const stepInsertPath = version === YamlVersion.V0 ? 'steps' : 'stages.0.spec.steps'
|
||||
let existingSteps: [unknown] = get(pipelineAsObjClone, stepInsertPath, [])
|
||||
if (existingSteps.length > 0) {
|
||||
existingSteps.push(payload)
|
||||
} else {
|
||||
existingSteps = [payload]
|
||||
}
|
||||
set(pipelineAsObjClone, stepInsertPath, existingSteps)
|
||||
return pipelineAsObjClone
|
||||
}
|
||||
|
||||
const addUpdatePluginToPipelineYAML = (_isUpdate: boolean, pluginFormData: Record<string, any>): void => {
|
||||
try {
|
||||
const updatedPipelineAsObj = updatePipeline(pluginFormData)
|
||||
setPipelineAsObj(updatedPipelineAsObj)
|
||||
setPipelineAsYaml(stringify(updatedPipelineAsObj))
|
||||
} catch (ex) {
|
||||
// ignore exception
|
||||
}
|
||||
}
|
||||
|
||||
const renderCTA = useCallback(() => {
|
||||
switch (selectedOption?.action) {
|
||||
case PipelineSaveAndRunAction.RUN:
|
||||
return (
|
||||
<Button
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
text={getString('run')}
|
||||
onClick={() => {
|
||||
if (repoMetadata && pipeline) {
|
||||
openRunPipelineModal({ repoMetadata, pipeline })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case PipelineSaveAndRunAction.SAVE:
|
||||
case PipelineSaveAndRunAction.SAVE_AND_RUN:
|
||||
return isExistingPipeline ? (
|
||||
<Button
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
text={getString('save')}
|
||||
onClick={() => {
|
||||
handleSaveAndRun(pipelineSaveOption)
|
||||
}}
|
||||
disabled={loading || !isDirty}
|
||||
/>
|
||||
) : (
|
||||
<SplitButton
|
||||
text={selectedOption?.title}
|
||||
disabled={loading || !isDirty}
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
popoverProps={{
|
||||
interactionKind: 'click',
|
||||
usePortal: true,
|
||||
position: PopoverPosition.BOTTOM_RIGHT,
|
||||
transitionDuration: 1000
|
||||
}}
|
||||
intent="primary"
|
||||
onClick={() => handleSaveAndRun(selectedOption)}>
|
||||
{pipelineSaveAndRunOptions.map(option => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={option.title}
|
||||
text={
|
||||
<Text color={Color.BLACK} font={{ variation: FontVariation.SMALL_BOLD }}>
|
||||
{option.title}
|
||||
</Text>
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedOption(option)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SplitButton>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}, [loading, fetchingPipeline, isDirty, repoMetadata, pipeline, selectedOption, isExistingPipeline, pipelineAsYAML])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className={css.main}>
|
||||
<PageHeader
|
||||
title={getString('pipelines.editPipeline', { pipeline })}
|
||||
breadcrumbs={
|
||||
<Container className={css.header}>
|
||||
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
||||
<Link to={routes.toCODEPipelines({ repoPath })}>{getString('pageTitle.pipelines')}</Link>
|
||||
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
|
||||
<Text font={{ size: 'small' }}>{pipeline}</Text>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
}
|
||||
content={<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>{renderCTA()}</Layout.Horizontal>}
|
||||
/>
|
||||
<PageBody>
|
||||
<LoadingSpinner visible={fetchingPipeline || fetchingPipelineYAMLFileContent} />
|
||||
<Layout.Horizontal>
|
||||
<Container className={css.editorContainer}>
|
||||
<MonacoSourceCodeEditor
|
||||
language={'yaml'}
|
||||
schema={version === YamlVersion.V0 ? pipelineSchemaV0 : pipelineSchemaV1}
|
||||
source={pipelineAsYAML}
|
||||
onChange={(value: string) => setPipelineAsYaml(value)}
|
||||
/>
|
||||
</Container>
|
||||
<Container className={css.pluginsContainer}>
|
||||
<PluginsPanel onPluginAddUpdate={addUpdatePluginToPipelineYAML} />
|
||||
</Container>
|
||||
</Layout.Horizontal>
|
||||
</PageBody>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddUpdatePipeline
|
7
web/src/pages/AddUpdatePipeline/Constants.ts
Normal file
7
web/src/pages/AddUpdatePipeline/Constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum YamlVersion {
|
||||
V0,
|
||||
V1
|
||||
}
|
||||
|
||||
export const DEFAULT_YAML_PATH_PREFIX = '.harness/'
|
||||
export const DEFAULT_YAML_PATH_SUFFIX = '.yaml'
|
@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["pipeline"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"event": {
|
||||
"type": "string"
|
||||
},
|
||||
"branch": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["event"]
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"type": "string"
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"when": {
|
||||
"type": "string",
|
||||
"enum": ["on_success", "on_failure", "always"]
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "commands"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["kind", "name", "steps"]
|
||||
}
|
2372
web/src/pages/AddUpdatePipeline/schema/pipeline-schema-v1.json
Normal file
2372
web/src/pages/AddUpdatePipeline/schema/pipeline-schema-v1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,53 +1,6 @@
|
||||
.main {
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
:global {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useGet } from 'restful-react'
|
||||
import SplitPane from 'react-split-pane'
|
||||
import { routes, type CODEProps } from 'RouteDefinitions'
|
||||
import type { TypesExecution } from 'services/code'
|
||||
import ExecutionStageList from 'components/ExecutionStageList/ExecutionStageList'
|
||||
@ -12,6 +11,7 @@ import { getErrorMessage, voidFn } from 'utils/Utils'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { Split } from 'components/Split/Split'
|
||||
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
|
||||
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||
import { ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
@ -102,7 +102,7 @@ const Execution = () => {
|
||||
}}>
|
||||
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!execution && isInitialLoad} />
|
||||
{execution && (
|
||||
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
|
||||
<Split split="vertical" size={300} minSize={200} maxSize={400}>
|
||||
<ExecutionStageList
|
||||
stages={execution?.stages || []}
|
||||
setSelectedStage={setSelectedStage}
|
||||
@ -111,7 +111,7 @@ const Execution = () => {
|
||||
{selectedStage && (
|
||||
<Console stage={execution?.stages?.[selectedStage - 1]} repoPath={repoMetadata?.path as string} />
|
||||
)}
|
||||
</SplitPane>
|
||||
</Split>
|
||||
)}
|
||||
</PageBody>
|
||||
</Container>
|
||||
|
@ -51,21 +51,4 @@
|
||||
font-size: var(--font-size-normal) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: var(--grey500) !important;
|
||||
font-size: var(--font-size-small) !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;
|
||||
}
|
||||
|
||||
.triggerLayout {
|
||||
align-items: center !important;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const author: string
|
||||
export declare const desc: string
|
||||
export declare const hash: string
|
||||
export declare const layout: string
|
||||
export declare const main: string
|
||||
export declare const name: string
|
||||
export declare const nameContainer: string
|
||||
export declare const number: string
|
||||
export declare const pinned: string
|
||||
export declare const triggerLayout: string
|
||||
export declare const withError: string
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
ButtonVariation,
|
||||
Container,
|
||||
@ -15,7 +14,7 @@ import {
|
||||
import { Color } from '@harnessio/design-system'
|
||||
import cx from 'classnames'
|
||||
import type { CellProps, Column } from 'react-table'
|
||||
import { Link, useHistory, useParams } from 'react-router-dom'
|
||||
import { useHistory, useParams } from 'react-router-dom'
|
||||
import { useGet, useMutate } from 'restful-react'
|
||||
import { Timer, Calendar } from 'iconoir-react'
|
||||
import { useStrings } from 'framework/strings'
|
||||
@ -24,7 +23,7 @@ import { useAppContext } from 'AppContext'
|
||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||
import { LIST_FETCHING_LIMIT, PageBrowserProps, getErrorMessage, timeDistance, voidFn } from 'utils/Utils'
|
||||
import type { CODEProps } from 'RouteDefinitions'
|
||||
import type { TypesExecution } from 'services/code'
|
||||
import type { EnumTriggerAction, TypesExecution } from 'services/code'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import { usePageIndex } from 'hooks/usePageIndex'
|
||||
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
|
||||
@ -32,8 +31,8 @@ import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { getStatus } from 'utils/PipelineUtils'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||
import { ExecutionText, ExecutionTrigger } from 'components/ExecutionText/ExecutionText'
|
||||
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
|
||||
import css from './ExecutionList.module.scss'
|
||||
|
||||
@ -94,6 +93,7 @@ const ExecutionList = () => {
|
||||
//TODO - this should NOT be hardcoded to master branch - need a modal to insert branch - but useful for testing until then
|
||||
await mutate({ branch: 'master' })
|
||||
showSuccess('Build started')
|
||||
executionsRefetch()
|
||||
} catch {
|
||||
showError('Failed to start build')
|
||||
}
|
||||
@ -118,27 +118,21 @@ const ExecutionList = () => {
|
||||
return (
|
||||
<Layout.Vertical className={css.nameContainer}>
|
||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
||||
<ExecutionStatus status={getStatus(record.status)} iconOnly noBackground iconSize={20} />
|
||||
<ExecutionStatus status={getStatus(record.status)} iconOnly noBackground iconSize={20} isCi />
|
||||
<Text className={css.number}>{`#${record.number}.`}</Text>
|
||||
<Text className={css.desc}>{record.message}</Text>
|
||||
</Layout.Horizontal>
|
||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center', marginLeft: '1.2rem' }}>
|
||||
<Avatar email={record.author_email} name={record.author_name} size="small" hoverCard={false} />
|
||||
{/* TODO need logic here for different trigger types */}
|
||||
<Text className={css.author}>{`${record.author_name} triggered manually`}</Text>
|
||||
<PipeSeparator height={7} />
|
||||
<Link
|
||||
to={routes.toCODECommit({
|
||||
repoPath: repoMetadata?.path as string,
|
||||
commitRef: record.after as string
|
||||
})}
|
||||
className={css.hash}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
{record.after?.slice(0, 6)}
|
||||
</Link>
|
||||
</Layout.Horizontal>
|
||||
<ExecutionText
|
||||
authorEmail={record.author_email as string}
|
||||
authorName={record.author_name as string}
|
||||
repoPath={repoMetadata?.path as string}
|
||||
commitRef={record.after as string}
|
||||
event={record.event as ExecutionTrigger}
|
||||
action={record.action as EnumTriggerAction}
|
||||
target={record.target as string}
|
||||
beforeRef={record.before as string}
|
||||
source={record.source as string}
|
||||
/>
|
||||
</Layout.Vertical>
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
.main {
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
.layout {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Container, PageHeader } from '@harnessio/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import css from './NewPipeline.module.scss'
|
||||
|
||||
const NewPipeline = () => {
|
||||
const { getString } = useStrings()
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<PageHeader title={getString('pipelines.newPipelineButton')} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPipeline
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Classes, Menu, MenuItem, Popover, Position } from '@blueprintjs/core'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
@ -33,6 +34,7 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
||||
import { ExecutionStatus, ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { getStatus } from 'utils/PipelineUtils'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import useNewPipelineModal from 'components/NewPipelineModal/NewPipelineModal'
|
||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||
import noPipelineImage from '../RepositoriesListing/no-repo.svg'
|
||||
@ -40,13 +42,13 @@ import css from './PipelineList.module.scss'
|
||||
|
||||
const PipelineList = () => {
|
||||
const { routes } = useAppContext()
|
||||
const space = useGetSpaceParam()
|
||||
const history = useHistory()
|
||||
const { getString } = useStrings()
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||
const pageBrowser = useQueryParams<PageBrowserProps>()
|
||||
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
|
||||
const [page, setPage] = usePageIndex(pageInit)
|
||||
const space = useGetSpaceParam()
|
||||
|
||||
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
||||
|
||||
@ -62,6 +64,7 @@ const PipelineList = () => {
|
||||
debounce: 500
|
||||
})
|
||||
|
||||
const { openModal } = useNewPipelineModal()
|
||||
//TODO - do not want to show load between refetchs - remove if/when we move to event stream method
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true)
|
||||
|
||||
@ -90,15 +93,17 @@ const PipelineList = () => {
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
history.push(routes.toCODEPipelinesNew({ space }))
|
||||
}}></Button>
|
||||
openModal({ repoMetadata })
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
)
|
||||
|
||||
const columns: Column<TypesPipeline>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: getString('pipelines.name'),
|
||||
width: 'calc(50% - 90px)',
|
||||
width: 'calc(100% - 210px)',
|
||||
Cell: ({ row }: CellProps<TypesPipeline>) => {
|
||||
const record = row.original
|
||||
return (
|
||||
@ -109,6 +114,7 @@ const PipelineList = () => {
|
||||
noBackground
|
||||
iconSize={24}
|
||||
className={css.statusIcon}
|
||||
isCi
|
||||
/>
|
||||
<Text className={css.repoName}>
|
||||
<Keywords value={searchTerm}>{record.uid}</Keywords>
|
||||
@ -128,7 +134,7 @@ const PipelineList = () => {
|
||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
||||
<Text className={css.desc}>{`#${record.number}`}</Text>
|
||||
<PipeSeparator height={7} />
|
||||
<Text className={css.desc}>{record.message}</Text>
|
||||
<Text className={css.desc}>{record.title || record.message}</Text>
|
||||
</Layout.Horizontal>
|
||||
<Layout.Horizontal spacing={'xsmall'} style={{ alignItems: 'center' }}>
|
||||
<Avatar
|
||||
@ -140,9 +146,13 @@ const PipelineList = () => {
|
||||
/>
|
||||
{/* TODO need logic here for different trigger types */}
|
||||
<Text className={css.author}>{record.author_name}</Text>
|
||||
<PipeSeparator height={7} />
|
||||
<GitFork height={12} width={12} color={Utils.getRealCSSColor(Color.GREY_500)} />
|
||||
<Text className={css.author}>{record.source}</Text>
|
||||
{record.target && (
|
||||
<>
|
||||
<PipeSeparator height={7} />
|
||||
<GitFork height={12} width={12} color={Utils.getRealCSSColor(Color.GREY_500)} />
|
||||
<Text className={css.author}>{record.target.split('/').pop()}</Text>
|
||||
</>
|
||||
)}
|
||||
<PipeSeparator height={7} />
|
||||
<Link
|
||||
to={routes.toCODECommit({
|
||||
@ -188,6 +198,46 @@ const PipelineList = () => {
|
||||
)
|
||||
},
|
||||
disableSortBy: true
|
||||
},
|
||||
{
|
||||
Header: ' ',
|
||||
width: '30px',
|
||||
Cell: ({ row }: CellProps<TypesPipeline>) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const record = row.original
|
||||
const { uid } = record
|
||||
return (
|
||||
<Popover
|
||||
isOpen={menuOpen}
|
||||
onInteraction={nextOpenState => {
|
||||
setMenuOpen(nextOpenState)
|
||||
}}
|
||||
className={Classes.DARK}
|
||||
position={Position.BOTTOM_RIGHT}>
|
||||
<Button
|
||||
variation={ButtonVariation.ICON}
|
||||
icon="Options"
|
||||
data-testid={`menu-${record.uid}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
setMenuOpen(true)
|
||||
}}
|
||||
/>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon="edit"
|
||||
text={getString('edit')}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
history.push(
|
||||
routes.toCODEPipelineEdit({ repoPath: repoMetadata?.path || '', pipeline: uid as string })
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
[getString, repoMetadata?.path, routes, searchTerm]
|
||||
|
@ -139,53 +139,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Falsy, Match, Render, Truthy } from 'react-jsx-match'
|
||||
import { /*CheckCircle,*/ NavArrowRight } from 'iconoir-react'
|
||||
import SplitPane from 'react-split-pane'
|
||||
import { get } from 'lodash-es'
|
||||
import cx from 'classnames'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
@ -24,6 +23,7 @@ import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { Split } from 'components/Split/Split'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
||||
import type { TypesCheck } from 'services/code'
|
||||
@ -56,7 +56,7 @@ export const Checks: React.FC<ChecksProps> = props => {
|
||||
<Container className={css.main}>
|
||||
<Match expr={props.prChecksDecisionResult?.overallStatus}>
|
||||
<Truthy>
|
||||
<SplitPane
|
||||
<Split
|
||||
split="vertical"
|
||||
size="calc(100% - 400px)"
|
||||
minSize={800}
|
||||
@ -118,7 +118,7 @@ export const Checks: React.FC<ChecksProps> = props => {
|
||||
</Falsy>
|
||||
</Match>
|
||||
</Container>
|
||||
</SplitPane>
|
||||
</Split>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<Container flex={{ align: 'center-center' }} height="90%">
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Container, Layout, PageBody, Tabs, Text } from '@harnessio/uicore'
|
||||
import { FontVariation } from '@harnessio/design-system'
|
||||
import { useGet } from 'restful-react'
|
||||
import { useGet, useMutate } from 'restful-react'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { compact } from 'lodash-es'
|
||||
@ -9,7 +9,7 @@ import { useAppContext } from 'AppContext'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { voidFn, getErrorMessage, PullRequestSection } from 'utils/Utils'
|
||||
import { voidFn, getErrorMessage, PullRequestSection, MergeCheckStatus } from 'utils/Utils'
|
||||
import { CodeIcon } from 'utils/GitUtils'
|
||||
import type { TypesPullReq, TypesPullReqStats, TypesRepository } from 'services/code'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
@ -83,6 +83,14 @@ export default function PullRequest() {
|
||||
})
|
||||
)
|
||||
}, [history, routes, repoMetadata?.path, pullRequestId])
|
||||
const recheckPath = useMemo(
|
||||
() => `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}/recheck`,
|
||||
[repoMetadata?.path, pullRequestId]
|
||||
)
|
||||
const { mutate : recheckPR, loading: loadingRecheckPR } = useMutate({
|
||||
verb: 'POST',
|
||||
path: recheckPath,
|
||||
})
|
||||
|
||||
useEffect(
|
||||
function setStatsIfNotSet() {
|
||||
@ -98,6 +106,27 @@ export default function PullRequest() {
|
||||
useEffect(
|
||||
function setPrDataIfNotSet() {
|
||||
if (pullRequestData) {
|
||||
// recheck pr (merge-check, ...) in case it's unavailable
|
||||
// Approximation of identifying target branch update:
|
||||
// 1. branch got updated before page was loaded (status is unchecked and prData is empty)
|
||||
// NOTE: This doesn't guarantee the status is UNCHECKED due to target branch update and can cause duplicate
|
||||
// PR merge checks being run on PR creation or source branch update.
|
||||
// 2. branch got updated while we are on the page (same source_sha but status changed to UNCHECKED)
|
||||
// NOTE: This doesn't cover the case in which the status changed back to UNCHECKED before the PR is refetched.
|
||||
// In that case, the user will have to re-open the PR - better than us spamming the backend with rechecks.
|
||||
// This is a TEMPORARY SOLUTION and will most likely change in the future (more so on backend side)
|
||||
if (pullRequestData.state == 'open' &&
|
||||
pullRequestData.merge_check_status == MergeCheckStatus.UNCHECKED &&
|
||||
(
|
||||
// case 1:
|
||||
!prData ||
|
||||
// case 2:
|
||||
(prData?.merge_check_status != MergeCheckStatus.UNCHECKED && prData?.source_sha == pullRequestData.source_sha)
|
||||
) && !loadingRecheckPR) {
|
||||
// best effort attempt to recheck PR - fail silently
|
||||
recheckPR({})
|
||||
}
|
||||
|
||||
setPrData(pullRequestData)
|
||||
}
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
.main {
|
||||
padding: var(--spacing-large) var(--spacing-xlarge) 0 var(--spacing-xlarge) !important;
|
||||
position: relative;
|
||||
|
||||
div[class*='TextInput'] {
|
||||
margin-bottom: 0 !important;
|
||||
@ -54,7 +55,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbItem {
|
||||
white-space: nowrap !important;
|
||||
.searchBox {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: -50px;
|
||||
z-index: 2;
|
||||
padding-bottom: 0 !important;
|
||||
margin: 0;
|
||||
|
||||
input,
|
||||
input:focus {
|
||||
border: 1px solid var(--ai-purple-600) !important;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 350px !important;
|
||||
}
|
||||
|
||||
svg path {
|
||||
fill: var(--ai-purple-600) !important;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const breadcrumbItem: string
|
||||
export declare const btnColorFix: string
|
||||
export declare const main: string
|
||||
export declare const refRoot: string
|
||||
export declare const rootSlash: string
|
||||
export declare const searchBox: string
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user