This is an automated email from the ASF dual-hosted git repository.

mintsweet pushed a commit to branch feat-dora-config
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git

commit 39c9f8b10491f32b19b08752f43b3608399cf86a
Author: abeizn <zikuan...@merico.dev>
AuthorDate: Tue Sep 10 16:36:01 2024 +0800

    feat: github/graphql dora config and adjust regex function (#8026)
---
 .../core/models/domainlayer/devops/cicd_task.go    |  10 ++
 .../helpers/pluginhelper/api/enrich_with_regex.go  |  59 +++++++++-
 backend/helpers/pluginhelper/api/pagenation.go     |  24 ++++
 backend/plugins/github/api/scope_config_api.go     | 125 +++++++++++++++++++++
 backend/plugins/github/impl/impl.go                |  17 ++-
 .../20240906_add_env_name_list_to_scope_config.go  |  52 +++++++++
 .../github/models/migrationscripts/register.go     |   1 +
 backend/plugins/github/models/scope_config.go      |   1 +
 .../github_graphql/tasks/deployment_convertor.go   |   2 +-
 9 files changed, 286 insertions(+), 5 deletions(-)

diff --git a/backend/core/models/domainlayer/devops/cicd_task.go 
b/backend/core/models/domainlayer/devops/cicd_task.go
index 5fd00bbe4..098836fda 100644
--- a/backend/core/models/domainlayer/devops/cicd_task.go
+++ b/backend/core/models/domainlayer/devops/cicd_task.go
@@ -36,6 +36,16 @@ const (
 
 const ENV_NAME_PATTERN = "ENV_NAME_PATTERN"
 
+type TransformDeployment struct {
+       Name string `json:"name"`
+       URL  string `json:"url"`
+}
+
+type TransformDeploymentResponse struct {
+       Total int                   `json:"total"`
+       Data  []TransformDeployment `json:"data"`
+}
+
 type CICDTask struct {
        domainlayer.DomainEntity
        Name              string `gorm:"type:varchar(255)"`
diff --git a/backend/helpers/pluginhelper/api/enrich_with_regex.go 
b/backend/helpers/pluginhelper/api/enrich_with_regex.go
index 5e7bd2a3a..d586b8187 100644
--- a/backend/helpers/pluginhelper/api/enrich_with_regex.go
+++ b/backend/helpers/pluginhelper/api/enrich_with_regex.go
@@ -28,12 +28,13 @@ import (
 // TODO: remove Enricher from naming since it is more like a util function
 type RegexEnricher struct {
        // This field will store compiled regular expression for every pattern
-       regexpMap map[string]*regexp.Regexp
+       regexpMap    map[string]*regexp.Regexp
+       regexMapList map[string][]*regexp.Regexp
 }
 
 // NewRegexEnricher initialize a regexEnricher
 func NewRegexEnricher() *RegexEnricher {
-       return &RegexEnricher{regexpMap: make(map[string]*regexp.Regexp)}
+       return &RegexEnricher{regexpMap: make(map[string]*regexp.Regexp), 
regexMapList: make(map[string][]*regexp.Regexp)}
 }
 
 // AddRegexp will add compiled regular expression for pattern to regexpMap
@@ -105,3 +106,57 @@ func (r *RegexEnricher) ReturnNameIfOmittedOrMatched(name 
string, targets ...str
        }
        return r.ReturnNameIfMatched(name, targets...)
 }
+
+// TryAdd a named regexp if given pattern is not empty
+func (r *RegexEnricher) TryAddList(name string, patterns ...string) 
errors.Error {
+       if _, ok := r.regexMapList[name]; ok {
+               return errors.Default.New(fmt.Sprintf("Regex pattern with name: 
%s already exists", name))
+       }
+
+       var regexList []*regexp.Regexp
+       for _, pattern := range patterns {
+               if pattern == "" {
+                       continue
+               }
+               regex, err := errors.Convert01(regexp.Compile(pattern))
+               if err != nil {
+                       return errors.BadInput.Wrap(err, fmt.Sprintf("Fail to 
compile pattern for regex pattern: %s", pattern))
+               }
+               regexList = append(regexList, regex)
+       }
+
+       // Only save non-empty regexList
+       if len(regexList) > 0 {
+               r.regexMapList[name] = regexList
+       }
+       return nil
+}
+
+// ReturnNameIfMatched will return name if any of the targets matches the 
regex associated with the given name
+func (r *RegexEnricher) ReturnNameIfMatchedList(name string, targets 
...string) string {
+       if regexList, ok := r.regexMapList[name]; !ok {
+               return ""
+       } else {
+               for _, regex := range regexList {
+                       matched := false
+                       for _, target := range targets {
+                               if regex.MatchString(target) {
+                                       matched = true
+                                       break
+                               }
+                       }
+                       if !matched {
+                               return "" // If any regex fails to match, 
return ""
+                       }
+               }
+       }
+       return name // Return name if all regex conditions were fulfilled
+}
+
+// ReturnNameIfOmittedOrMatched returns the given name if regex of the given 
name is omitted or fallback to ReturnNameIfMatched
+func (r *RegexEnricher) ReturnNameIfOmittedOrMatchedList(name string, targets 
...string) string {
+       if _, ok := r.regexMapList[name]; !ok {
+               return name
+       }
+       return r.ReturnNameIfMatched(name, targets...)
+}
diff --git a/backend/helpers/pluginhelper/api/pagenation.go 
b/backend/helpers/pluginhelper/api/pagenation.go
index 784ac61e4..6c3ea7e5f 100644
--- a/backend/helpers/pluginhelper/api/pagenation.go
+++ b/backend/helpers/pluginhelper/api/pagenation.go
@@ -20,6 +20,8 @@ package api
 import (
        "net/url"
        "strconv"
+
+       "github.com/go-errors/errors"
 )
 
 const pageSize = 50
@@ -47,3 +49,25 @@ func GetLimitOffset(q url.Values, pageSizeKey, pageKey 
string) (limit int, offse
        offset = (page - 1) * limit
        return limit, offset
 }
+
+func ParsePageParam(body map[string]interface{}, paramName string, 
defaultValue int) (int, error) {
+       value, exists := body[paramName]
+       if !exists {
+               return defaultValue, nil
+       }
+
+       switch v := value.(type) {
+       case int:
+               return v, nil
+       case float64:
+               return int(v), nil
+       case string:
+               parsedValue, err := strconv.Atoi(v)
+               if err != nil {
+                       return 0, errors.New("invalid " + paramName + " value")
+               }
+               return parsedValue, nil
+       default:
+               return 0, errors.New(paramName + " must be int or string")
+       }
+}
diff --git a/backend/plugins/github/api/scope_config_api.go 
b/backend/plugins/github/api/scope_config_api.go
index b109341e7..6d72f6933 100644
--- a/backend/plugins/github/api/scope_config_api.go
+++ b/backend/plugins/github/api/scope_config_api.go
@@ -18,8 +18,13 @@ limitations under the License.
 package api
 
 import (
+       "fmt"
+
+       "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       githubModels "github.com/apache/incubator-devlake/plugins/github/models"
 )
 
 // PostScopeConfig create scope config for Github
@@ -109,3 +114,123 @@ func GetProjectsByScopeConfig(input 
*plugin.ApiResourceInput) (*plugin.ApiResour
 func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return dsHelper.ScopeConfigApi.Delete(input)
 }
+
+// GetScopeConfig return one scope config deployments
+// @Summary return one scope config deployments
+// @Description return one scope config deployments
+// @Tags plugins/github
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200  {object} models.GithubScopeConfigDeployment
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router 
/plugins/github/connections/{connectionId}/scope-configs/{id}/deployments [GET]
+func GetScopeConfigDeployments(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       db := basicRes.GetDal()
+       connectionId := input.Params["connectionId"]
+       var environments []string
+       err := db.All(&environments,
+               dal.From(&githubModels.GithubDeployment{}),
+               dal.Where("connection_id = ?", connectionId),
+               dal.Select("DISTINCT environment"))
+       if err != nil {
+               return nil, err
+       }
+
+       return &plugin.ApiResourceOutput{
+               Body: environments,
+       }, nil
+}
+
+// GetScopeConfig return one scope config deployments
+// @Summary return one scope config deployments
+// @Description return one scope config deployments
+// @Tags plugins/github
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200  {object} models.GithubScopeConfigDeployment
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router 
/plugins/github/connections/{connectionId}/scope-configs/{id}/transform-to-deployments
 [POST]
+func GetScopeConfigTransformToDeployments(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       db := basicRes.GetDal()
+       connectionId := input.Params["connectionId"]
+       deploymentPattern := input.Body["deploymentPattern"]
+       productionPattern := input.Body["productionPattern"]
+       page, err := api.ParsePageParam(input.Body, "page", 1)
+       if err != nil {
+               return nil, errors.Default.New("invalid page value")
+       }
+       pageSize, err := api.ParsePageParam(input.Body, "pageSize", 10)
+       if err != nil {
+               return nil, errors.Default.New("invalid pageSize value")
+       }
+
+       cursor, err := db.RawCursor(`
+               SELECT DISTINCT r.run_number, r.name, r.head_branch, 
r.html_url, r.run_started_at
+               FROM (
+                       SELECT id, run_number, name, head_branch, html_url, 
run_started_at
+                       FROM _tool_github_runs
+                       WHERE connection_id = ? AND name REGEXP ?
+                       AND (name REGEXP ? OR head_branch REGEXP ?)
+                       UNION
+                       SELECT r.id, r.run_number, r.name, r.head_branch, 
r.html_url, r.run_started_at
+                       FROM _tool_github_jobs j
+                       LEFT JOIN _tool_github_runs r ON j.run_id = r.id
+                       WHERE j.connection_id = ? AND j.name REGEXP ?
+                       AND j.name REGEXP ?
+               ) r
+               ORDER BY r.run_started_at DESC
+       `, connectionId, deploymentPattern, productionPattern, 
productionPattern, connectionId, deploymentPattern, productionPattern)
+       if err != nil {
+               return nil, errors.Default.Wrap(err, "error on get")
+       }
+       defer cursor.Close()
+
+       type selectFileds struct {
+               RunNumber  int
+               Name       string
+               HeadBranch string
+               HTMLURL    string
+       }
+       type transformedFields struct {
+               Name string
+               URL  string
+       }
+       var allRuns []transformedFields
+       for cursor.Next() {
+               sf := &selectFileds{}
+               err = db.Fetch(cursor, sf)
+               if err != nil {
+                       return nil, errors.Default.Wrap(err, "error on fetch")
+               }
+               // Directly transform and append to allRuns
+               transformed := transformedFields{
+                       Name: fmt.Sprintf("#%d - %s", sf.RunNumber, sf.Name),
+                       URL:  sf.HTMLURL,
+               }
+               allRuns = append(allRuns, transformed)
+       }
+       // Calculate total count
+       totalCount := len(allRuns)
+
+       // Paginate in memory
+       start := (page - 1) * pageSize
+       end := start + pageSize
+       if start > totalCount {
+               start = totalCount
+       }
+       if end > totalCount {
+               end = totalCount
+       }
+       pagedRuns := allRuns[start:end]
+
+       // Return result containing paged runs and total count
+       result := map[string]interface{}{
+               "total": totalCount,
+               "data":  pagedRuns,
+       }
+       return &plugin.ApiResourceOutput{
+               Body: result,
+       }, nil
+}
diff --git a/backend/plugins/github/impl/impl.go 
b/backend/plugins/github/impl/impl.go
index 07342b786..ac8f4e829 100644
--- a/backend/plugins/github/impl/impl.go
+++ b/backend/plugins/github/impl/impl.go
@@ -164,8 +164,15 @@ func (p Github) PrepareTaskData(taskCtx 
plugin.TaskContext, options map[string]i
        if err = regexEnricher.TryAdd(devops.PRODUCTION, 
op.ScopeConfig.ProductionPattern); err != nil {
                return nil, errors.BadInput.Wrap(err, "invalid value for 
`productionPattern`")
        }
-       if err = regexEnricher.TryAdd(devops.ENV_NAME_PATTERN, 
op.ScopeConfig.EnvNamePattern); err != nil {
-               return nil, errors.BadInput.Wrap(err, "invalid value for 
`envNamePattern`")
+       if len(op.ScopeConfig.EnvNameList) > 0 || 
(len(op.ScopeConfig.EnvNameList) == 0 && op.ScopeConfig.EnvNamePattern == "") {
+               if err = regexEnricher.TryAddList(devops.ENV_NAME_PATTERN, 
op.ScopeConfig.EnvNameList...); err != nil {
+                       return nil, errors.BadInput.Wrap(err, "invalid value 
for `envNameList`")
+               }
+       } else {
+               if err = regexEnricher.TryAdd(devops.ENV_NAME_PATTERN, 
op.ScopeConfig.EnvNamePattern); err != nil {
+                       return nil, errors.BadInput.Wrap(err, "invalid value 
for `envNamePattern`")
+               }
+
        }
        taskData.RegexEnricher = regexEnricher
 
@@ -218,6 +225,12 @@ func (p Github) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":    api.GetScopeConfig,
                        "DELETE": api.DeleteScopeConfig,
                },
+               
"connections/:connectionId/scope-configs/:scopeConfigId/deployments": {
+                       "GET": api.GetScopeConfigDeployments,
+               },
+               
"connections/:connectionId/scope-configs/:scopeConfigId/transform-to-deployments":
 {
+                       "POST": api.GetScopeConfigTransformToDeployments,
+               },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
                },
diff --git 
a/backend/plugins/github/models/migrationscripts/20240906_add_env_name_list_to_scope_config.go
 
b/backend/plugins/github/models/migrationscripts/20240906_add_env_name_list_to_scope_config.go
new file mode 100644
index 000000000..fe08552ad
--- /dev/null
+++ 
b/backend/plugins/github/models/migrationscripts/20240906_add_env_name_list_to_scope_config.go
@@ -0,0 +1,52 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+)
+
+var _ plugin.MigrationScript = (*addEnvNameListToScopeConfig)(nil)
+
+type scopeConfig20240906 struct {
+       EnvNameList []string `gorm:"type:json;serializer:json" 
json:"env_name_list" mapstructure:"env_name_list"`
+}
+
+func (scopeConfig20240906) TableName() string {
+       return "_tool_github_scope_configs"
+}
+
+type addEnvNameListToScopeConfig struct{}
+
+func (*addEnvNameListToScopeConfig) Up(basicRes context.BasicRes) errors.Error 
{
+       db := basicRes.GetDal()
+       if err := db.AutoMigrate(&scopeConfig20240906{}); err != nil {
+               return err
+       }
+       return nil
+}
+
+func (*addEnvNameListToScopeConfig) Version() uint64 {
+       return 20240906142100
+}
+
+func (*addEnvNameListToScopeConfig) Name() string {
+       return "add is_draft to _tool_github_pull_requests"
+}
diff --git a/backend/plugins/github/models/migrationscripts/register.go 
b/backend/plugins/github/models/migrationscripts/register.go
index b8a0722eb..b69d56b85 100644
--- a/backend/plugins/github/models/migrationscripts/register.go
+++ b/backend/plugins/github/models/migrationscripts/register.go
@@ -55,5 +55,6 @@ func All() []plugin.MigrationScript {
                new(addIsDraftToPr),
                new(changeIssueComponentType),
                new(addIndexToGithubJobs),
+               new(addEnvNameListToScopeConfig),
        }
 }
diff --git a/backend/plugins/github/models/scope_config.go 
b/backend/plugins/github/models/scope_config.go
index 60b298d36..e880d500e 100644
--- a/backend/plugins/github/models/scope_config.go
+++ b/backend/plugins/github/models/scope_config.go
@@ -39,6 +39,7 @@ type GithubScopeConfig struct {
        DeploymentPattern    string            
`mapstructure:"deploymentPattern,omitempty" json:"deploymentPattern" 
gorm:"type:varchar(255)"`
        ProductionPattern    string            
`mapstructure:"productionPattern,omitempty" json:"productionPattern" 
gorm:"type:varchar(255)"`
        EnvNamePattern       string            
`mapstructure:"envNamePattern,omitempty" json:"envNamePattern" 
gorm:"type:varchar(255)"`
+       EnvNameList          []string          
`gorm:"type:json;serializer:json" json:"envNameList" mapstructure:"envNameList"`
        Refdiff              datatypes.JSONMap 
`mapstructure:"refdiff,omitempty" json:"refdiff" swaggertype:"object" 
format:"json"`
 }
 
diff --git a/backend/plugins/github_graphql/tasks/deployment_convertor.go 
b/backend/plugins/github_graphql/tasks/deployment_convertor.go
index fdc439553..c0b6a29aa 100644
--- a/backend/plugins/github_graphql/tasks/deployment_convertor.go
+++ b/backend/plugins/github_graphql/tasks/deployment_convertor.go
@@ -104,7 +104,7 @@ func ConvertDeployment(taskCtx plugin.SubTaskContext) 
errors.Error {
                        deploymentCommit.DurationSec = &durationSec
 
                        if data.RegexEnricher != nil {
-                               if 
data.RegexEnricher.ReturnNameIfMatched(devops.ENV_NAME_PATTERN, 
githubDeployment.Environment) != "" {
+                               if 
data.RegexEnricher.ReturnNameIfMatchedList(devops.ENV_NAME_PATTERN, 
githubDeployment.Environment) != "" {
                                        deploymentCommit.Environment = 
devops.PRODUCTION
                                }
                        }

Reply via email to