You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2025/12/31 12:02:56 UTC
(incubator-devlake) branch fix-8670 created (now a0d038898)
This is an automated email from the ASF dual-hosted git repository.
warren pushed a change to branch fix-8670
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
at a0d038898 feat(azuredevops): add support for Release Pipelines (Classic Pipelines) (#8670)
This branch includes the following new commits:
new a0d038898 feat(azuredevops): add support for Release Pipelines (Classic Pipelines) (#8670)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(incubator-devlake) 01/01: feat(azuredevops): add support for Release Pipelines (Classic Pipelines) (#8670)
Posted by wa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
warren pushed a commit to branch fix-8670
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
commit a0d0388986269222fbb1966cc3a1500b58cd57f1
Author: warren <wa...@gmail.com>
AuthorDate: Wed Dec 31 19:56:35 2025 +0800
feat(azuredevops): add support for Release Pipelines (Classic Pipelines) (#8670)
Add support for Azure DevOps Release Pipelines (Classic/XAML pipelines)
to collect deployment data from the Release Management API.
Changes:
- Add Release and ReleaseDeployment data models
- Add collectors for releases and deployments using vsrm.dev.azure.com API
- Add extractors to parse raw API responses
- Add converter to transform deployments into CICDPipeline domain model
- Add database migration for new tables
The deployments are converted to DevLake's standard CICDPipeline format
with type set to DEPLOYMENT, enabling DORA metrics calculation for
classic release pipelines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <no...@anthropic.com>
---
backend/plugins/azuredevops_go/impl/impl.go | 2 +
...{register.go => 20241231_add_release_tables.go} | 29 +++-
.../models/migrationscripts/archived/release.go | 69 +++++++++
.../models/migrationscripts/register.go | 1 +
backend/plugins/azuredevops_go/models/release.go | 151 ++++++++++++++++++++
.../azuredevops_go/tasks/release_collector.go | 93 ++++++++++++
.../tasks/release_deployment_collector.go | 93 ++++++++++++
.../tasks/release_deployment_converter.go | 157 +++++++++++++++++++++
.../tasks/release_deployment_extractor.go | 91 ++++++++++++
.../azuredevops_go/tasks/release_extractor.go | 83 +++++++++++
10 files changed, 762 insertions(+), 7 deletions(-)
diff --git a/backend/plugins/azuredevops_go/impl/impl.go b/backend/plugins/azuredevops_go/impl/impl.go
index 44cc01e52..b5dc254fb 100644
--- a/backend/plugins/azuredevops_go/impl/impl.go
+++ b/backend/plugins/azuredevops_go/impl/impl.go
@@ -88,6 +88,8 @@ func (p Azuredevops) GetTablesInfo() []dal.Tabler {
&models.AzuredevopsPrLabel{},
&models.AzuredevopsProject{},
&models.AzuredevopsPullRequest{},
+ &models.AzuredevopsRelease{},
+ &models.AzuredevopsReleaseDeployment{},
&models.AzuredevopsRepo{},
&models.AzuredevopsRepoCommit{},
&models.AzuredevopsScopeConfig{},
diff --git a/backend/plugins/azuredevops_go/models/migrationscripts/register.go b/backend/plugins/azuredevops_go/models/migrationscripts/20241231_add_release_tables.go
similarity index 54%
copy from backend/plugins/azuredevops_go/models/migrationscripts/register.go
copy to backend/plugins/azuredevops_go/models/migrationscripts/20241231_add_release_tables.go
index 59fa7832f..d4934434a 100644
--- a/backend/plugins/azuredevops_go/models/migrationscripts/register.go
+++ b/backend/plugins/azuredevops_go/models/migrationscripts/20241231_add_release_tables.go
@@ -18,13 +18,28 @@ limitations under the License.
package migrationscripts
import (
- "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+ "github.com/apache/incubator-devlake/plugins/azuredevops_go/models/migrationscripts/archived"
)
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
- return []plugin.MigrationScript{
- new(addInitTables),
- new(extendRepoTable),
- }
+type addReleaseTables struct {
+}
+
+func (u *addReleaseTables) Up(basicRes context.BasicRes) errors.Error {
+ err := migrationhelper.AutoMigrateTables(
+ basicRes,
+ &archived.AzuredevopsRelease{},
+ &archived.AzuredevopsReleaseDeployment{},
+ )
+ return err
+}
+
+func (*addReleaseTables) Version() uint64 {
+ return 20241231000001
+}
+
+func (*addReleaseTables) Name() string {
+ return "Add Azure DevOps Release Pipeline tables"
}
diff --git a/backend/plugins/azuredevops_go/models/migrationscripts/archived/release.go b/backend/plugins/azuredevops_go/models/migrationscripts/archived/release.go
new file mode 100644
index 000000000..d771a1426
--- /dev/null
+++ b/backend/plugins/azuredevops_go/models/migrationscripts/archived/release.go
@@ -0,0 +1,69 @@
+/*
+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 archived
+
+import (
+ "github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+ "time"
+)
+
+type AzuredevopsRelease struct {
+ archived.NoPKModel
+
+ ConnectionId uint64 `gorm:"primaryKey"`
+ AzuredevopsId int `gorm:"primaryKey"`
+ ProjectId string `gorm:"type:varchar(255)"`
+ Name string `gorm:"type:varchar(255)"`
+ Status string `gorm:"type:varchar(100)"`
+ ReleaseDefinitionId int
+ ReleaseDefinitionName string `gorm:"type:varchar(255)"`
+ Description string `gorm:"type:text"`
+ CreatedOn *time.Time
+ ModifiedOn *time.Time
+}
+
+func (AzuredevopsRelease) TableName() string {
+ return "_tool_azuredevops_go_releases"
+}
+
+type AzuredevopsReleaseDeployment struct {
+ archived.NoPKModel
+
+ ConnectionId uint64 `gorm:"primaryKey"`
+ AzuredevopsId int `gorm:"primaryKey"`
+ ReleaseId int `gorm:"index"`
+ ProjectId string `gorm:"type:varchar(255)"`
+ Name string `gorm:"type:varchar(255)"`
+ Status string `gorm:"type:varchar(100)"`
+ OperationStatus string `gorm:"type:varchar(100)"`
+ DeploymentStatus string `gorm:"type:varchar(100)"`
+ DefinitionName string `gorm:"type:varchar(255)"`
+ DefinitionId int
+ EnvironmentId int
+ EnvironmentName string `gorm:"type:varchar(255)"`
+ AttemptNumber int
+ Reason string `gorm:"type:varchar(100)"`
+ QueuedOn *time.Time
+ StartedOn *time.Time
+ CompletedOn *time.Time
+ LastModifiedOn *time.Time
+}
+
+func (AzuredevopsReleaseDeployment) TableName() string {
+ return "_tool_azuredevops_go_release_deployments"
+}
diff --git a/backend/plugins/azuredevops_go/models/migrationscripts/register.go b/backend/plugins/azuredevops_go/models/migrationscripts/register.go
index 59fa7832f..c9a409c4c 100644
--- a/backend/plugins/azuredevops_go/models/migrationscripts/register.go
+++ b/backend/plugins/azuredevops_go/models/migrationscripts/register.go
@@ -26,5 +26,6 @@ func All() []plugin.MigrationScript {
return []plugin.MigrationScript{
new(addInitTables),
new(extendRepoTable),
+ new(addReleaseTables),
}
}
diff --git a/backend/plugins/azuredevops_go/models/release.go b/backend/plugins/azuredevops_go/models/release.go
new file mode 100644
index 000000000..cace21e94
--- /dev/null
+++ b/backend/plugins/azuredevops_go/models/release.go
@@ -0,0 +1,151 @@
+/*
+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 models
+
+import (
+ "github.com/apache/incubator-devlake/core/models/common"
+ "time"
+)
+
+// AzuredevopsRelease represents a release from Azure DevOps Release Pipelines (Classic)
+type AzuredevopsRelease struct {
+ common.NoPKModel
+
+ ConnectionId uint64 `gorm:"primaryKey"`
+ AzuredevopsId int `json:"id" gorm:"primaryKey"`
+ ProjectId string
+ Name string
+ Status string
+ ReleaseDefinitionId int
+ ReleaseDefinitionName string
+ Description string
+ CreatedOn *time.Time
+ ModifiedOn *time.Time
+}
+
+func (AzuredevopsRelease) TableName() string {
+ return "_tool_azuredevops_go_releases"
+}
+
+// AzuredevopsReleaseDeployment represents a deployment (environment) within a release
+type AzuredevopsReleaseDeployment struct {
+ common.NoPKModel
+
+ ConnectionId uint64 `gorm:"primaryKey"`
+ AzuredevopsId int `json:"id" gorm:"primaryKey"`
+ ReleaseId int `gorm:"index"`
+ ProjectId string
+ Name string
+ Status string
+ OperationStatus string
+ DeploymentStatus string
+ DefinitionName string
+ DefinitionId int
+ EnvironmentId int
+ EnvironmentName string
+ AttemptNumber int
+ Reason string
+ QueuedOn *time.Time
+ StartedOn *time.Time
+ CompletedOn *time.Time
+ LastModifiedOn *time.Time
+}
+
+func (AzuredevopsReleaseDeployment) TableName() string {
+ return "_tool_azuredevops_go_release_deployments"
+}
+
+// AzuredevopsApiRelease is the API response structure from Azure DevOps Release API
+type AzuredevopsApiRelease struct {
+ Id int `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ CreatedOn *time.Time `json:"createdOn"`
+ ModifiedOn *time.Time `json:"modifiedOn"`
+ Description string `json:"description"`
+ ReleaseDefinition struct {
+ Id int `json:"id"`
+ Name string `json:"name"`
+ Url string `json:"url"`
+ Path string `json:"path"`
+ } `json:"releaseDefinition"`
+ Environments []AzuredevopsApiReleaseEnvironment `json:"environments"`
+ ProjectReference struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ } `json:"projectReference"`
+}
+
+// AzuredevopsApiReleaseEnvironment represents an environment in the release
+type AzuredevopsApiReleaseEnvironment struct {
+ Id int `json:"id"`
+ ReleaseId int `json:"releaseId"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ DeploySteps []AzuredevopsApiDeployStep `json:"deploySteps"`
+ PreDeployApprovals []struct {
+ Status string `json:"status"`
+ } `json:"preDeployApprovals"`
+ PostDeployApprovals []struct {
+ Status string `json:"status"`
+ } `json:"postDeployApprovals"`
+}
+
+// AzuredevopsApiDeployStep represents a deployment step
+type AzuredevopsApiDeployStep struct {
+ Id int `json:"id"`
+ DeploymentId int `json:"deploymentId"`
+ Attempt int `json:"attempt"`
+ Reason string `json:"reason"`
+ Status string `json:"status"`
+ OperationStatus string `json:"operationStatus"`
+ QueuedOn *time.Time `json:"queuedOn"`
+ LastModifiedOn *time.Time `json:"lastModifiedOn"`
+ HasStarted bool `json:"hasStarted"`
+}
+
+// AzuredevopsApiDeployment is the API response structure for deployments
+type AzuredevopsApiDeployment struct {
+ Id int `json:"id"`
+ Release struct {
+ Id int `json:"id"`
+ Name string `json:"name"`
+ } `json:"release"`
+ ReleaseDefinition struct {
+ Id int `json:"id"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ } `json:"releaseDefinition"`
+ ReleaseEnvironment struct {
+ Id int `json:"id"`
+ Name string `json:"name"`
+ } `json:"releaseEnvironment"`
+ ProjectReference struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ } `json:"projectReference"`
+ DefinitionEnvironmentId int `json:"definitionEnvironmentId"`
+ Attempt int `json:"attempt"`
+ Reason string `json:"reason"`
+ DeploymentStatus string `json:"deploymentStatus"`
+ OperationStatus string `json:"operationStatus"`
+ QueuedOn *time.Time `json:"queuedOn"`
+ StartedOn *time.Time `json:"startedOn"`
+ CompletedOn *time.Time `json:"completedOn"`
+ LastModifiedOn *time.Time `json:"lastModifiedOn"`
+}
\ No newline at end of file
diff --git a/backend/plugins/azuredevops_go/tasks/release_collector.go b/backend/plugins/azuredevops_go/tasks/release_collector.go
new file mode 100644
index 000000000..7744cb85b
--- /dev/null
+++ b/backend/plugins/azuredevops_go/tasks/release_collector.go
@@ -0,0 +1,93 @@
+/*
+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 (
+ "encoding/json"
+ "net/url"
+ "strconv"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+func init() {
+ RegisterSubtaskMeta(&CollectReleasesMeta)
+}
+
+const RawReleaseTable = "azuredevops_go_api_releases"
+
+var CollectReleasesMeta = plugin.SubTaskMeta{
+ Name: "collectApiReleases",
+ EntryPoint: CollectReleases,
+ EnabledByDefault: true,
+ Description: "Collect Release Pipeline data from Azure DevOps Release API (Classic Pipelines)",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CICD},
+ ProductTables: []string{RawReleaseTable},
+}
+
+func CollectReleases(taskCtx plugin.SubTaskContext) errors.Error {
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RawReleaseTable)
+
+ collector, err := api.NewStatefulApiCollectorForFinalizableEntity(api.FinalizableApiCollectorArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ ApiClient: data.ApiClient,
+ CollectNewRecordsByList: api.FinalizableApiCollectorListArgs{
+ GetNextPageCustomData: ExtractContToken,
+ PageSize: 100,
+ FinalizableApiCollectorCommonArgs: api.FinalizableApiCollectorCommonArgs{
+ // Azure DevOps Release API uses a different base URL: vsrm.dev.azure.com
+ UrlTemplate: "https://vsrm.dev.azure.com/{{ .Params.OrganizationId }}/{{ .Params.ProjectId }}/_apis/release/releases?api-version=7.1",
+ Query: func(reqData *api.RequestData, createdAfter *time.Time) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("$top", strconv.Itoa(reqData.Pager.Size))
+ query.Set("$expand", "environments")
+ if reqData.CustomData != nil {
+ pag := reqData.CustomData.(CustomPageDate)
+ query.Set("continuationToken", pag.ContinuationToken)
+ }
+
+ if createdAfter != nil {
+ query.Set("minCreatedTime", createdAfter.Format(time.RFC3339))
+ }
+ return query, nil
+ },
+ ResponseParser: ParseRawMessageFromValue,
+ AfterResponse: change203To401,
+ },
+ GetCreated: func(item json.RawMessage) (time.Time, errors.Error) {
+ var release struct {
+ CreatedOn time.Time `json:"createdOn"`
+ }
+ err := json.Unmarshal(item, &release)
+ if err != nil {
+ return time.Time{}, errors.BadInput.Wrap(err, "failed to unmarshal Azure DevOps Release")
+ }
+ return release.CreatedOn, nil
+ },
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
diff --git a/backend/plugins/azuredevops_go/tasks/release_deployment_collector.go b/backend/plugins/azuredevops_go/tasks/release_deployment_collector.go
new file mode 100644
index 000000000..b69d0db99
--- /dev/null
+++ b/backend/plugins/azuredevops_go/tasks/release_deployment_collector.go
@@ -0,0 +1,93 @@
+/*
+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 (
+ "encoding/json"
+ "net/url"
+ "strconv"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+func init() {
+ RegisterSubtaskMeta(&CollectReleaseDeploymentsMeta)
+}
+
+const RawReleaseDeploymentTable = "azuredevops_go_api_release_deployments"
+
+var CollectReleaseDeploymentsMeta = plugin.SubTaskMeta{
+ Name: "collectApiReleaseDeployments",
+ EntryPoint: CollectReleaseDeployments,
+ EnabledByDefault: true,
+ Description: "Collect Release Deployment data from Azure DevOps Release API (Classic Pipelines)",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CICD},
+ ProductTables: []string{RawReleaseDeploymentTable},
+}
+
+func CollectReleaseDeployments(taskCtx plugin.SubTaskContext) errors.Error {
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RawReleaseDeploymentTable)
+
+ collector, err := api.NewStatefulApiCollectorForFinalizableEntity(api.FinalizableApiCollectorArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ ApiClient: data.ApiClient,
+ CollectNewRecordsByList: api.FinalizableApiCollectorListArgs{
+ GetNextPageCustomData: ExtractContToken,
+ PageSize: 100,
+ FinalizableApiCollectorCommonArgs: api.FinalizableApiCollectorCommonArgs{
+ // Azure DevOps Release API uses a different base URL: vsrm.dev.azure.com
+ UrlTemplate: "https://vsrm.dev.azure.com/{{ .Params.OrganizationId }}/{{ .Params.ProjectId }}/_apis/release/deployments?api-version=7.1",
+ Query: func(reqData *api.RequestData, createdAfter *time.Time) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("$top", strconv.Itoa(reqData.Pager.Size))
+ query.Set("queryOrder", "descending")
+ if reqData.CustomData != nil {
+ pag := reqData.CustomData.(CustomPageDate)
+ query.Set("continuationToken", pag.ContinuationToken)
+ }
+
+ if createdAfter != nil {
+ query.Set("minStartedTime", createdAfter.Format(time.RFC3339))
+ }
+ return query, nil
+ },
+ ResponseParser: ParseRawMessageFromValue,
+ AfterResponse: change203To401,
+ },
+ GetCreated: func(item json.RawMessage) (time.Time, errors.Error) {
+ var deployment struct {
+ QueuedOn time.Time `json:"queuedOn"`
+ }
+ err := json.Unmarshal(item, &deployment)
+ if err != nil {
+ return time.Time{}, errors.BadInput.Wrap(err, "failed to unmarshal Azure DevOps Release Deployment")
+ }
+ return deployment.QueuedOn, nil
+ },
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
diff --git a/backend/plugins/azuredevops_go/tasks/release_deployment_converter.go b/backend/plugins/azuredevops_go/tasks/release_deployment_converter.go
new file mode 100644
index 000000000..805e06489
--- /dev/null
+++ b/backend/plugins/azuredevops_go/tasks/release_deployment_converter.go
@@ -0,0 +1,157 @@
+/*
+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 (
+ "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/devops"
+ "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/azuredevops_go/models"
+)
+
+func init() {
+ RegisterSubtaskMeta(&ConvertReleaseDeploymentsMeta)
+}
+
+var ConvertReleaseDeploymentsMeta = plugin.SubTaskMeta{
+ Name: "convertApiReleaseDeployments",
+ EntryPoint: ConvertReleaseDeployments,
+ EnabledByDefault: true,
+ Description: "Convert tool layer table azuredevops_release_deployments into domain layer table cicd_pipelines",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CICD},
+ DependencyTables: []string{
+ models.AzuredevopsReleaseDeployment{}.TableName(),
+ },
+}
+
+// Release deployment status and operation status mappings
+// Reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/release/deployments/list
+const (
+ releaseDeploymentStatusSucceeded = "succeeded"
+ releaseDeploymentStatusFailed = "failed"
+ releaseDeploymentStatusNotDeployed = "notDeployed"
+ releaseDeploymentStatusPartiallySucceeded = "partiallySucceeded"
+ releaseOperationStatusApproved = "Approved"
+ releaseOperationStatusCanceled = "Canceled"
+ releaseOperationStatusCancelling = "Cancelling"
+ releaseOperationStatusDeferred = "Deferred"
+ releaseOperationStatusEvaluatingGates = "EvaluatingGates"
+ releaseOperationStatusGateFailed = "GateFailed"
+ releaseOperationStatusManualInterventionPending = "ManualInterventionPending"
+ releaseOperationStatusPending = "Pending"
+ releaseOperationStatusPhaseCanceled = "PhaseCanceled"
+ releaseOperationStatusPhaseFailed = "PhaseFailed"
+ releaseOperationStatusPhaseInProgress = "PhaseInProgress"
+ releaseOperationStatusPhasePartiallySucceeded = "PhasePartiallySucceeded"
+ releaseOperationStatusPhaseSucceeded = "PhaseSucceeded"
+ releaseOperationStatusQueued = "Queued"
+ releaseOperationStatusRejected = "Rejected"
+ releaseOperationStatusScheduled = "Scheduled"
+ releaseOperationStatusUndefined = "Undefined"
+)
+
+var releaseDeploymentResultRule = devops.ResultRule{
+ Success: []string{releaseDeploymentStatusSucceeded, releaseDeploymentStatusPartiallySucceeded},
+ Failure: []string{releaseDeploymentStatusFailed, releaseDeploymentStatusNotDeployed},
+ Default: devops.RESULT_DEFAULT,
+}
+
+var releaseDeploymentStatusRule = devops.StatusRule{
+ Done: []string{releaseOperationStatusApproved, releaseOperationStatusCanceled, releaseOperationStatusRejected, releaseOperationStatusPhaseCanceled, releaseOperationStatusPhaseFailed, releaseOperationStatusPhasePartiallySucceeded, releaseOperationStatusPhaseSucceeded, releaseOperationStatusGateFailed},
+ InProgress: []string{releaseOperationStatusPending, releaseOperationStatusQueued, releaseOperationStatusScheduled, releaseOperationStatusDeferred, releaseOperationStatusCancelling, releaseOperationStatusEvaluatingGates, releaseOperationStatusManualInterventionPending, releaseOperationStatusPhaseInProgress, releaseOperationStatusUndefined},
+ Default: devops.STATUS_OTHER,
+}
+
+func ConvertReleaseDeployments(taskCtx plugin.SubTaskContext) errors.Error {
+ db := taskCtx.GetDal()
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RawReleaseDeploymentTable)
+ clauses := []dal.Clause{
+ dal.Select("*"),
+ dal.From(&models.AzuredevopsReleaseDeployment{}),
+ dal.Where("project_id = ? and connection_id = ?",
+ data.Options.ProjectId, data.Options.ConnectionId),
+ }
+
+ cursor, err := db.Cursor(clauses...)
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+
+ deploymentIdGen := didgen.NewDomainIdGenerator(&models.AzuredevopsReleaseDeployment{})
+ projectIdGen := didgen.NewDomainIdGenerator(&models.AzuredevopsProject{})
+
+ converter, err := api.NewDataConverter(api.DataConverterArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ InputRowType: reflect.TypeOf(models.AzuredevopsReleaseDeployment{}),
+ Input: cursor,
+ Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+ deployment := inputRow.(*models.AzuredevopsReleaseDeployment)
+ duration := 0.0
+
+ if deployment.CompletedOn != nil && deployment.StartedOn != nil {
+ duration = float64(deployment.CompletedOn.Sub(*deployment.StartedOn).Milliseconds() / 1e3)
+ }
+
+ // Create a unique pipeline name combining release definition and environment
+ pipelineName := deployment.DefinitionName
+ if deployment.EnvironmentName != "" {
+ pipelineName = deployment.DefinitionName + " - " + deployment.EnvironmentName
+ }
+
+ domainPipeline := &devops.CICDPipeline{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: deploymentIdGen.Generate(data.Options.ConnectionId, deployment.AzuredevopsId),
+ },
+ Name: pipelineName,
+ Result: devops.GetResult(&releaseDeploymentResultRule, deployment.DeploymentStatus),
+ Status: devops.GetStatus(&releaseDeploymentStatusRule, deployment.OperationStatus),
+ OriginalStatus: deployment.OperationStatus,
+ OriginalResult: deployment.DeploymentStatus,
+ CicdScopeId: projectIdGen.Generate(data.Options.ConnectionId, data.Options.ProjectId),
+ Environment: data.RegexEnricher.ReturnNameIfMatched(devops.PRODUCTION, pipelineName+";"+deployment.EnvironmentName),
+ Type: devops.DEPLOYMENT,
+ DurationSec: duration,
+ }
+
+ if deployment.QueuedOn != nil {
+ domainPipeline.TaskDatesInfo = devops.TaskDatesInfo{
+ CreatedDate: *deployment.QueuedOn,
+ QueuedDate: deployment.QueuedOn,
+ StartedDate: deployment.StartedOn,
+ FinishedDate: deployment.CompletedOn,
+ }
+ }
+
+ return []interface{}{
+ domainPipeline,
+ }, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return converter.Execute()
+}
diff --git a/backend/plugins/azuredevops_go/tasks/release_deployment_extractor.go b/backend/plugins/azuredevops_go/tasks/release_deployment_extractor.go
new file mode 100644
index 000000000..af75bdc1f
--- /dev/null
+++ b/backend/plugins/azuredevops_go/tasks/release_deployment_extractor.go
@@ -0,0 +1,91 @@
+/*
+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 (
+ "encoding/json"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/azuredevops_go/models"
+)
+
+func init() {
+ RegisterSubtaskMeta(&ExtractApiReleaseDeploymentsMeta)
+}
+
+var ExtractApiReleaseDeploymentsMeta = plugin.SubTaskMeta{
+ Name: "extractApiReleaseDeployments",
+ EntryPoint: ExtractApiReleaseDeployments,
+ EnabledByDefault: true,
+ Description: "Extract raw release deployment data into tool layer table",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CICD},
+ DependencyTables: []string{RawReleaseDeploymentTable},
+ ProductTables: []string{
+ models.AzuredevopsReleaseDeployment{}.TableName(),
+ },
+}
+
+func ExtractApiReleaseDeployments(taskCtx plugin.SubTaskContext) errors.Error {
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RawReleaseDeploymentTable)
+
+ extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+ results := make([]interface{}, 0, 1)
+
+ deploymentApi := &models.AzuredevopsApiDeployment{}
+ err := errors.Convert(json.Unmarshal(row.Data, deploymentApi))
+ if err != nil {
+ return nil, err
+ }
+
+ deployment := &models.AzuredevopsReleaseDeployment{
+ ConnectionId: data.Options.ConnectionId,
+ AzuredevopsId: deploymentApi.Id,
+ ReleaseId: deploymentApi.Release.Id,
+ ProjectId: data.Options.ProjectId,
+ Name: deploymentApi.Release.Name,
+ Status: deploymentApi.OperationStatus,
+ OperationStatus: deploymentApi.OperationStatus,
+ DeploymentStatus: deploymentApi.DeploymentStatus,
+ DefinitionName: deploymentApi.ReleaseDefinition.Name,
+ DefinitionId: deploymentApi.ReleaseDefinition.Id,
+ EnvironmentId: deploymentApi.ReleaseEnvironment.Id,
+ EnvironmentName: deploymentApi.ReleaseEnvironment.Name,
+ AttemptNumber: deploymentApi.Attempt,
+ Reason: deploymentApi.Reason,
+ QueuedOn: deploymentApi.QueuedOn,
+ StartedOn: deploymentApi.StartedOn,
+ CompletedOn: deploymentApi.CompletedOn,
+ LastModifiedOn: deploymentApi.LastModifiedOn,
+ }
+
+ results = append(results, deployment)
+
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/backend/plugins/azuredevops_go/tasks/release_extractor.go b/backend/plugins/azuredevops_go/tasks/release_extractor.go
new file mode 100644
index 000000000..ee01a06ce
--- /dev/null
+++ b/backend/plugins/azuredevops_go/tasks/release_extractor.go
@@ -0,0 +1,83 @@
+/*
+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 (
+ "encoding/json"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/azuredevops_go/models"
+)
+
+func init() {
+ RegisterSubtaskMeta(&ExtractApiReleasesMeta)
+}
+
+var ExtractApiReleasesMeta = plugin.SubTaskMeta{
+ Name: "extractApiReleases",
+ EntryPoint: ExtractApiReleases,
+ EnabledByDefault: true,
+ Description: "Extract raw release data into tool layer table",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CICD},
+ DependencyTables: []string{RawReleaseTable},
+ ProductTables: []string{
+ models.AzuredevopsRelease{}.TableName(),
+ },
+}
+
+func ExtractApiReleases(taskCtx plugin.SubTaskContext) errors.Error {
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RawReleaseTable)
+
+ extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+ results := make([]interface{}, 0, 1)
+
+ releaseApi := &models.AzuredevopsApiRelease{}
+ err := errors.Convert(json.Unmarshal(row.Data, releaseApi))
+ if err != nil {
+ return nil, err
+ }
+
+ release := &models.AzuredevopsRelease{
+ ConnectionId: data.Options.ConnectionId,
+ AzuredevopsId: releaseApi.Id,
+ ProjectId: data.Options.ProjectId,
+ Name: releaseApi.Name,
+ Status: releaseApi.Status,
+ ReleaseDefinitionId: releaseApi.ReleaseDefinition.Id,
+ ReleaseDefinitionName: releaseApi.ReleaseDefinition.Name,
+ Description: releaseApi.Description,
+ CreatedOn: releaseApi.CreatedOn,
+ ModifiedOn: releaseApi.ModifiedOn,
+ }
+
+ results = append(results, release)
+
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}