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
229 changes: 226 additions & 3 deletions pkg/microservice/aslan/core/plugin/service/lark_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,52 @@ func UpdateLarkWorkItemStageWorkflowInputV2(ctx *internalhandler.Context, stageN
}

func ExecuteLarkWorkitemWorkflowV2(ctx *internalhandler.Context, workspaceID, workItemTypeKey, workItemID string) error {
templateID, nodeID, err := getWorkItemInfo(ctx, workspaceID, workItemTypeKey, workItemID)
workItemIDInt, err := strconv.ParseInt(workItemID, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse workitem id: %w", err)
}

larkClient := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), ctx.LarkPlugin.LarkType)
workItemResp, err := larkClient.ClientV2.WorkItem.GetWorkItemsByIds(ctx, workitem.NewGetWorkItemsByIdsReqBuilder().
ProjectKey(workspaceID).
WorkItemTypeKey(workItemTypeKey).
WorkItemIDs([]int64{workItemIDInt}).
Build(),
sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken),
sdkcore.WithUserKey(ctx.LarkPlugin.UserKey),
)
if err != nil {
return fmt.Errorf("failed to get work item info: %w", err)
return fmt.Errorf("failed to get lark workitem: %w", err)
}
if workItemResp.Code() != 0 {
return fmt.Errorf("failed to get lark workitem, code: %d, message: %s", workItemResp.Code(), workItemResp.ErrMsg)
}
if len(workItemResp.Data) == 0 {
return fmt.Errorf("workitem could not be found")
}

currentWorkItem := workItemResp.Data[0]
currentWorkItemPattern := util.GetStringFromPointer(currentWorkItem.Pattern)

templateID := util.GetInt64FromPointer(currentWorkItem.TemplateID)
currentNodeIDs := make([]string, 0)
if currentWorkItemPattern == string(meego.WorkItemPatternNode) {
for _, currentNode := range currentWorkItem.CurrentNodes {
currentNodeIDs = append(currentNodeIDs, util.GetStringFromPointer(currentNode.ID))
}
} else if currentWorkItemPattern == string(meego.WorkItemPatternState) {
if currentWorkItem.WorkItemStatus == nil {
return fmt.Errorf("workitem status could not be found")
}
currentNodeIDs = append(currentNodeIDs, util.GetStringFromPointer(currentWorkItem.WorkItemStatus.StateKey))
} else {
return fmt.Errorf("unsupported pattern %s", currentWorkItemPattern)
}
if len(currentNodeIDs) == 0 {
return fmt.Errorf("no current node found")
}
nodeID := currentNodeIDs[0]

workflowConfig, err := mongodb.NewLarkPluginWorkflowConfigV2Coll().Find(workspaceID, workItemTypeKey, templateID, nodeID)
if err != nil {
return fmt.Errorf("failed to find workflow config: %w", err)
Expand Down Expand Up @@ -445,7 +486,6 @@ func ExecuteLarkWorkitemWorkflowV2(ctx *internalhandler.Context, workspaceID, wo
}

// Get Lark project info for task metadata
larkClient := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), ctx.LarkPlugin.LarkType)
projectResp, err := larkClient.Client.Project.GetProjectDetail(ctx, project.NewGetProjectDetailReqBuilder().
ProjectKeys([]string{workspaceID}).
Build(),
Expand All @@ -472,13 +512,23 @@ func ExecuteLarkWorkitemWorkflowV2(ctx *internalhandler.Context, workspaceID, wo
return fmt.Errorf("failed to list project work item type: %w", err)
}

projectName := projectResp.Data[workspaceID].Name
workItemTypeName := stageConfig.WorkItemType
if workItemTypeName == "" {
workItemTypeName = workItemTypeKey
}
workitemTypeApiName := workItemTypeKey
for _, workitemType := range projectWorkItemTypes.Data {
if workitemType.TypeKey == workItemTypeKey {
workItemTypeName = workitemType.Name
workitemTypeApiName = workitemType.APIName
break
}
}
meegoLink := ""
if baseURL := strings.TrimRight(larkplugin.GetLarkPluginBaseUrl(ctx.LarkPlugin.LarkType), "/"); baseURL != "" {
meegoLink = fmt.Sprintf("%s/%s/%s/detail/%s", baseURL, projectResp.Data[workspaceID].SimpleName, workitemTypeApiName, workItemID)
}

// Build lookup map from user-selected service configs
serviceConfigMap := make(map[string]*commonmodels.LarkPluginWorkItemStageWorkflowInputConfig)
Expand Down Expand Up @@ -594,6 +644,44 @@ func ExecuteLarkWorkitemWorkflowV2(ctx *internalhandler.Context, workspaceID, wo
}
// ProductTestType: keep all defaults
job.Spec = testSpec
case aslanconfig.JobMeegoTransition:
if workflowConfig.StageName != "release" {
job.Skipped = true
continue
}

meegoTransitionSpec := &commonmodels.MeegoTransitionJobSpec{}
if err := commonmodels.IToiYaml(job.Spec, meegoTransitionSpec); err != nil {
return fmt.Errorf("failed to parse meego transition job spec: %w", err)
}

statusWorkItems := make([]*commonmodels.MeegoWorkItemTransition, 0)
nodeWorkItems := make([]*commonmodels.MeegoWorkItemNodeOperate, 0)
switch currentWorkItemPattern {
case string(meego.WorkItemPatternState):
statusWorkItem, err := buildAutoMeegoStatusWorkItemForLarkV2(ctx, larkClient, workspaceID, workItemTypeKey, workItemIDInt, currentWorkItem, meegoTransitionSpec.StatusWorkItems)
if err != nil {
return fmt.Errorf("failed to build meego status work item: %w", err)
}
statusWorkItems = append(statusWorkItems, statusWorkItem)
case string(meego.WorkItemPatternNode):
nodeWorkItems, err = buildAutoMeegoNodeWorkItemsForLarkV2(currentWorkItem)
if err != nil {
return fmt.Errorf("failed to build meego node work items: %w", err)
}
default:
return fmt.Errorf("unsupported pattern %s", currentWorkItemPattern)
}

meegoTransitionSpec.ProjectName = projectName
meegoTransitionSpec.ProjectKey = workspaceID
meegoTransitionSpec.WorkItemType = workItemTypeName
meegoTransitionSpec.WorkItemTypeKey = workItemTypeKey
meegoTransitionSpec.StatusWorkItems = statusWorkItems
meegoTransitionSpec.NodeWorkItems = nodeWorkItems
meegoTransitionSpec.Link = meegoLink

job.Spec = meegoTransitionSpec
}
}
}
Expand All @@ -615,6 +703,141 @@ func ExecuteLarkWorkitemWorkflowV2(ctx *internalhandler.Context, workspaceID, wo
return nil
}

func buildAutoMeegoStatusWorkItemForLarkV2(ctx *internalhandler.Context,
larkClient *larkplugin.Client, workspaceID, workItemTypeKey string, workItemID int64,
currentWorkItem workitem.WorkItem_work_item_WorkItemInfo, templateItems []*commonmodels.MeegoWorkItemTransition,
) (*commonmodels.MeegoWorkItemTransition, error) {
if currentWorkItem.WorkItemStatus == nil {
return nil, fmt.Errorf("workitem status could not be found")
}

currentStateKey := util.GetStringFromPointer(currentWorkItem.WorkItemStatus.StateKey)
if currentStateKey == "" {
return nil, fmt.Errorf("current state key could not be found")
}

workflowResp, err := larkClient.ClientV2.WorkItem.GetWorkFlow(ctx, workitem.NewGetWorkFlowReqBuilder().
ProjectKey(workspaceID).
WorkItemTypeKey(workItemTypeKey).
WorkItemID(workItemID).
FlowType(int64(meego.StatusFlowType)).
Build(),
sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken),
sdkcore.WithUserKey(ctx.LarkPlugin.UserKey),
)
if err != nil {
return nil, fmt.Errorf("failed to get lark workflow: %w", err)
}
if workflowResp.Code() != 0 {
return nil, fmt.Errorf("failed to get lark workflow, code: %d, message: %s", workflowResp.Code(), workflowResp.ErrMsg)
}
if workflowResp.Data == nil {
return nil, fmt.Errorf("workflow data could not be found")
}

preferredTargetStateKey := ""
preferredTargetStateName := ""
for _, item := range templateItems {
if item == nil {
continue
}
if item.TargetStateKey != "" {
preferredTargetStateKey = item.TargetStateKey
break
}
if preferredTargetStateName == "" && item.TargetStateName != "" {
preferredTargetStateName = item.TargetStateName
}
}
if preferredTargetStateKey == "" && preferredTargetStateName != "" {
for _, state := range workflowResp.Data.StateFlowNodes {
if util.GetStringFromPointer(state.Name) == preferredTargetStateName {
preferredTargetStateKey = util.GetStringFromPointer(state.ID)
break
}
}
}

transitionID := int64(0)
targetStateKey := ""
hasAvailableTransition := false
matchedPreferred := preferredTargetStateKey == ""
for _, conn := range workflowResp.Data.Connections {
if util.GetStringFromPointer(conn.SourceStateKey) != currentStateKey {
continue
}

hasAvailableTransition = true
currentTransitionID := util.GetInt64FromPointer(conn.TransitionID)
if currentTransitionID == 0 {
continue
}
if transitionID == 0 {
transitionID = currentTransitionID
targetStateKey = util.GetStringFromPointer(conn.TargetStateKey)
}
if preferredTargetStateKey != "" && util.GetStringFromPointer(conn.TargetStateKey) == preferredTargetStateKey {
transitionID = currentTransitionID
targetStateKey = preferredTargetStateKey
matchedPreferred = true
break
}
}
if !hasAvailableTransition {
return nil, fmt.Errorf("no available next state transition from state %s", currentStateKey)
}
if preferredTargetStateKey != "" && !matchedPreferred {
return nil, fmt.Errorf("no available transition from state %s to target state %s", currentStateKey, preferredTargetStateKey)
}
if transitionID == 0 {
return nil, fmt.Errorf("next state transition is incomplete")
}

targetStateName := ""
for _, state := range workflowResp.Data.StateFlowNodes {
if util.GetStringFromPointer(state.ID) == targetStateKey {
targetStateName = util.GetStringFromPointer(state.Name)
break
}
}

return &commonmodels.MeegoWorkItemTransition{
ID: int(util.GetInt64FromPointer(currentWorkItem.ID)),
Name: util.GetStringFromPointer(currentWorkItem.Name),
TransitionID: transitionID,
TargetStateKey: targetStateKey,
TargetStateName: targetStateName,
}, nil
}

func buildAutoMeegoNodeWorkItemsForLarkV2(currentWorkItem workitem.WorkItem_work_item_WorkItemInfo) ([]*commonmodels.MeegoWorkItemNodeOperate, error) {
if len(currentWorkItem.CurrentNodes) == 0 {
return nil, fmt.Errorf("no current nodes found")
}

seenNodeIDs := sets.New[string]()
resp := make([]*commonmodels.MeegoWorkItemNodeOperate, 0, len(currentWorkItem.CurrentNodes))
for _, node := range currentWorkItem.CurrentNodes {
nodeID := util.GetStringFromPointer(node.ID)
if nodeID == "" || seenNodeIDs.Has(nodeID) {
continue
}
seenNodeIDs.Insert(nodeID)
resp = append(resp, &commonmodels.MeegoWorkItemNodeOperate{
ID: int(util.GetInt64FromPointer(currentWorkItem.ID)),
Name: util.GetStringFromPointer(currentWorkItem.Name),
NodeID: nodeID,
NodeName: util.GetStringFromPointer(node.Name),
})
}

if len(resp) == 0 {
return nil, fmt.Errorf("no current nodes found")
}

return resp, nil
}

type LarkWorkItemResp struct {
ID int64 `json:"id"`
Name string `json:"name"`
Expand Down
12 changes: 6 additions & 6 deletions pkg/microservice/aslan/core/plugin/service/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func validateWorkflowForLarkPlugin(workflowName string) error {
return fmt.Errorf("failed to find workflow %s: %w", workflowName, err)
}

var buildCount, deployCount, testCount int
var buildCount, deployCount, testCount, meegoCount int
buildIdx, deployIdx := -1, -1
jobIdx := 0

Expand All @@ -56,6 +56,8 @@ func validateWorkflowForLarkPlugin(workflowName string) error {
deployIdx = jobIdx
case aslanconfig.JobZadigTesting:
testCount++
case aslanconfig.JobMeegoTransition:
meegoCount++
default:
return fmt.Errorf("workflow %s contains unsupported job type %s, only build, deploy, and test jobs are allowed", workflowName, job.JobType)
}
Expand Down Expand Up @@ -123,12 +125,10 @@ func getWorkItemInfo(ctx *internalhandler.Context, workspaceID, workItemType, wo
return 0, "", fmt.Errorf("no current node found")
}



templateID = util.GetInt64FromPointer(workItem.TemplateID)
nodeID = currentNodeIDs[0]

return
return
}

// getWorkflowFromWorkItem gets the workflow from the given work item
Expand All @@ -138,7 +138,7 @@ func getWorkflowFromWorkItem(ctx *internalhandler.Context, workspaceID, workItem
return nil, fmt.Errorf("failed to get work item info: %s", err)
}

config, err := mongodb.NewLarkPluginWorkflowConfigV2Coll().Find(workspaceID, workItemType, templateID, nodeID)
config, err := mongodb.NewLarkPluginWorkflowConfigV2Coll().Find(workspaceID, workItemType, templateID, nodeID)
if err != nil {
return nil, fmt.Errorf("failed to find workflow config: %w", err)
}
Expand Down Expand Up @@ -195,4 +195,4 @@ func getFirstRepoFromWorkflow(workflow *models.WorkflowV4, serviceName, serviceM
}

return buildInfo.Repos[0], nil
}
}
Loading