This is an automated email from the ASF dual-hosted git repository.
klesh 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 5be63862 feat: finish pipeline and issue webhook (#3052)
5be63862 is described below
commit 5be63862bf2fd15bb6af2ed6e77a2a47793a54bf
Author: likyh <[email protected]>
AuthorDate: Mon Sep 19 14:49:11 2022 +0800
feat: finish pipeline and issue webhook (#3052)
* feat: finish pipeline and issue webhook
* feat: add webhooks cicd_task and pipeline_finish
* test: add unittest for type and result on webhook pipeline
* feat: use new type; add environment for task
* fix: try to fix for lint
* fix: change a name
Co-authored-by: linyh <[email protected]>
---
plugins/gitlab/tasks/pipeline_project_convertor.go | 4 +-
plugins/webhook/api/cicd_pipeline.go | 182 +++++++++++++++++++--
plugins/webhook/api/cicd_pipeline_test.go | 67 ++++++++
plugins/webhook/api/issue.go | 97 ++++++++++-
plugins/webhook/impl/impl.go | 7 +-
5 files changed, 338 insertions(+), 19 deletions(-)
diff --git a/plugins/gitlab/tasks/pipeline_project_convertor.go
b/plugins/gitlab/tasks/pipeline_project_convertor.go
index eada51e5..11da9bcc 100644
--- a/plugins/gitlab/tasks/pipeline_project_convertor.go
+++ b/plugins/gitlab/tasks/pipeline_project_convertor.go
@@ -62,7 +62,7 @@ func ConvertPipelineProjects(taskCtx core.SubTaskContext)
errors.Error {
Convert: func(inputRow interface{}) ([]interface{},
errors.Error) {
gitlabPipelineProject :=
inputRow.(*gitlabModels.GitlabPipelineProject)
- domainPipelineRepo := &devops.CiCDPipelineCommit{
+ domainPipelineCommit := &devops.CiCDPipelineCommit{
PipelineId:
pipelineIdGen.Generate(data.Options.ConnectionId,
gitlabPipelineProject.PipelineId),
CommitSha: gitlabPipelineProject.Sha,
Branch: gitlabPipelineProject.Ref,
@@ -71,7 +71,7 @@ func ConvertPipelineProjects(taskCtx core.SubTaskContext)
errors.Error {
}
return []interface{}{
- domainPipelineRepo,
+ domainPipelineCommit,
}, nil
},
})
diff --git a/plugins/webhook/api/cicd_pipeline.go
b/plugins/webhook/api/cicd_pipeline.go
index c15cf63c..064ca983 100644
--- a/plugins/webhook/api/cicd_pipeline.go
+++ b/plugins/webhook/api/cicd_pipeline.go
@@ -18,41 +18,199 @@ limitations under the License.
package api
import (
+ "fmt"
"github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/devops"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
"github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
"github.com/apache/incubator-devlake/plugins/webhook/models"
+ "github.com/go-playground/validator/v10"
"net/http"
"time"
)
-type WebhookPipelineRequest struct {
- Id string `validate:"required"`
- Result string `validate:"oneof=SUCCESS FAILURE ABORT"`
+type WebhookTaskRequest struct {
+ // PipelineName can be filled by any string unique in one pipeline
+ PipelineName string `mapstructure:"pipeline_name" validate:"required"`
+
+ Name string `validate:"required"` // Name should be unique
in one pipeline
+ Result string `validate:"oneof=SUCCESS FAILURE ABORT
IN_PROGRESS"`
Status string `validate:"oneof=IN_PROGRESS DONE"`
- Type string `validate:"oneof=CI CD CI/CD"`
- CreatedDate time.Time `mapstructure:"created_date"
validate:"required"`
+ Type string `validate:"oneof=TEST LINT BUILD DEPLOYMENT"`
+ Environment string `validate:"oneof=PRODUCTION STAGING TESTING"`
+ StartedDate time.Time `mapstructure:"created_date"
validate:"required"`
FinishedDate *time.Time `mapstructure:"finished_date"`
- Repo string `validate:"required"`
+ RepoId string `mapstructure:"repo_id" validate:"required"` // RepoId
should be unique string
Branch string
CommitSha string `mapstructure:"commit_sha"`
}
-// PostCicdPipeline
+// PostCicdTask
// @Summary create pipeline by webhook
-// @Description Create pipeline by webhook, example:
{"id":"A123123","result":"one of SUCCESS/FAILURE/ABORT","status":"one of
IN_PROGRESS/DONE","type":"CI/CD","created_date":"2020-01-01T12:00:00+00:00","finished_date":"2020-01-01T12:59:59+00:00","repo":"devlake","branch":"main","commit_sha":"015e3d3b480e417aede5a1293bd61de9b0fd051d"}
+// @Description Create pipeline by webhook.<br/>
+// @Description example1:
{"pipeline_name":"A123","name":"unit-test","result":"IN_PROGRESS","status":"IN_PROGRESS","type":"TEST","environment":"PRODUCTION","created_date":"2020-01-01T12:00:00+00:00","finished_date":"2020-01-01T12:59:59+00:00","repo_id":"devlake","branch":"main","commit_sha":"015e3d3b480e417aede5a1293bd61de9b0fd051d"}<br/>
+// @Description example2:
{"pipeline_name":"A123","name":"unit-test","result":"SUCCESS","status":"DONE","type":"DEPLOYMENT","environment":"PRODUCTION","created_date":"2020-01-01T12:00:00+00:00","finished_date":"2020-01-01T12:59:59+00:00","repo_id":"devlake","branch":"main","commit_sha":"015e3d3b480e417aede5a1293bd61de9b0fd051d"}<br/>
+// @Description When request webhook first time for each pipeline, it will be
created.
+// @Description So we suggest request before task start and after pipeline
finish.
+// @Description Remember fill all data to request after pipeline finish.
// @Tags plugins/webhook
-// @Param body body WebhookPipelineRequest true "json body"
+// @Param body body WebhookTaskRequest true "json body"
// @Success 200
// @Failure 400 {string} errcode.Error "Bad Request"
+// @Failure 403 {string} errcode.Error "Forbidden"
// @Failure 500 {string} errcode.Error "Internal Error"
-// @Router /plugins/webhook/:connectionId/cicd_pipelines [POST]
-func PostCicdPipeline(input *core.ApiResourceInput) (*core.ApiResourceOutput,
errors.Error) {
+// @Router /plugins/webhook/:connectionId/cicd_tasks [POST]
+func PostCicdTask(input *core.ApiResourceInput) (*core.ApiResourceOutput,
errors.Error) {
connection := &models.WebhookConnection{}
err := connectionHelper.First(connection, input.Params)
if err != nil {
return nil, err
}
- // TODO save pipeline
+ // get request
+ request := &WebhookTaskRequest{}
+ err = helper.DecodeMapStruct(input.Body, request)
+ if err != nil {
+ return &core.ApiResourceOutput{Body: err.Error(), Status:
http.StatusBadRequest}, nil
+ }
+ // validate
+ vld = validator.New()
+ err = errors.Convert(vld.Struct(request))
+ if err != nil {
+ return nil, errors.BadInput.Wrap(vld.Struct(request), `input
json error`)
+ }
+
+ db := basicRes.GetDal()
+ pipelineId := fmt.Sprintf("%s:%d:%s", "webhook", connection.ID,
request.PipelineName)
+ domainCicdTask := &devops.CICDTask{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: fmt.Sprintf("%s:%d:%s:%s", "webhook",
connection.ID, request.PipelineName, request.Name),
+ },
+ PipelineId: pipelineId,
+ Name: request.Name,
+ Result: request.Result,
+ Status: request.Status,
+ Type: request.Type,
+ Environment: request.Environment,
+ StartedDate: request.StartedDate,
+ FinishedDate: request.FinishedDate,
+ }
+ if domainCicdTask.FinishedDate != nil {
+ domainCicdTask.DurationSec =
uint64(domainCicdTask.FinishedDate.Sub(domainCicdTask.StartedDate).Seconds())
+ }
+
+ domainPipeline := &devops.CICDPipeline{}
+ err = db.First(domainPipeline, dal.Where("id = ?", pipelineId))
+ if err != nil {
+ domainPipeline = &devops.CICDPipeline{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: pipelineId,
+ },
+ Name: request.PipelineName,
+ Result: ``,
+ Status: `IN_PROGRESS`,
+ Type: ``,
+ CreatedDate: request.StartedDate,
+ FinishedDate: nil,
+ }
+ } else if domainPipeline.Status == `DONE` {
+ return nil, errors.Forbidden.New(`can not receive this task
because pipeline has already been done.`)
+ }
+
+ domainPipelineCommit := &devops.CiCDPipelineCommit{
+ PipelineId: pipelineId,
+ CommitSha: request.CommitSha,
+ Branch: request.Branch,
+ RepoId: request.RepoId,
+ }
+
+ // save
+ err = db.CreateOrUpdate(domainCicdTask)
+ if err != nil {
+ return nil, err
+ }
+ err = db.CreateOrUpdate(domainPipeline)
+ if err != nil {
+ return nil, err
+ }
+ err = db.CreateOrUpdate(domainPipelineCommit)
+ if err != nil {
+ return nil, err
+ }
+
return &core.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
}
+
+// PostPipelineFinish
+// @Summary set pipeline's status to DONE
+// @Description set pipeline's status to DONE and cal duration
+// @Tags plugins/webhook
+// @Success 200
+// @Failure 400 {string} errcode.Error "Bad Request"
+// @Failure 500 {string} errcode.Error "Internal Error"
+// @Router /plugins/webhook/:connectionId/cicd_pipeline/:pipelineName/finish
[POST]
+func PostPipelineFinish(input *core.ApiResourceInput)
(*core.ApiResourceOutput, errors.Error) {
+ connection := &models.WebhookConnection{}
+ err := connectionHelper.First(connection, input.Params)
+ if err != nil {
+ return nil, err
+ }
+
+ db := basicRes.GetDal()
+ pipelineId := fmt.Sprintf("%s:%d:%s", "webhook", connection.ID,
input.Params[`pipelineName`])
+ println(pipelineId)
+ domainPipeline := &devops.CICDPipeline{}
+ err = db.First(domainPipeline, dal.Where("id = ?", pipelineId))
+ if err != nil {
+ return nil, errors.NotFound.Wrap(err, `pipeline not found`)
+ }
+
+ domainTasks := []devops.CICDTask{}
+ err = db.All(&domainTasks, dal.Where("pipeline_id = ?", pipelineId))
+ if err != nil {
+ return nil, errors.NotFound.Wrap(err, `tasks not found`)
+ }
+ pipelineType, result := getTypeAndResultFromTasks(domainTasks)
+ domainPipeline.Type = pipelineType
+ domainPipeline.Result = result
+ domainPipeline.Status = ticket.DONE
+ now := time.Now()
+ domainPipeline.FinishedDate = &now
+ domainPipeline.DurationSec =
uint64(domainPipeline.FinishedDate.Sub(domainPipeline.CreatedDate).Seconds())
+
+ // save
+ err = db.Update(domainPipeline)
+ if err != nil {
+ return nil, err
+ }
+
+ return &core.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
+}
+
+// getTypeAndResultFromTasks will extract pipeline type and result from tasks
+// type = tasks' type if all tasks have the same type, or empty string
+// result = ABORT if any tasks' type is ABORT,
+// or result = FAILURE if any tasks' type is ABORT and others are SUCCESS
+// or result = SUCCESS if all tasks' type is SUCCESS
+func getTypeAndResultFromTasks(domainTasks []devops.CICDTask) (pipelineType,
result string) {
+ result = `SUCCESS`
+ if len(domainTasks) > 0 {
+ pipelineType = domainTasks[0].Type
+ }
+ for _, domainTask := range domainTasks {
+ if domainTask.Type != pipelineType {
+ pipelineType = ``
+ }
+ if domainTask.Result == `ABORT` {
+ result = `ABORT`
+ } else if domainTask.Result == `FAILURE` {
+ if result == `SUCCESS` {
+ result = `FAILURE`
+ }
+ }
+ }
+ return
+}
diff --git a/plugins/webhook/api/cicd_pipeline_test.go
b/plugins/webhook/api/cicd_pipeline_test.go
new file mode 100644
index 00000000..f1862c33
--- /dev/null
+++ b/plugins/webhook/api/cicd_pipeline_test.go
@@ -0,0 +1,67 @@
+/*
+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 api
+
+import (
+ "github.com/apache/incubator-devlake/models/domainlayer/devops"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestGetTypeAndResultFromTasks(t *testing.T) {
+ testDomainTask := devops.CICDTask{Type: devops.TEST}
+ buildDomainTask := devops.CICDTask{Type: devops.BUILD}
+ deploymentDomainTask := devops.CICDTask{Type: devops.DEPLOYMENT}
+
+ abortDomainTask := devops.CICDTask{Result: `ABORT`}
+ failureDomainTask := devops.CICDTask{Result: `FAILURE`}
+ successDomainTask := devops.CICDTask{Result: `SUCCESS`}
+
+ pipelineType, _ :=
getTypeAndResultFromTasks([]devops.CICDTask{testDomainTask})
+ assert.Equal(t, devops.TEST, pipelineType)
+
+ pipelineType, _ =
getTypeAndResultFromTasks([]devops.CICDTask{buildDomainTask})
+ assert.Equal(t, devops.BUILD, pipelineType)
+
+ pipelineType, _ =
getTypeAndResultFromTasks([]devops.CICDTask{deploymentDomainTask,
deploymentDomainTask, deploymentDomainTask})
+ assert.Equal(t, devops.DEPLOYMENT, pipelineType)
+
+ pipelineType, _ =
getTypeAndResultFromTasks([]devops.CICDTask{buildDomainTask,
deploymentDomainTask, buildDomainTask})
+ assert.Equal(t, ``, pipelineType)
+
+ _, result :=
getTypeAndResultFromTasks([]devops.CICDTask{abortDomainTask, failureDomainTask,
successDomainTask})
+ assert.Equal(t, `ABORT`, result)
+
+ _, result =
getTypeAndResultFromTasks([]devops.CICDTask{failureDomainTask,
successDomainTask})
+ assert.Equal(t, `FAILURE`, result)
+
+ _, result =
getTypeAndResultFromTasks([]devops.CICDTask{successDomainTask})
+ assert.Equal(t, `SUCCESS`, result)
+
+ _, result =
getTypeAndResultFromTasks([]devops.CICDTask{successDomainTask,
successDomainTask})
+ assert.Equal(t, `SUCCESS`, result)
+
+ _, result =
getTypeAndResultFromTasks([]devops.CICDTask{successDomainTask,
successDomainTask, failureDomainTask})
+ assert.Equal(t, `FAILURE`, result)
+
+ _, result =
getTypeAndResultFromTasks([]devops.CICDTask{successDomainTask,
successDomainTask, abortDomainTask})
+ assert.Equal(t, `ABORT`, result)
+
+ _, result =
getTypeAndResultFromTasks([]devops.CICDTask{failureDomainTask,
failureDomainTask, abortDomainTask})
+ assert.Equal(t, `ABORT`, result)
+}
diff --git a/plugins/webhook/api/issue.go b/plugins/webhook/api/issue.go
index 93cfea37..06a3fc4b 100644
--- a/plugins/webhook/api/issue.go
+++ b/plugins/webhook/api/issue.go
@@ -18,9 +18,15 @@ limitations under the License.
package api
import (
+ "fmt"
"github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
"github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
"github.com/apache/incubator-devlake/plugins/webhook/models"
+ "github.com/go-playground/validator/v10"
"net/http"
"time"
)
@@ -59,7 +65,7 @@ type WebhookIssueRequest struct {
// @Summary receive a record as defined and save it
// @Description receive a record as follow and save it, example:
{"board_key":"DLK","url":"","issue_key":"DLK-1234","title":"a feature from
DLK","description":"","epic_key":"","type":"BUG","status":"TODO","original_status":"created","story_point":0,"resolution_date":null,"created_date":"2020-01-01T12:00:00+00:00","updated_date":null,"lead_time_minutes":0,"parent_issue_key":"DLK-1200","priority":"","original_estimate_minutes":0,"time_spent_minutes":0,"time_remaining_minutes":0,"creator_id
[...]
// @Tags plugins/webhook
-// @Param body body WebhookPipelineRequest true "json body"
+// @Param body body WebhookIssueRequest true "json body"
// @Success 200 {string} noResponse ""
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"
@@ -70,7 +76,78 @@ func PostIssue(input *core.ApiResourceInput)
(*core.ApiResourceOutput, errors.Er
if err != nil {
return nil, err
}
- // TODO save issue
+ // get request
+ request := &WebhookIssueRequest{}
+ err = helper.DecodeMapStruct(input.Body, request)
+ if err != nil {
+ return &core.ApiResourceOutput{Body: err.Error(), Status:
http.StatusBadRequest}, nil
+ }
+ // validate
+ vld = validator.New()
+ err = errors.Convert(vld.Struct(request))
+ if err != nil {
+ return &core.ApiResourceOutput{Body: err.Error(), Status:
http.StatusBadRequest}, nil
+ }
+
+ db := basicRes.GetDal()
+ domainIssue := &ticket.Issue{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: fmt.Sprintf("%s:%d:%s:%s", "webhook",
connection.ID, request.BoardKey, request.IssueKey),
+ },
+ Url: request.Url,
+ IssueKey: request.IssueKey,
+ Title: request.Title,
+ Description: request.Description,
+ EpicKey: request.EpicKey,
+ Type: request.Type,
+ Status: request.Status,
+ OriginalStatus: request.OriginalStatus,
+ StoryPoint: request.StoryPoint,
+ ResolutionDate: request.ResolutionDate,
+ CreatedDate: request.CreatedDate,
+ UpdatedDate: request.UpdatedDate,
+ LeadTimeMinutes: request.LeadTimeMinutes,
+ Priority: request.Priority,
+ OriginalEstimateMinutes: request.OriginalEstimateMinutes,
+ TimeSpentMinutes: request.TimeSpentMinutes,
+ TimeRemainingMinutes: request.TimeRemainingMinutes,
+ CreatorName: request.CreatorName,
+ AssigneeName: request.AssigneeName,
+ Severity: request.Severity,
+ Component: request.Component,
+ }
+ if request.CreatorId != "" {
+ domainIssue.CreatorId = fmt.Sprintf("%s:%d:%s", "webhook",
connection.ID, request.CreatorId)
+ }
+ if request.AssigneeId != "" {
+ domainIssue.AssigneeId = fmt.Sprintf("%s:%d:%s", "webhook",
connection.ID, request.AssigneeId)
+ }
+ if request.ParentIssueKey != "" {
+ domainIssue.ParentIssueId = fmt.Sprintf("%s:%d:%s:%s",
"webhook", connection.ID, request.BoardKey, request.ParentIssueKey)
+ }
+ domainBoard := &ticket.Board{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: fmt.Sprintf("%s:%d:%s", "webhook", connection.ID,
request.BoardKey),
+ },
+ }
+ boardIssue := &ticket.BoardIssue{
+ BoardId: domainBoard.Id,
+ IssueId: domainIssue.Id,
+ }
+ // save
+ err = db.CreateOrUpdate(domainIssue)
+ if err != nil {
+ return nil, err
+ }
+ err = db.CreateOrUpdate(domainBoard)
+ if err != nil {
+ return nil, err
+ }
+ err = db.CreateOrUpdate(boardIssue)
+ if err != nil {
+ return nil, err
+ }
+
return &core.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
}
@@ -88,6 +165,20 @@ func CloseIssue(input *core.ApiResourceInput)
(*core.ApiResourceOutput, errors.E
if err != nil {
return nil, err
}
- // TODO close issue
+
+ db := basicRes.GetDal()
+ domainIssue := &ticket.Issue{}
+ err = db.First(domainIssue, dal.Where("id = ?",
fmt.Sprintf("%s:%d:%s:%s", "webhook", connection.ID, input.Params[`boardKey`],
input.Params[`issueId`])))
+ if err != nil {
+ return nil, errors.NotFound.Wrap(err, `issue not found`)
+ }
+ domainIssue.Status = ticket.DONE
+ domainIssue.OriginalStatus = ``
+
+ // save
+ err = db.Update(domainIssue)
+ if err != nil {
+ return nil, err
+ }
return &core.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
}
diff --git a/plugins/webhook/impl/impl.go b/plugins/webhook/impl/impl.go
index 670423e3..b8001434 100644
--- a/plugins/webhook/impl/impl.go
+++ b/plugins/webhook/impl/impl.go
@@ -63,8 +63,11 @@ func (plugin Webhook) ApiResources()
map[string]map[string]core.ApiResourceHandl
"PATCH": api.PatchConnection,
"DELETE": api.DeleteConnection,
},
- ":connectionId/cicd_pipelines": {
- "POST": api.PostCicdPipeline,
+ ":connectionId/cicd_tasks": {
+ "POST": api.PostCicdTask,
+ },
+ ":connectionId/cicd_pipeline/:pipelineName/finish": {
+ "POST": api.PostPipelineFinish,
},
":connectionId/issues": {
"POST": api.PostIssue,