blob: 33e80f5daa00a4dee7663ad964b9c29a6eb03f81 [file] [log] [blame]
/*
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 (
goerror "errors"
"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
"reflect"
"time"
"github.com/apache/incubator-devlake/errors"
"github.com/apache/incubator-devlake/models/domainlayer/code"
"github.com/apache/incubator-devlake/plugins/core"
"github.com/apache/incubator-devlake/plugins/core/dal"
"github.com/apache/incubator-devlake/plugins/helper"
"gorm.io/gorm"
)
func CalculateChangeLeadTime(taskCtx core.SubTaskContext) errors.Error {
db := taskCtx.GetDal()
log := taskCtx.GetLogger()
data := taskCtx.GetData().(*DoraTaskData)
// construct a list of tuple[task, oldPipelineCommitSha, newPipelineCommitSha, taskFinishedDate]
pipelineIdClauses := []dal.Clause{
dal.Select(`ct.id as task_id, cpc.commit_sha as new_deploy_commit_sha,
ct.finished_date as task_finished_date, cpc.repo_id as repo_id`),
dal.From(`cicd_tasks ct`),
dal.Join(`left join cicd_pipeline_commits cpc on ct.pipeline_id = cpc.pipeline_id`),
dal.Join(`left join project_mapping pm on pm.row_id = ct.cicd_scope_id`),
dal.Where(`ct.environment = ? and ct.type = ? and ct.result = ? and pm.project_name = ? and pm.table = ?`,
"PRODUCTION", "DEPLOYMENT", "SUCCESS", data.Options.ProjectName, "cicd_scopes"),
dal.Orderby(`cpc.repo_id, ct.started_date `),
}
deploymentPairList := make([]deploymentPair, 0)
err := db.All(&deploymentPairList, pipelineIdClauses...)
if err != nil {
return err
}
// deploymentPairList[i-1].NewDeployCommitSha is deploymentPairList[i].OldDeployCommitSha
oldDeployCommitSha := ""
lastRepoId := ""
for i := 0; i < len(deploymentPairList); i++ {
// if two deployments belong to different repo, let's skip
if lastRepoId == deploymentPairList[i].RepoId {
deploymentPairList[i].OldDeployCommitSha = oldDeployCommitSha
} else {
lastRepoId = deploymentPairList[i].RepoId
}
oldDeployCommitSha = deploymentPairList[i].NewDeployCommitSha
}
// get prs by repo project_name
clauses := []dal.Clause{
dal.From(&code.PullRequest{}),
dal.Join(`left join project_mapping pm on pm.row_id = pull_requests.base_repo_id`),
dal.Where("pull_requests.merged_date IS NOT NULL and pm.project_name = ? and pm.table = ?", data.Options.ProjectName, "repos"),
}
cursor, err := db.Cursor(clauses...)
if err != nil {
return err
}
defer cursor.Close()
converter, err := helper.NewDataConverter(helper.DataConverterArgs{
RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
Ctx: taskCtx,
Params: DoraApiParams{
ProjectName: data.Options.ProjectName,
},
Table: "pull_requests",
},
BatchSize: 100,
InputRowType: reflect.TypeOf(code.PullRequest{}),
Input: cursor,
Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
pr := inputRow.(*code.PullRequest)
firstCommit, err := getFirstCommit(pr.Id, db)
if err != nil {
return nil, err
}
projectPrMetric := &crossdomain.ProjectPrMetric{}
projectPrMetric.Id = pr.Id
projectPrMetric.ProjectName = data.Options.ProjectName
if err != nil {
return nil, err
}
if firstCommit != nil {
codingTime := int64(pr.CreatedDate.Sub(firstCommit.AuthoredDate).Seconds())
if codingTime/60 == 0 && codingTime%60 > 0 {
codingTime = 1
} else {
codingTime = codingTime / 60
}
projectPrMetric.CodingTimespan = processNegativeValue(codingTime)
projectPrMetric.FirstCommitSha = firstCommit.Sha
}
firstReview, err := getFirstReview(pr.Id, pr.AuthorId, db)
if err != nil {
return nil, err
}
if firstReview != nil {
projectPrMetric.ReviewLag = processNegativeValue(int64(firstReview.CreatedDate.Sub(pr.CreatedDate).Minutes()))
projectPrMetric.ReviewTimespan = processNegativeValue(int64(pr.MergedDate.Sub(firstReview.CreatedDate).Minutes()))
projectPrMetric.FirstReviewId = firstReview.ReviewId
}
deployment, err := getDeployment(pr.MergeCommitSha, pr.BaseRepoId, deploymentPairList, db)
if err != nil {
return nil, err
}
if deployment != nil && deployment.TaskFinishedDate != nil {
timespan := deployment.TaskFinishedDate.Sub(*pr.MergedDate)
projectPrMetric.DeployTimespan = processNegativeValue(int64(timespan.Minutes()))
projectPrMetric.DeploymentId = deployment.TaskId
} else {
log.Debug("deploy time of pr %v is nil\n", pr.PullRequestKey)
}
projectPrMetric.ChangeTimespan = nil
var result int64
if projectPrMetric.CodingTimespan != nil {
result += *projectPrMetric.CodingTimespan
}
if projectPrMetric.ReviewLag != nil {
result += *projectPrMetric.ReviewLag
}
if projectPrMetric.ReviewTimespan != nil {
result += *projectPrMetric.ReviewTimespan
}
if projectPrMetric.DeployTimespan != nil {
result += *projectPrMetric.DeployTimespan
}
if result > 0 {
projectPrMetric.ChangeTimespan = &result
}
return []interface{}{projectPrMetric}, nil
},
})
if err != nil {
return err
}
return converter.Execute()
}
func getFirstCommit(prId string, db dal.Dal) (*code.Commit, errors.Error) {
commit := &code.Commit{}
commitClauses := []dal.Clause{
dal.From(&code.Commit{}),
dal.Join("left join pull_request_commits on commits.sha = pull_request_commits.commit_sha"),
dal.Where("pull_request_commits.pull_request_id = ?", prId),
dal.Orderby("commits.authored_date ASC"),
}
err := db.First(commit, commitClauses...)
if goerror.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return commit, nil
}
func getFirstReview(prId string, prCreator string, db dal.Dal) (*code.PullRequestComment, errors.Error) {
review := &code.PullRequestComment{}
commentClauses := []dal.Clause{
dal.From(&code.PullRequestComment{}),
dal.Where("pull_request_id = ? and account_id != ?", prId, prCreator),
dal.Orderby("created_date ASC"),
}
err := db.First(review, commentClauses...)
if goerror.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return review, nil
}
func getDeployment(mergeSha string, repoId string, deploymentPairList []deploymentPair, db dal.Dal) (*deploymentPair, errors.Error) {
// ignore environment at this point because detecting it by name is obviously not engouh
// take https://github.com/apache/incubator-devlake/actions/workflows/build.yml for example
// one can not distingush testing/production by looking at the job name solely.
commitDiff := &code.CommitsDiff{}
// find if tuple[merge_sha, new_commit_sha, old_commit_sha] exist in commits_diffs, if yes, return pair.FinishedDate
for _, pair := range deploymentPairList {
if repoId != pair.RepoId {
continue
}
err := db.First(commitDiff, dal.Where(`commit_sha = ? and new_commit_sha = ? and old_commit_sha = ?`,
mergeSha, pair.NewDeployCommitSha, pair.OldDeployCommitSha))
if err == nil {
return &pair, nil
}
if goerror.Is(err, gorm.ErrRecordNotFound) {
continue
}
if err != nil {
return nil, err
}
}
return nil, nil
}
func processNegativeValue(v int64) *int64 {
if v > 0 {
return &v
} else {
return nil
}
}
var CalculateChangeLeadTimeMeta = core.SubTaskMeta{
Name: "calculateChangeLeadTime",
EntryPoint: CalculateChangeLeadTime,
EnabledByDefault: true,
Description: "Calculate change lead time",
DomainTypes: []string{core.DOMAIN_TYPE_CICD, core.DOMAIN_TYPE_CODE},
}
type deploymentPair struct {
TaskId string
RepoId string
NewDeployCommitSha string
OldDeployCommitSha string
TaskFinishedDate *time.Time
}