Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/plugins/schema_e2e/migration_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import (
testmo "github.com/apache/devlake/plugins/testmo/impl"
trello "github.com/apache/devlake/plugins/trello/impl"
webhook "github.com/apache/devlake/plugins/webhook/impl"
youtrack "github.com/apache/devlake/plugins/youtrack/impl"
zentao "github.com/apache/devlake/plugins/zentao/impl"
)

Expand Down Expand Up @@ -141,6 +142,7 @@ func allGoPlugins() []plugin.PluginMeta {
testmo.Testmo{},
trello.Trello{},
webhook.Webhook{},
youtrack.Youtrack{},
zentao.Zentao{},
}
}
Expand Down
2 changes: 2 additions & 0 deletions backend/plugins/table_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import (
testmo "github.com/apache/devlake/plugins/testmo/impl"
trello "github.com/apache/devlake/plugins/trello/impl"
webhook "github.com/apache/devlake/plugins/webhook/impl"
youtrack "github.com/apache/devlake/plugins/youtrack/impl"
zentao "github.com/apache/devlake/plugins/zentao/impl"
)

Expand Down Expand Up @@ -108,6 +109,7 @@ func Test_GetPluginTablesInfo(t *testing.T) {
checker.FeedIn("testmo/models", testmo.Testmo{}.GetTablesInfo)
checker.FeedIn("trello/models", trello.Trello{}.GetTablesInfo)
checker.FeedIn("webhook/models", webhook.Webhook{}.GetTablesInfo)
checker.FeedIn("youtrack/models", youtrack.Youtrack{}.GetTablesInfo)
checker.FeedIn("zentao/models", zentao.Zentao{}.GetTablesInfo)
checker.FeedIn("claude_code/models", claudeCode.ClaudeCode{}.GetTablesInfo)
checker.FeedIn("cursor/models", cursor.Cursor{}.GetTablesInfo)
Expand Down
102 changes: 102 additions & 0 deletions backend/plugins/youtrack/api/blueprint_v200.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
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/devlake/core/errors"
coreModels "github.com/apache/devlake/core/models"
"github.com/apache/devlake/core/models/domainlayer/didgen"
"github.com/apache/devlake/core/models/domainlayer/ticket"
"github.com/apache/devlake/core/plugin"
"github.com/apache/devlake/core/utils"
helper "github.com/apache/devlake/helpers/pluginhelper/api"
"github.com/apache/devlake/helpers/srvhelper"
"github.com/apache/devlake/plugins/youtrack/models"
"github.com/apache/devlake/plugins/youtrack/tasks"
)

func MakePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
connectionId uint64,
bpScopes []*coreModels.BlueprintScope,
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
if err != nil {
return nil, nil, err
}
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
if err != nil {
return nil, nil, err
}
plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection)
if err != nil {
return nil, nil, err
}
scopes, err := makeScopesV200(scopeDetails, connection)
return plan, scopes, err
}

func makePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
scopeDetails []*srvhelper.ScopeDetail[models.YoutrackProject, models.YoutrackScopeConfig],
connection *models.YoutrackConnection,
) (coreModels.PipelinePlan, errors.Error) {
plan := make(coreModels.PipelinePlan, len(scopeDetails))
for i, scopeDetail := range scopeDetails {
stage := plan[i]
if stage == nil {
stage = coreModels.PipelineStage{}
}
scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
task, err := helper.MakePipelinePlanTask(
"youtrack",
subtaskMetas,
scopeConfig.Entities,
tasks.YoutrackOptions{
ConnectionId: connection.ID,
ProjectId: scope.Id,
ScopeConfigId: scope.ScopeConfigId,
},
)
if err != nil {
return nil, err
}
stage = append(stage, task)
plan[i] = stage
}
return plan, nil
}

// makeScopesV200 maps each attached project to its domain scope: a
// ticket.Board. The board id is generated from the internal project id only,
// so it stays stable when the project's shortName is renamed upstream.
func makeScopesV200(
scopeDetails []*srvhelper.ScopeDetail[models.YoutrackProject, models.YoutrackScopeConfig],
connection *models.YoutrackConnection,
) ([]plugin.Scope, errors.Error) {
scopes := make([]plugin.Scope, 0, len(scopeDetails))
idgen := didgen.NewDomainIdGenerator(&models.YoutrackProject{})
for _, scopeDetail := range scopeDetails {
scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
id := idgen.Generate(connection.ID, scope.Id)
if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) {
scopes = append(scopes, ticket.NewBoard(id, scope.Name))
}
}
return scopes, nil
}
134 changes: 134 additions & 0 deletions backend/plugins/youtrack/api/blueprint_v200_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
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 (
"testing"

"github.com/apache/devlake/core/models/common"
"github.com/apache/devlake/core/plugin"
helper "github.com/apache/devlake/helpers/pluginhelper/api"
"github.com/apache/devlake/helpers/srvhelper"
mockplugin "github.com/apache/devlake/mocks/core/plugin"
"github.com/apache/devlake/plugins/youtrack/models"
"github.com/stretchr/testify/assert"
)

func mockYoutrackPlugin(t *testing.T) {
mockMeta := mockplugin.NewPluginMeta(t)
mockMeta.On("RootPkgPath").Return("github.com/apache/devlake/plugins/youtrack")
mockMeta.On("Name").Return("youtrack").Maybe()
_ = plugin.RegisterPlugin("youtrack", mockMeta)
}

func TestMakeScopesV200(t *testing.T) {
mockYoutrackPlugin(t)

const connectionId uint64 = 1
const projectId = "0-12"
const expectDomainScopeId = "youtrack:YoutrackProject:1:0-12"

scopes, err := makeScopesV200(
[]*srvhelper.ScopeDetail[models.YoutrackProject, models.YoutrackScopeConfig]{
{
Scope: models.YoutrackProject{
Scope: common.Scope{ConnectionId: connectionId},
Id: projectId,
ShortName: "PROJ2",
Name: "Project 2",
},
ScopeConfig: &models.YoutrackScopeConfig{
ScopeConfig: common.ScopeConfig{Entities: []string{plugin.DOMAIN_TYPE_TICKET}},
},
},
},
&models.YoutrackConnection{
BaseConnection: helper.BaseConnection{Model: common.Model{ID: connectionId}},
},
)
assert.Nil(t, err)
assert.Equal(t, 1, len(scopes))
// the domain board id is built from the internal project id, so it
// survives a shortName rename
assert.Equal(t, expectDomainScopeId, scopes[0].ScopeId())
// the board is named after the project, not the cryptic internal id
assert.Equal(t, "Project 2", scopes[0].ScopeName())
}

func TestMakeScopesV200WithoutTicketEntity(t *testing.T) {
mockYoutrackPlugin(t)

scopes, err := makeScopesV200(
[]*srvhelper.ScopeDetail[models.YoutrackProject, models.YoutrackScopeConfig]{
{
Scope: models.YoutrackProject{
Scope: common.Scope{ConnectionId: 1},
Id: "0-12",
Name: "Project 2",
},
ScopeConfig: &models.YoutrackScopeConfig{
ScopeConfig: common.ScopeConfig{Entities: []string{plugin.DOMAIN_TYPE_CROSS}},
},
},
},
&models.YoutrackConnection{
BaseConnection: helper.BaseConnection{Model: common.Model{ID: 1}},
},
)
assert.Nil(t, err)
// no ticket entity selected => no domain board scope produced
assert.Equal(t, 0, len(scopes))
}

func TestMakePipelinePlanV200PassesScopeParams(t *testing.T) {
const scopeConfigId uint64 = 42
subtaskMetas := []plugin.SubTaskMeta{
{Name: "convertIssues", EnabledByDefault: true, DomainTypes: []string{plugin.DOMAIN_TYPE_TICKET}},
}

plan, err := makePipelinePlanV200(
subtaskMetas,
[]*srvhelper.ScopeDetail[models.YoutrackProject, models.YoutrackScopeConfig]{
{
Scope: models.YoutrackProject{
Scope: common.Scope{ConnectionId: 1, ScopeConfigId: scopeConfigId},
Id: "0-12",
ShortName: "PROJ2",
Name: "Project 2",
},
ScopeConfig: &models.YoutrackScopeConfig{
ScopeConfig: common.ScopeConfig{Entities: []string{plugin.DOMAIN_TYPE_TICKET}},
},
},
},
&models.YoutrackConnection{
BaseConnection: helper.BaseConnection{Model: common.Model{ID: 1}},
},
)
assert.Nil(t, err)
assert.Equal(t, 1, len(plan))
assert.Equal(t, 1, len(plan[0]))
// the scope's project id and scope config must be threaded into the task
// options so PrepareTaskData can scope collection and resolve the
// field-name slots and mappings at runtime
assert.Equal(t, "0-12", plan[0][0].Options["projectId"])
assert.EqualValues(t, scopeConfigId, plan[0][0].Options["scopeConfigId"])
assert.EqualValues(t, 1, plan[0][0].Options["connectionId"])
// the generated subtask list reflects the entity selection
assert.Contains(t, plan[0][0].Subtasks, "convertIssues")
}
Loading
Loading