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

mappjzc pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 9d69664fe Refactor Chage Lead Time - Step 4: generate deployments 
based on cicd pipelines/tasks (#4911)
9d69664fe is described below

commit 9d69664feea42320d3a51d8f08bb595482f7d482
Author: Klesh Wong <[email protected]>
AuthorDate: Thu Apr 13 18:55:49 2023 +0800

    Refactor Chage Lead Time - Step 4: generate deployments based on cicd 
pipelines/tasks (#4911)
    
    * fix: bitbucket pipeline convertor scoping / missed cicd_scope_id
    
    * fix: bitbucket repo_convertor missed cicd_scope conversion
    
    * feat: dora generate deployments from cicd pipelines/tasks
    
    * fix: typo and e2e
---
 .../20230411_add_cicd_deployment_commits.go        |   2 +-
 backend/plugins/bitbucket/e2e/repo_test.go         |  16 ++-
 .../bitbucket/e2e/snapshot_tables/cicd_scopes.csv  |   2 +
 .../plugins/bitbucket/tasks/pipeline_convertor.go  |  16 ++-
 backend/plugins/bitbucket/tasks/repo_convertor.go  |  30 +++-
 backend/plugins/dora/dora.go                       |   2 -
 backend/plugins/dora/impl/impl.go                  |   2 +
 .../dora/tasks/deployment_commits_generator.go     | 156 +++++++++++++++++++++
 8 files changed, 209 insertions(+), 17 deletions(-)

diff --git 
a/backend/core/models/migrationscripts/20230411_add_cicd_deployment_commits.go 
b/backend/core/models/migrationscripts/20230411_add_cicd_deployment_commits.go
index 92eb82196..0c4c16f3d 100644
--- 
a/backend/core/models/migrationscripts/20230411_add_cicd_deployment_commits.go
+++ 
b/backend/core/models/migrationscripts/20230411_add_cicd_deployment_commits.go
@@ -41,5 +41,5 @@ func (*addCicdDeploymentCommits) Version() uint64 {
 }
 
 func (*addCicdDeploymentCommits) Name() string {
-       return "Rename cicd_piopeline_commits repo to repo_url"
+       return "Rename cicd_pipeline_commits repo to repo_url"
 }
diff --git a/backend/plugins/bitbucket/e2e/repo_test.go 
b/backend/plugins/bitbucket/e2e/repo_test.go
index dbd30692f..239bf6c88 100644
--- a/backend/plugins/bitbucket/e2e/repo_test.go
+++ b/backend/plugins/bitbucket/e2e/repo_test.go
@@ -19,8 +19,11 @@ package e2e
 
 import (
        "encoding/json"
+       "testing"
+
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/domainlayer/code"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/devops"
        "github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
        "github.com/apache/incubator-devlake/helpers/e2ehelper"
        "github.com/apache/incubator-devlake/helpers/pluginhelper"
@@ -28,7 +31,6 @@ import (
        "github.com/apache/incubator-devlake/plugins/bitbucket/models"
        "github.com/apache/incubator-devlake/plugins/bitbucket/tasks"
        "github.com/stretchr/testify/assert"
-       "testing"
 )
 
 func TestRepoDataFlow(t *testing.T) {
@@ -79,6 +81,7 @@ func TestRepoDataFlow(t *testing.T) {
        // verify extraction
        dataflowTester.FlushTabler(&code.Repo{})
        dataflowTester.FlushTabler(&ticket.Board{})
+       dataflowTester.FlushTabler(&devops.CicdScope{})
        dataflowTester.Subtask(tasks.ConvertRepoMeta, taskData)
        dataflowTester.VerifyTable(
                code.Repo{},
@@ -105,4 +108,15 @@ func TestRepoDataFlow(t *testing.T) {
                        "created_date",
                ),
        )
+       dataflowTester.VerifyTable(
+               devops.CicdScope{},
+               "./snapshot_tables/cicd_scopes.csv",
+               e2ehelper.ColumnWithRawData(
+                       "id",
+                       "name",
+                       "description",
+                       "url",
+                       "created_date",
+               ),
+       )
 }
diff --git a/backend/plugins/bitbucket/e2e/snapshot_tables/cicd_scopes.csv 
b/backend/plugins/bitbucket/e2e/snapshot_tables/cicd_scopes.csv
new file mode 100644
index 000000000..cf77c6767
--- /dev/null
+++ b/backend/plugins/bitbucket/e2e/snapshot_tables/cicd_scopes.csv
@@ -0,0 +1,2 @@
+id,name,description,url,created_date,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark
+bitbucket:BitbucketRepo:1:likyh/likyhphp,likyhphp,,https://bitbucket.org/likyh/likyhphp/issues,2022-06-17T03:27:18.865+00:00,,,0,
diff --git a/backend/plugins/bitbucket/tasks/pipeline_convertor.go 
b/backend/plugins/bitbucket/tasks/pipeline_convertor.go
index 673fa09fa..3f38cd291 100644
--- a/backend/plugins/bitbucket/tasks/pipeline_convertor.go
+++ b/backend/plugins/bitbucket/tasks/pipeline_convertor.go
@@ -48,8 +48,12 @@ func ConvertPipelines(taskCtx plugin.SubTaskContext) 
errors.Error {
        if err != nil {
                return err
        }
+       repoId := 
didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(repo.ConnectionId,
 repo.BitbucketId)
 
-       cursor, err := db.Cursor(dal.From(models.BitbucketPipeline{}))
+       cursor, err := db.Cursor(
+               dal.From(models.BitbucketPipeline{}),
+               dal.Where("connection_id = ? AND repo_id = ?", 
data.Options.ConnectionId, data.Options.FullName),
+       )
        if err != nil {
                return err
        }
@@ -71,11 +75,10 @@ func ConvertPipelines(taskCtx plugin.SubTaskContext) 
errors.Error {
                        results := make([]interface{}, 0, 2)
                        domainPipelineCommit := &devops.CiCDPipelineCommit{
                                PipelineId: 
pipelineIdGen.Generate(data.Options.ConnectionId, 
bitbucketPipeline.BitbucketId),
-                               RepoId: 
didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).
-                                       
Generate(bitbucketPipeline.ConnectionId, bitbucketPipeline.RepoId),
-                               CommitSha: bitbucketPipeline.CommitSha,
-                               Branch:    bitbucketPipeline.RefName,
-                               RepoUrl:   repo.HTMLUrl,
+                               RepoId:     repoId,
+                               CommitSha:  bitbucketPipeline.CommitSha,
+                               Branch:     bitbucketPipeline.RefName,
+                               RepoUrl:    repo.HTMLUrl,
                        }
                        domainPipeline := &devops.CICDPipeline{
                                DomainEntity: domainlayer.DomainEntity{
@@ -99,6 +102,7 @@ func ConvertPipelines(taskCtx plugin.SubTaskContext) 
errors.Error {
                                CreatedDate:  createdAt,
                                DurationSec:  
bitbucketPipeline.DurationInSeconds,
                                FinishedDate: 
bitbucketPipeline.BitbucketCompleteOn,
+                               CicdScopeId:  repoId,
                        }
                        results = append(results, domainPipelineCommit, 
domainPipeline)
                        return results, nil
diff --git a/backend/plugins/bitbucket/tasks/repo_convertor.go 
b/backend/plugins/bitbucket/tasks/repo_convertor.go
index 90e6fc9c4..48a49cfc8 100644
--- a/backend/plugins/bitbucket/tasks/repo_convertor.go
+++ b/backend/plugins/bitbucket/tasks/repo_convertor.go
@@ -20,20 +20,22 @@ package tasks
 import (
        "encoding/json"
        "fmt"
+       "io"
+       "net/http"
+       "path"
+       "reflect"
+
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/domainlayer"
        "github.com/apache/incubator-devlake/core/models/domainlayer/code"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/devops"
        "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
        "github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
        plugin "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        aha 
"github.com/apache/incubator-devlake/helpers/pluginhelper/api/apihelperabstract"
        "github.com/apache/incubator-devlake/plugins/bitbucket/models"
-       "io"
-       "net/http"
-       "path"
-       "reflect"
 )
 
 const RAW_REPOSITORIES_TABLE = "bitbucket_api_repositories"
@@ -87,7 +89,7 @@ func ConvertRepo(taskCtx plugin.SubTaskContext) errors.Error {
 
        cursor, err := db.Cursor(
                dal.From(&models.BitbucketRepo{}),
-               dal.Where("bitbucket_id = ?", repoId),
+               dal.Where("connection_id = ? AND bitbucket_id = ?", 
data.Options.ConnectionId, repoId),
        )
        if err != nil {
                return err
@@ -102,9 +104,12 @@ func ConvertRepo(taskCtx plugin.SubTaskContext) 
errors.Error {
                RawDataSubTaskArgs: *rawDataSubTaskArgs,
                Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
                        repository := inputRow.(*models.BitbucketRepo)
+
+                       repoId := repoIdGen.Generate(data.Options.ConnectionId, 
repository.BitbucketId)
+
                        domainRepository := &code.Repo{
                                DomainEntity: domainlayer.DomainEntity{
-                                       Id: 
repoIdGen.Generate(data.Options.ConnectionId, repository.BitbucketId),
+                                       Id: repoId,
                                },
                                Name:        repository.Name,
                                Url:         repository.HTMLUrl,
@@ -116,7 +121,17 @@ func ConvertRepo(taskCtx plugin.SubTaskContext) 
errors.Error {
 
                        domainBoard := &ticket.Board{
                                DomainEntity: domainlayer.DomainEntity{
-                                       Id: 
repoIdGen.Generate(data.Options.ConnectionId, repository.BitbucketId),
+                                       Id: repoId,
+                               },
+                               Name:        repository.Name,
+                               Url:         fmt.Sprintf("%s/%s", 
repository.HTMLUrl, "issues"),
+                               Description: repository.Description,
+                               CreatedDate: repository.CreatedDate,
+                       }
+
+                       domainCicdScope := &devops.CicdScope{
+                               DomainEntity: domainlayer.DomainEntity{
+                                       Id: repoId,
                                },
                                Name:        repository.Name,
                                Url:         fmt.Sprintf("%s/%s", 
repository.HTMLUrl, "issues"),
@@ -127,6 +142,7 @@ func ConvertRepo(taskCtx plugin.SubTaskContext) 
errors.Error {
                        return []interface{}{
                                domainRepository,
                                domainBoard,
+                               domainCicdScope,
                        }, nil
                },
        })
diff --git a/backend/plugins/dora/dora.go b/backend/plugins/dora/dora.go
index cbe95f772..a4ee412d3 100644
--- a/backend/plugins/dora/dora.go
+++ b/backend/plugins/dora/dora.go
@@ -30,12 +30,10 @@ var PluginEntry impl.Dora //nolint
 func main() {
        cmd := &cobra.Command{Use: "dora"}
 
-       repoId := cmd.Flags().StringP("repoId", "r", "", "repo id")
        projectName := cmd.Flags().StringP("projectName", "p", "", "project 
name")
 
        cmd.Run = func(cmd *cobra.Command, args []string) {
                runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
-                       "repoId":      *repoId,
                        "projectName": *projectName,
                })
        }
diff --git a/backend/plugins/dora/impl/impl.go 
b/backend/plugins/dora/impl/impl.go
index 892343acd..e4bbcfaa8 100644
--- a/backend/plugins/dora/impl/impl.go
+++ b/backend/plugins/dora/impl/impl.go
@@ -19,6 +19,7 @@ package impl
 
 import (
        "encoding/json"
+
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
@@ -81,6 +82,7 @@ func (p Dora) Settings() interface{} {
 
 func (p Dora) SubTaskMetas() []plugin.SubTaskMeta {
        return []plugin.SubTaskMeta{
+               tasks.DeploymentCommitsGeneratorMeta,
                tasks.EnrichTaskEnvMeta,
                tasks.CalculateChangeLeadTimeMeta,
                tasks.ConnectIncidentToDeploymentMeta,
diff --git a/backend/plugins/dora/tasks/deployment_commits_generator.go 
b/backend/plugins/dora/tasks/deployment_commits_generator.go
new file mode 100644
index 000000000..2aae4a4d7
--- /dev/null
+++ b/backend/plugins/dora/tasks/deployment_commits_generator.go
@@ -0,0 +1,156 @@
+/*
+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 tasks
+
+import (
+       "fmt"
+       "reflect"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/models/domainlayer"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/devops"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+var DeploymentCommitsGeneratorMeta = plugin.SubTaskMeta{
+       Name:             "generateDeploymentCommits",
+       EntryPoint:       GenerateDeploymentCommits,
+       EnabledByDefault: false, // it should be executed before 
refdiff.calculateDeploymentCommitsDiff, check 
https://github.com/apache/incubator-devlake/issues/4869 for detail
+       Description:      "Generate deployment_commits from 
cicd_pipeline_commits if cicd_pipeline.type == DEPLOYMENT or any of its 
cicd_tasks is a deployment task",
+       DomainTypes:      []string{plugin.DOMAIN_TYPE_CICD},
+}
+
+type pipelineCommitEx struct {
+       devops.CiCDPipelineCommit
+       PipelineName       string
+       Result             string
+       Status             string
+       DurationSec        *uint64
+       CreatedDate        *time.Time
+       FinishedDate       *time.Time
+       Environment        string
+       CicdScopeId        string
+       HasTestingTasks    bool
+       HasStagingTasks    bool
+       HasProductionTasks bool
+}
+
+func GenerateDeploymentCommits(taskCtx plugin.SubTaskContext) errors.Error {
+       db := taskCtx.GetDal()
+       data := taskCtx.GetData().(*DoraTaskData)
+       // select all cicd_pipeline_commits from all "Deployments" in the 
project
+       // Note that failed records shall be included as well
+       cursor, err := db.Cursor(
+               dal.Select(`
+                       pc.*, p.name as pipeline_name,
+                       p.result,
+                       p.status,
+                       p.duration_sec,
+                       p.created_date,
+                       p.finished_date,
+                       p.environment,
+                       p.cicd_scope_id,
+                       EXISTS(SELECT 1 FROM cicd_tasks t WHERE t.pipeline_id = 
p.id AND t.environment = ?)
+                        as has_testing_tasks,
+                       EXISTS(SELECT 1 FROM cicd_tasks t WHERE t.pipeline_id = 
p.id AND t.environment = ?)
+                        as has_staging_tasks,
+                       EXISTS( SELECT 1 FROM cicd_tasks t WHERE t.pipeline_id 
= p.id AND t.environment = ?)
+                        as has_production_tasks
+               `),
+               dal.From("cicd_pipeline_commits pc"),
+               dal.Join("LEFT JOIN cicd_pipelines p ON (p.id = 
pc.pipeline_id)"),
+               dal.Join("LEFT JOIN project_mapping pm ON (pm.table = ? AND 
pm.row_id = p.cicd_scope_id)"),
+               dal.Where(
+                       `
+                       pm.project_name = ? AND (
+                               p.type = ? OR EXISTS(
+                                       SELECT 1 FROM cicd_tasks t WHERE 
t.pipeline_id = p.id AND t.type = p.type
+                               )
+                       )
+                       `, devops.STAGING, devops.PRODUCTION, "cicd_scopes", 
data.Options.ProjectName, devops.DEPLOYMENT,
+               ),
+       )
+       if err != nil {
+               return err
+       }
+       defer cursor.Close()
+
+       enricher, err := api.NewDataConverter(api.DataConverterArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: DoraApiParams{
+                               ProjectName: data.Options.ProjectName,
+                       },
+                       Table: "cicd_pipeline_commits",
+               },
+               InputRowType: reflect.TypeOf(pipelineCommitEx{}),
+               Input:        cursor,
+               Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
+                       pipelineCommit := inputRow.(*pipelineCommitEx)
+
+                       domainDeployCommit := &devops.CicdDeploymentCommit{
+                               DomainEntity: domainlayer.DomainEntity{
+                                       Id: fmt.Sprintf("%s:%s", 
pipelineCommit.PipelineId, pipelineCommit.RepoUrl),
+                               },
+                               CicdScopeId:    pipelineCommit.CicdScopeId,
+                               CicdPipelineId: pipelineCommit.PipelineId,
+                               Name:           pipelineCommit.PipelineName,
+                               Result:         pipelineCommit.Result,
+                               Status:         pipelineCommit.Status,
+                               Environment:    pipelineCommit.Environment,
+                               CreatedDate:    *pipelineCommit.CreatedDate,
+                               DurationSec:    pipelineCommit.DurationSec,
+                               CommitSha:      pipelineCommit.CommitSha,
+                               RefName:        pipelineCommit.Branch,
+                               RepoId:         pipelineCommit.RepoId,
+                               RepoUrl:        pipelineCommit.RepoUrl,
+                       }
+                       if pipelineCommit.FinishedDate != nil && 
pipelineCommit.DurationSec != nil {
+                               s := 
pipelineCommit.FinishedDate.Add(-time.Duration(*pipelineCommit.DurationSec) * 
time.Second)
+                               domainDeployCommit.StartedDate = &s
+                       }
+                       // it is tricky when Environment was declared on the 
cicd_tasks level
+                       // lets talk common sense and assume that one pipeline 
can only be deployed to one environment
+                       // so if the pipeline has both staging and production 
tasks, we will treat it as a production pipeline
+                       // and if it has staging tasks without production 
tasks, we will treat it as a staging pipeline
+                       // and then a testing pipeline
+                       // lastly, we will leave Environment empty if any of 
the above measures didn't work out
+
+                       // However, there is another catch, what if one 
deployed multiple TESTING(STAGING or PRODUCTION)
+                       // environments? e.g. testing1, testing2, etc., Does it 
matter?
+                       if pipelineCommit.Environment == "" {
+                               if pipelineCommit.HasProductionTasks {
+                                       domainDeployCommit.Environment = 
devops.PRODUCTION
+                               } else if pipelineCommit.HasStagingTasks {
+                                       domainDeployCommit.Environment = 
devops.STAGING
+                               } else if pipelineCommit.HasTestingTasks {
+                                       domainDeployCommit.Environment = 
devops.TESTING
+                               }
+                       }
+                       return []interface{}{domainDeployCommit}, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return enricher.Execute()
+}

Reply via email to