From 895228284b0db2c8651792e27b2ee243cfff0673 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 7 May 2026 14:37:15 +0800 Subject: [PATCH 01/22] feat(workflow): pass through webhook payload to runtime notifications Signed-off-by: huanghongbo-hhb --- .../repository/models/wokflow_task_v4.go | 1 + .../core/common/repository/models/workflow.go | 1 + .../common/repository/models/workflow_v4.go | 64 ++- .../jobcontroller/job_notification.go | 423 +++++++++++++++++- .../service/workflowcontroller/workflow.go | 11 +- .../core/common/util/workflow_variables.go | 249 +++++++++++ .../service/webhook/gerrit_workflowv4_task.go | 5 + .../core/workflow/service/webhook/gitee.go | 6 +- .../service/webhook/gitee_workflowv4_task.go | 13 +- .../core/workflow/service/webhook/github.go | 6 +- .../service/webhook/github_workflowv4_task.go | 13 +- .../core/workflow/service/webhook/gitlab.go | 6 +- .../service/webhook/gitlab_workflowv4_task.go | 13 +- .../controller/job/job_notification.go | 13 + .../service/workflow/controller/job/utils.go | 59 +-- .../service/workflow/controller/workflow.go | 10 + .../core/workflow/service/workflow/types.go | 36 +- .../service/workflow/workflow_task_v4.go | 102 ++++- pkg/types/repo.go | 10 +- 19 files changed, 907 insertions(+), 134 deletions(-) create mode 100644 pkg/microservice/aslan/core/common/util/workflow_variables.go diff --git a/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go b/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go index f68200a438..78d0ee39ea 100644 --- a/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go @@ -794,6 +794,7 @@ type LarkChat struct { type JobTaskNotificationSpec struct { WebHookType setting.NotifyWebHookType `bson:"webhook_type" yaml:"webhook_type" json:"webhook_type"` + LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"` LarkGroupNotificationConfig *LarkGroupNotificationConfig `bson:"lark_group_notification_config,omitempty" yaml:"lark_group_notification_config,omitempty" json:"lark_group_notification_config,omitempty"` LarkPersonNotificationConfig *LarkPersonNotificationConfig `bson:"lark_person_notification_config,omitempty" yaml:"lark_person_notification_config,omitempty" json:"lark_person_notification_config,omitempty"` WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"` diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow.go b/pkg/microservice/aslan/core/common/repository/models/workflow.go index 20ffe6e6c9..fbb17ba28b 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -493,6 +493,7 @@ type HookPayload struct { DeliveryID string `bson:"delivery_id" json:"delivery_id,omitempty"` CodehostID int `bson:"codehost_id" json:"codehost_id"` EventType string `bson:"event_type" json:"event_type"` + RawPayload string `bson:"raw_payload" json:"raw_payload,omitempty"` } type TargetArgs struct { diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go index 913082a42a..08b4147927 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1173,12 +1173,12 @@ type NotificationJobSpec struct { LarkGroupNotificationConfig *LarkGroupNotificationConfig `bson:"lark_group_notification_config,omitempty" yaml:"lark_group_notification_config,omitempty" json:"lark_group_notification_config,omitempty"` LarkPersonNotificationConfig *LarkPersonNotificationConfig `bson:"lark_person_notification_config,omitempty" yaml:"lark_person_notification_config,omitempty" json:"lark_person_notification_config,omitempty"` - //LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"` - WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"` - DingDingNotificationConfig *DingDingNotificationConfig `bson:"dingding_notification_config,omitempty" yaml:"dingding_notification_config,omitempty" json:"dingding_notification_config,omitempty"` - MSTeamsNotificationConfig *MSTeamsNotificationConfig `bson:"msteams_notification_config,omitempty" yaml:"msteams_notification_config,omitempty" json:"msteams_notification_config,omitempty"` - MailNotificationConfig *MailNotificationConfig `bson:"mail_notification_config,omitempty" yaml:"mail_notification_config,omitempty" json:"mail_notification_config,omitempty"` - WebhookNotificationConfig *WebhookNotificationConfig `bson:"webhook_notification_config,omitempty" yaml:"webhook_notification_config,omitempty" json:"webhook_notification_config,omitempty"` + LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"` + WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"` + DingDingNotificationConfig *DingDingNotificationConfig `bson:"dingding_notification_config,omitempty" yaml:"dingding_notification_config,omitempty" json:"dingding_notification_config,omitempty"` + MSTeamsNotificationConfig *MSTeamsNotificationConfig `bson:"msteams_notification_config,omitempty" yaml:"msteams_notification_config,omitempty" json:"msteams_notification_config,omitempty"` + MailNotificationConfig *MailNotificationConfig `bson:"mail_notification_config,omitempty" yaml:"mail_notification_config,omitempty" json:"mail_notification_config,omitempty"` + WebhookNotificationConfig *WebhookNotificationConfig `bson:"webhook_notification_config,omitempty" yaml:"webhook_notification_config,omitempty" json:"webhook_notification_config,omitempty"` Content string `bson:"content" yaml:"content" json:"content"` Title string `bson:"title" yaml:"title" json:"title"` @@ -1262,6 +1262,10 @@ func (n *NotificationJobSpec) GenerateNewNotifyConfigWithOldData() error { if n.LarkPersonNotificationConfig == nil { return fmt.Errorf("lark_person_notification_config cannot be empty for type feishu_person notification") } + case setting.NotifyWebHookTypeFeishu: + if n.LarkHookNotificationConfig == nil { + return fmt.Errorf("lark_hook_notification_config cannot be empty for type feishu notification") + } default: // TODO: this code is commented because of chagee old data. uncomment it if possible //return fmt.Errorf("unsupported notification type: %s", n.WebHookType) @@ -1270,44 +1274,56 @@ func (n *NotificationJobSpec) GenerateNewNotifyConfigWithOldData() error { return nil } +type DynamicRecipient struct { + Value string `bson:"value" json:"value" yaml:"value"` + IdentityType string `bson:"identity_type" json:"identity_type" yaml:"identity_type"` +} + // TODO: why is_at_all? it could be done in backend type LarkGroupNotificationConfig struct { - AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` - Chat *LarkChat `bson:"chat" json:"chat" yaml:"chat"` - AtUsers []*lark.UserInfo `bson:"at_users" json:"at_users" yaml:"at_users"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + Chat *LarkChat `bson:"chat" json:"chat" yaml:"chat"` + AtUsers []*lark.UserInfo `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type LarkPersonNotificationConfig struct { - AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` - TargetUsers []*lark.UserInfo `bson:"target_users" json:"target_users" yaml:"target_users"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + TargetUsers []*lark.UserInfo `bson:"target_users" json:"target_users" yaml:"target_users"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type LarkHookNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type WechatNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type DingDingNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtMobiles []string `bson:"at_mobiles" json:"at_mobiles" yaml:"at_mobiles"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtMobiles []string `bson:"at_mobiles" json:"at_mobiles" yaml:"at_mobiles"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type MSTeamsNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtEmails []string `bson:"at_emails" json:"at_emails" yaml:"at_emails"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtEmails []string `bson:"at_emails" json:"at_emails" yaml:"at_emails"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type MailNotificationConfig struct { - TargetUsers []*User `bson:"target_users" json:"target_users" yaml:"target_users"` + TargetUsers []*User `bson:"target_users" json:"target_users" yaml:"target_users"` + DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type WebhookNotificationConfig struct { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index dd2053f0e7..5c42464f8a 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -75,6 +75,14 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { c.job.Status = config.StatusRunning c.ack() + if err := c.prepareRuntimeNotificationFields(); err != nil { + c.logger.Error(err) + c.job.Status = config.StatusFailed + c.job.Error = err.Error() + c.ack() + return + } + if c.jobTaskSpec.WebHookType == setting.NotifyWebhookTypeFeishuApp { larkAtUserIDs := make([]string, 0) @@ -109,6 +117,15 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { c.ack() return } + } else if c.jobTaskSpec.WebHookType == setting.NotifyWebHookTypeFeishu { + err := sendLarkHookMessage(c.workflowCtx.ProjectName, c.workflowCtx.WorkflowName, c.workflowCtx.WorkflowDisplayName, c.workflowCtx.TaskID, c.jobTaskSpec.LarkHookNotificationConfig.HookAddress, c.jobTaskSpec.Title, c.jobTaskSpec.Content, c.jobTaskSpec.LarkHookNotificationConfig.AtUsers, c.jobTaskSpec.LarkHookNotificationConfig.IsAtAll) + if err != nil { + c.logger.Error(err) + c.job.Status = config.StatusFailed + c.job.Error = err.Error() + c.ack() + return + } } else if c.jobTaskSpec.WebHookType == setting.NotifyWebHookTypeMSTeam { err := sendMSTeamsMessage(c.workflowCtx.ProjectName, c.workflowCtx.WorkflowName, c.workflowCtx.WorkflowDisplayName, c.workflowCtx.TaskID, c.jobTaskSpec.MSTeamsNotificationConfig.HookAddress, c.jobTaskSpec.Title, c.jobTaskSpec.Content, c.jobTaskSpec.MSTeamsNotificationConfig.AtEmails) if err != nil { @@ -207,6 +224,330 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { return } +func (c *NotificationJobCtl) prepareRuntimeNotificationFields() error { + keyMap := c.buildRuntimeNotificationKeyMap() + + c.jobTaskSpec.Title = renderNotificationString(c.jobTaskSpec.Title, keyMap) + c.jobTaskSpec.Content = renderNotificationString(c.jobTaskSpec.Content, keyMap) + + if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { + cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, keyMap) + } + if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { + cfg.AtMobiles = renderNotificationStrings(cfg.AtMobiles, keyMap) + } + if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { + cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, keyMap) + } + if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { + cfg.AtEmails = renderNotificationStrings(cfg.AtEmails, keyMap) + } + return c.resolveDynamicRecipients(keyMap) +} + +func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string { + keyMap := make(map[string]string) + + insertKVs := func(kvs []*commonmodels.KeyVal) { + for _, kv := range kvs { + if kv == nil || kv.Key == "" || kv.GetValue() == "" { + continue + } + keyMap[kv.Key] = kv.GetValue() + } + } + + insertKVs(c.workflowCtx.WorkflowKeyVals) + return keyMap +} + +func renderNotificationStrings(inputs []string, keyMap map[string]string) []string { + if len(keyMap) == 0 { + return inputs + } + pairs := make([]string, 0, len(keyMap)*2) + for key, value := range keyMap { + pairs = append(pairs, "{{."+key+"}}", value) + } + replacer := strings.NewReplacer(pairs...) + + resp := make([]string, 0, len(inputs)) + for _, item := range inputs { + resp = append(resp, replacer.Replace(item)) + } + return resp +} + +func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) error { + if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { + users := c.resolveDynamicRecipientsToDirectValues(cfg.DynamicRecipients, keyMap, "open_id", "user_id", "id") + cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) + } + if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil { + users, err := c.resolveDynamicRecipientsToLarkUsers(cfg.DynamicRecipients, cfg.AppID, keyMap) + if err != nil { + return err + } + cfg.AtUsers = uniqLarkUsers(append(cfg.AtUsers, users...)) + } + if cfg := c.jobTaskSpec.LarkPersonNotificationConfig; cfg != nil { + users, err := c.resolveDynamicRecipientsToLarkUsers(cfg.DynamicRecipients, cfg.AppID, keyMap) + if err != nil { + return err + } + cfg.TargetUsers = uniqLarkUsers(append(cfg.TargetUsers, users...)) + } + if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { + emails, err := c.resolveDynamicRecipientsToEmails(cfg.DynamicRecipients, keyMap) + if err != nil { + return err + } + cfg.AtEmails = lo.Uniq(append(cfg.AtEmails, emails...)) + } + if cfg := c.jobTaskSpec.MailNotificationConfig; cfg != nil { + emails, err := c.resolveDynamicRecipientsToEmails(cfg.DynamicRecipients, keyMap) + if err != nil { + return err + } + cfg.TargetUsers = uniqMailUsers(append(cfg.TargetUsers, buildMailUsersFromEmails(emails)...)) + } + if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { + mobiles, err := c.resolveDynamicRecipientsToMobiles(cfg.DynamicRecipients, keyMap) + if err != nil { + return err + } + cfg.AtMobiles = lo.Uniq(append(cfg.AtMobiles, mobiles...)) + } + if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { + users := c.resolveDynamicRecipientsToDirectValues(cfg.DynamicRecipients, keyMap, "user_id", "userid", "id") + cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) + } + + return nil +} + +func (c *NotificationJobCtl) resolveDynamicRecipientsToLarkUsers(recipients []*commonmodels.DynamicRecipient, appID string, keyMap map[string]string) ([]*lark.UserInfo, error) { + if len(recipients) == 0 { + return nil, nil + } + + client, err := larkservice.GetLarkClientByIMAppID(appID) + if err != nil { + return nil, err + } + + resp := make([]*lark.UserInfo, 0) + for _, recipient := range recipients { + value := renderNotificationString(recipient.Value, keyMap) + if value == "" { + continue + } + + idType, id, err := resolveLarkRecipient(client, recipient.IdentityType, value) + if err != nil { + return nil, err + } + if id == "" { + continue + } + resp = append(resp, &lark.UserInfo{ID: id, IDType: idType}) + } + + return uniqLarkUsers(resp), nil +} + +func (c *NotificationJobCtl) resolveDynamicRecipientsToEmails(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string) ([]string, error) { + resp := make([]string, 0) + for _, recipient := range recipients { + value := renderNotificationString(recipient.Value, keyMap) + if value == "" { + continue + } + + switch recipient.IdentityType { + case "", "email": + resp = append(resp, value) + case "account": + userInfo, err := searchUserByAccount(value) + if err != nil { + return nil, err + } + if userInfo != nil && userInfo.Email != "" { + resp = append(resp, userInfo.Email) + } + } + } + return lo.Uniq(resp), nil +} + +func (c *NotificationJobCtl) resolveDynamicRecipientsToMobiles(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string) ([]string, error) { + resp := make([]string, 0) + for _, recipient := range recipients { + value := renderNotificationString(recipient.Value, keyMap) + if value == "" { + continue + } + + switch recipient.IdentityType { + case "mobile": + resp = append(resp, value) + case "account": + userInfo, err := searchUserByAccount(value) + if err != nil { + return nil, err + } + if userInfo != nil && userInfo.Phone != "" { + resp = append(resp, userInfo.Phone) + } + } + } + return lo.Uniq(resp), nil +} + +func (c *NotificationJobCtl) resolveDynamicRecipientsToDirectValues(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string, supportedTypes ...string) []string { + if len(recipients) == 0 { + return nil + } + supported := make(map[string]struct{}, len(supportedTypes)) + for _, identityType := range supportedTypes { + supported[identityType] = struct{}{} + } + resp := make([]string, 0) + for _, recipient := range recipients { + if _, ok := supported[recipient.IdentityType]; !ok { + continue + } + value := renderNotificationString(recipient.Value, keyMap) + if value == "" { + continue + } + resp = append(resp, value) + } + return lo.Uniq(resp) +} + +func resolveLarkRecipient(client *lark.Client, identityType, value string) (string, string, error) { + switch identityType { + case "", "email": + userInfo, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeEmail, value, setting.LarkUserID) + if err != nil { + return "", "", err + } + return setting.LarkUserID, util2.GetStringFromPointer(userInfo.UserId), nil + case "mobile": + userInfo, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, value, setting.LarkUserID) + if err != nil { + return "", "", err + } + return setting.LarkUserID, util2.GetStringFromPointer(userInfo.UserId), nil + case "account": + userInfo, err := searchUserByAccount(value) + if err != nil { + return "", "", err + } + if userInfo == nil { + return "", "", nil + } + if userInfo.Email != "" { + larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeEmail, userInfo.Email, setting.LarkUserID) + if err == nil { + return setting.LarkUserID, util2.GetStringFromPointer(larkUser.UserId), nil + } + } + if userInfo.Phone != "" { + larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, userInfo.Phone, setting.LarkUserID) + if err == nil { + return setting.LarkUserID, util2.GetStringFromPointer(larkUser.UserId), nil + } + } + return "", "", nil + default: + return "", "", fmt.Errorf("unsupported lark dynamic recipient identity type: %s", identityType) + } +} + +func searchUserByAccount(account string) (*user.User, error) { + resp, err := user.New().SearchUser(&user.SearchUserArgs{ + Account: account, + Page: 1, + PerPage: 1, + }) + if err != nil { + return nil, err + } + if resp == nil || len(resp.Users) == 0 { + return nil, nil + } + return resp.Users[0], nil +} + +func uniqLarkUsers(users []*lark.UserInfo) []*lark.UserInfo { + seen := make(map[string]struct{}) + resp := make([]*lark.UserInfo, 0, len(users)) + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + key := user.IDType + ":" + user.ID + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + resp = append(resp, user) + } + return resp +} + +func buildMailUsersFromEmails(emails []string) []*commonmodels.User { + resp := make([]*commonmodels.User, 0, len(emails)) + for _, email := range lo.Uniq(emails) { + if email == "" { + continue + } + resp = append(resp, &commonmodels.User{ + Type: "email", + UserName: email, + }) + } + return resp +} + +func uniqMailUsers(users []*commonmodels.User) []*commonmodels.User { + seen := make(map[string]struct{}) + resp := make([]*commonmodels.User, 0, len(users)) + for _, user := range users { + if user == nil { + continue + } + key := user.Type + ":" + switch user.Type { + case "email": + key += user.UserName + case setting.UserTypeGroup: + key += user.GroupID + default: + key += user.UserID + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + resp = append(resp, user) + } + return resp +} + +func renderNotificationString(input string, keyMap map[string]string) string { + if len(keyMap) == 0 || !strings.Contains(input, "{{.") { + return input + } + pairs := make([]string, 0, len(keyMap)*2) + for key, value := range keyMap { + pairs = append(pairs, "{{."+key+"}}", value) + } + return strings.NewReplacer(pairs...).Replace(input) +} + func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDisplayName string, taskID int64, receiverType, receiverID, title, message string, idList []string, isAtAll bool) error { // first generate lark card card := instantmessage.NewLarkCard() @@ -268,6 +609,58 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis return nil } +func sendLarkHookMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error { + card := instantmessage.NewLarkCard() + card.SetConfig(true) + card.SetHeader("blue", title, "plain_text") + card.AddI18NElementsZhcnFeild(message, true) + + detailURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s", + configbase.SystemAddress(), + productName, + workflowName, + taskID, + workflowDisplayName, + ) + card.AddI18NElementsZhcnAction("点击查看更多信息", detailURL) + + messageReq := instantmessage.LarkCardReq{ + MsgType: "interactive", + Card: card, + } + if _, err := httpclient.New().Post(uri, httpclient.SetBody(messageReq)); err != nil { + return err + } + + if len(idList) == 0 && !isAtAll { + return nil + } + + atUserList := make([]string, 0, len(idList)) + idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) + for _, userID := range idList { + atUserList = append(atUserList, fmt.Sprintf("", userID)) + } + atMessage := strings.Join(atUserList, " ") + if isAtAll { + atMessage += "" + } + + if strings.Contains(uri, "bot/v2/hook") { + _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessageV2{ + MsgType: "text", + Content: instantmessage.FeiShuContentV2{Text: atMessage}, + })) + return err + } + + _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessage{ + Title: "", + Text: atMessage, + })) + return err +} + func sendDingDingMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error { processedMessage := generateDingDingNotificationMessage(title, message, idList) @@ -438,7 +831,35 @@ func sendMailMessage(title, message string, users []*commonmodels.User, callerID return err } - users, userMap := util.GeneFlatUsersWithCaller(users, callerID) + directEmailUsers := make([]*commonmodels.User, 0) + lookupUsers := make([]*commonmodels.User, 0) + for _, u := range users { + if u != nil && u.Type == "email" { + directEmailUsers = append(directEmailUsers, u) + continue + } + lookupUsers = append(lookupUsers, u) + } + + users, userMap := util.GeneFlatUsersWithCaller(lookupUsers, callerID) + for _, u := range directEmailUsers { + log.Infof("Sending Mail to email: %s", u.UserName) + err = mail.SendEmail(&mail.EmailParams{ + From: emailSvc.Address, + To: u.UserName, + Subject: title, + Host: email.Name, + UserName: email.UserName, + Password: email.Password, + Port: email.Port, + TlsSkipVerify: email.TlsSkipVerify, + Body: message, + }) + if err != nil { + log.Errorf("sendMailMessage SendEmail error, error msg:%s", err) + } + } + for _, u := range users { log.Infof("Sending Mail to user: %s", u.UserName) info, ok := userMap[u.UserID] diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go index 71446019d9..5737a84780 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go @@ -25,11 +25,6 @@ import ( "time" "github.com/google/uuid" - "go.uber.org/zap" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/rand" - config2 "github.com/koderover/zadig/v2/pkg/config" "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" @@ -39,6 +34,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/scmnotify" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowstat" + commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" "github.com/koderover/zadig/v2/pkg/setting" "github.com/koderover/zadig/v2/pkg/tool/cache" "github.com/koderover/zadig/v2/pkg/tool/clientmanager" @@ -47,6 +43,10 @@ import ( "github.com/koderover/zadig/v2/pkg/tool/kube/podexec" "github.com/koderover/zadig/v2/pkg/tool/kube/updater" "github.com/koderover/zadig/v2/pkg/tool/log" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/rand" ) const ( @@ -261,6 +261,7 @@ func (c *workflowCtl) Run(ctx context.Context, concurrency int) { WorkflowTaskCreatorUserID: c.workflowTask.TaskCreatorID, WorkflowTaskCreatorMobile: c.workflowTask.TaskCreatorPhone, WorkflowTaskCreatorEmail: c.workflowTask.TaskCreatorEmail, + WorkflowKeyVals: commonutil.BuildWorkflowRuntimeVariableKVs(c.workflowTask.WorkflowArgs, c.workflowTask.ProjectName, c.workflowTask.ProjectDisplayName, c.workflowTask.TaskID, c.workflowTask.TaskCreator, c.workflowTask.TaskCreatorAccount, c.workflowTask.TaskCreatorID, time.Unix(c.workflowTask.StartTime, 0)), Workspace: "/workspace", DistDir: fmt.Sprintf("%s/%s/dist/%d", config.S3StoragePath(), c.workflowTask.WorkflowName, c.workflowTask.TaskID), DockerMountDir: fmt.Sprintf("/tmp/%s/docker/%d", uuid.NewString(), time.Now().Unix()), diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go new file mode 100644 index 0000000000..b66f703ac0 --- /dev/null +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -0,0 +1,249 @@ +package util + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + configbase "github.com/koderover/zadig/v2/pkg/config" + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + "github.com/koderover/zadig/v2/pkg/types" +) + +func BuildPayloadVariables(rawPayload string) []*commonmodels.KeyVal { + if rawPayload == "" { + return nil + } + + var payload interface{} + if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil { + return nil + } + + resp := make([]*commonmodels.KeyVal, 0) + flattenPayloadValue("payload", payload, &resp) + return resp +} + +func flattenPayloadValue(prefix string, value interface{}, resp *[]*commonmodels.KeyVal) { + switch val := value.(type) { + case map[string]interface{}: + for key, item := range val { + flattenPayloadValue(prefix+"."+key, item, resp) + } + case []interface{}: + for index, item := range val { + flattenPayloadValue(fmt.Sprintf("%s.%d", prefix, index), item, resp) + } + case string: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: val, IsCredential: false}) + case float64: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatFloat(val, 'f', -1, 64), IsCredential: false}) + case bool: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatBool(val), IsCredential: false}) + case nil: + return + default: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: fmt.Sprint(val), IsCredential: false}) + } +} + +func RepoVariableKVs(repos []*types.Repository) []*commonmodels.KeyVal { + ret := make([]*commonmodels.KeyVal, 0) + for index, repo := range repos { + repoNameIndex := fmt.Sprintf("REPONAME_%d", index) + ret = append(ret, &commonmodels.KeyVal{Key: repoNameIndex, Value: repo.RepoName, IsCredential: false}) + + repoIndex := fmt.Sprintf("REPO_%d", index) + repoName := RepoNameToRepoIndex(repo.RepoName) + ret = append(ret, &commonmodels.KeyVal{Key: repoIndex, Value: repoName, IsCredential: false}) + + if len(repo.Branch) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_BRANCH", repoName), Value: repo.Branch, IsCredential: false}) + } + + if len(repo.Tag) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TAG", repoName), Value: repo.Tag, IsCredential: false}) + } + + if repo.PR > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strconv.Itoa(repo.PR), IsCredential: false}) + } + + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PRE_MERGE_BRANCHES", repoName), Value: repo.GetPreMergeBranches(), IsCredential: false}) + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_ORG", repoName), Value: repo.RepoOwner, IsCredential: false}) + + if len(repo.PRs) > 0 { + prStrs := []string{} + for _, pr := range repo.PRs { + prStrs = append(prStrs, strconv.Itoa(pr)) + } + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strings.Join(prStrs, ","), IsCredential: false}) + } + + if len(repo.CommitID) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_ID", repoName), Value: repo.CommitID, IsCredential: false}) + } + if len(repo.AuthorName) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_AUTHOR", repoName), Value: repo.AuthorName, IsCredential: false}) + } + if len(repo.Committer) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMITTER", repoName), Value: repo.Committer, IsCredential: false}) + } + if len(repo.CommitMessage) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_MESSAGE", repoName), Value: repo.CommitMessage, IsCredential: false}) + } + if len(repo.TargetBranch) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TARGET_BRANCH", repoName), Value: repo.TargetBranch, IsCredential: false}) + } + } + return ret +} + +func RepoNameToRepoIndex(repoName string) string { + words := map[rune]string{ + '0': "A", '1': "B", '2': "C", '3': "D", '4': "E", + '5': "F", '6': "G", '7': "H", '8': "I", '9': "J", + } + result := "" + for i, digit := range repoName { + if word, ok := words[digit]; ok { + result += word + } else { + result += repoName[i:] + break + } + } + + result = strings.ReplaceAll(result, "-", "_") + result = strings.ReplaceAll(result, ".", "_") + return result +} + +func CollectWorkflowRepos(workflow *commonmodels.WorkflowV4) []*types.Repository { + if workflow == nil { + return nil + } + + resp := make([]*types.Repository, 0) + repoKeySet := make(map[string]struct{}) + appendRepo := func(repo *types.Repository) { + if repo == nil { + return + } + key := fmt.Sprintf("%d/%s/%s/%s/%s/%d", repo.CodehostID, repo.RepoOwner, repo.RepoNamespace, repo.RepoName, repo.Branch, repo.PR) + if _, ok := repoKeySet[key]; ok { + return + } + repoKeySet[key] = struct{}{} + resp = append(resp, repo) + } + + for _, stage := range workflow.Stages { + for _, jobInfo := range stage.Jobs { + switch spec := jobInfo.Spec.(type) { + case *commonmodels.ZadigBuildJobSpec: + for _, build := range spec.ServiceAndBuilds { + for _, repo := range build.Repos { + appendRepo(repo) + } + } + case *commonmodels.ZadigTestingJobSpec: + for _, testModule := range spec.TestModules { + for _, repo := range testModule.Repos { + appendRepo(repo) + } + } + for _, serviceTest := range spec.ServiceAndTests { + for _, repo := range serviceTest.Repos { + appendRepo(repo) + } + } + case *commonmodels.ZadigScanningJobSpec: + for _, scanning := range spec.Scannings { + for _, repo := range scanning.Repos { + appendRepo(repo) + } + } + for _, serviceScanning := range spec.ServiceAndScannings { + for _, repo := range serviceScanning.Repos { + appendRepo(repo) + } + } + } + } + } + + return resp +} + +func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { + if workflow == nil { + return nil + } + + resp := []*commonmodels.KeyVal{ + {Key: "project", Value: projectName, IsCredential: false}, + {Key: "project.id", Value: projectName, IsCredential: false}, + {Key: "project.name", Value: projectDisplayName, IsCredential: false}, + {Key: "workflow.id", Value: workflow.Name, IsCredential: false}, + {Key: "workflow.name", Value: workflow.DisplayName, IsCredential: false}, + {Key: "workflow.task.id", Value: fmt.Sprintf("%d", taskID), IsCredential: false}, + {Key: "workflow.task.creator", Value: creator, IsCredential: false}, + {Key: "workflow.task.creator.id", Value: account, IsCredential: false}, + {Key: "workflow.task.creator.userId", Value: uid, IsCredential: false}, + {Key: "workflow.task.timestamp", Value: fmt.Sprintf("%d", now.Unix()), IsCredential: false}, + {Key: "workflow.task.datetime", Value: now.Format(time.DateTime), IsCredential: false}, + { + Key: "workflow.task.url", + Value: fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s", configbase.SystemAddress(), projectName, workflow.Name, taskID, url.QueryEscape(workflow.DisplayName)), + IsCredential: false, + }, + } + + for _, param := range workflow.Params { + if param == nil { + continue + } + value := param.Value + if param.ParamsType == string(commonmodels.MultiSelectType) { + value = strings.Join(param.ChoiceValue, ",") + } else if param.ParamsType == string(commonmodels.FileType) { + continue + } + resp = append(resp, &commonmodels.KeyVal{ + Key: strings.Join([]string{"workflow", "params", param.Name}, "."), + Value: value, + IsCredential: false, + }) + } + if workflow.HookPayload != nil { + resp = append(resp, BuildPayloadVariables(workflow.HookPayload.RawPayload)...) + } + + return resp +} + +func BuildWorkflowRuntimeVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { + resp := BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now) + if workflow == nil { + return resp + } + resp = append(resp, RepoVariableKVs(CollectWorkflowRepos(workflow))...) + + return resp +} + +func KeyValsToMap(kvs []*commonmodels.KeyVal) map[string]string { + resp := make(map[string]string) + for _, kv := range kvs { + if kv == nil || kv.Key == "" || kv.GetValue() == "" { + continue + } + resp[kv.Key] = kv.GetValue() + } + return resp +} diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go index 3084f186a1..f13cab28fb 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go @@ -164,6 +164,8 @@ func (gruem *gerritChangeMergedEventMatcherForWorkflowV4) GetHookRepo(hookRepo * RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -206,7 +208,9 @@ func (gpcem *gerritPatchsetCreatedEventMatcherForWorkflowV4) GetHookRepo(hookRep RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, PR: gpcem.Event.Change.Number, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -344,6 +348,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba CodehostID: item.MainRepo.CodehostID, MergeRequestID: mergeRequestID, CommitID: commitID, + RawPayload: string(body), } } workflowController := controller.CreateWorkflowController(item.WorkflowArg) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go index 418c817baa..44d445a5a6 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go @@ -116,7 +116,7 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log * wg.Add(1) go func() { defer wg.Done() - if err = TriggerWorkflowV4ByGiteeEvent(event, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGiteeEvent(event, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } }() @@ -150,7 +150,7 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log * wg.Add(1) go func() { defer wg.Done() - if err = TriggerWorkflowV4ByGiteeEvent(event, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGiteeEvent(event, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } }() @@ -176,7 +176,7 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log * wg.Add(1) go func() { defer wg.Done() - if err = TriggerWorkflowV4ByGiteeEvent(event, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGiteeEvent(event, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } }() diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go index e00e12e37d..656fbd6373 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go @@ -79,6 +79,8 @@ func (gpem *giteePushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoNamespace: hookRepo.GetRepoNamespace(), RepoOwner: hookRepo.RepoOwner, Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -125,7 +127,9 @@ func (gmem *giteeMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: gmem.event.PullRequest.Base.Ref, PR: gmem.event.PullRequest.Number, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -164,7 +168,9 @@ func (gtem *giteeTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodel RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, Tag: hookRepo.Tag, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -197,7 +203,7 @@ func createGiteeEventMatcherForWorkflowV4( return nil } -func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, log *zap.SugaredLogger) error { +func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, requestID string, log *zap.SugaredLogger) error { workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0) if err != nil { errMsg := fmt.Sprintf("list workflow v4 error: %v", err) @@ -271,6 +277,7 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, MergeRequestID: mergeRequestID, CommitID: commitID, EventType: eventType, + RawPayload: rawPayload, } case *gitee.PushEvent: eventType = EventTypePush @@ -288,11 +295,13 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, IsPr: false, CommitID: commitID, EventType: eventType, + RawPayload: rawPayload, } case *gitee.TagPushEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github.go b/pkg/microservice/aslan/core/workflow/service/webhook/github.go index 8a9ee2b9b1..360f452d43 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github.go @@ -429,19 +429,19 @@ func ProcessGithubWebHookForWorkflowV4(payload []byte, req *http.Request, reques if *et.Action != "opened" && *et.Action != "synchronize" { return nil } - err = TriggerWorkflowV4ByGithubEvent(et, baseURI, deliveryID, requestID, log) + err = TriggerWorkflowV4ByGithubEvent(et, string(payload), baseURI, deliveryID, requestID, log) if err != nil { log.Errorf("prEventToPipelineTasks error: %v", err) return e.ErrGithubWebHook.AddErr(err) } case *github.PushEvent: - err = TriggerWorkflowV4ByGithubEvent(et, baseURI, deliveryID, requestID, log) + err = TriggerWorkflowV4ByGithubEvent(et, string(payload), baseURI, deliveryID, requestID, log) if err != nil { log.Infof("pushEventToPipelineTasks error: %v", err) return e.ErrGithubWebHook.AddErr(err) } case *github.CreateEvent: - err = TriggerWorkflowV4ByGithubEvent(et, baseURI, deliveryID, requestID, log) + err = TriggerWorkflowV4ByGithubEvent(et, string(payload), baseURI, deliveryID, requestID, log) if err != nil { log.Errorf("tagEventToPipelineTasks error: %s", err) return e.ErrGithubWebHook.AddErr(err) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go index 81594ed0cc..60f192b5e5 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go @@ -79,8 +79,10 @@ func (gpem *githubPushEventMatcheForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, CommitID: *gpem.event.HeadCommit.ID, CommitMessage: *gpem.event.HeadCommit.Message, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -131,9 +133,11 @@ func (gmem *githubMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: *gmem.event.PullRequest.Base.Ref, PR: *gmem.event.PullRequest.Number, CommitID: *gmem.event.PullRequest.Head.SHA, CommitMessage: *gmem.event.PullRequest.Title, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -175,7 +179,9 @@ func (gtem *githubTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, Tag: hookRepo.Tag, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -208,7 +214,7 @@ func createGithubEventMatcherForWorkflowV4( return nil } -func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requestID string, log *zap.SugaredLogger) error { +func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deliveryID, requestID string, log *zap.SugaredLogger) error { workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0) if err != nil { errMsg := fmt.Sprintf("list workflow v4 error: %v", err) @@ -277,6 +283,7 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requ MergeRequestID: mergeRequestID, CommitID: commitID, EventType: eventType, + RawPayload: rawPayload, } case *github.PushEvent: if ev.GetRef() != "" && ev.GetHeadCommit().GetID() != "" { @@ -295,12 +302,14 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requ DeliveryID: deliveryID, CommitID: commitID, EventType: eventType, + RawPayload: rawPayload, } } case *github.CreateEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go index e733b47c72..b3963515d7 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go @@ -163,7 +163,7 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log go func() { defer wg.Done() triggerWorkflowV4Start := time.Now() - if err = TriggerWorkflowV4ByGitlabEvent(pushEvent, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGitlabEvent(pushEvent, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } log.Infof("gitlab webhook TriggerWorkflowV4ByGitlabEvent push cost %s", time.Since(triggerWorkflowV4Start)) @@ -196,7 +196,7 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log go func() { defer wg.Done() triggerWorkflowV4Start := time.Now() - if err = TriggerWorkflowV4ByGitlabEvent(mergeEvent, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGitlabEvent(mergeEvent, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } log.Infof("gitlab webhook TriggerWorkflowV4ByGitlabEvent merge cost %s", time.Since(triggerWorkflowV4Start)) @@ -229,7 +229,7 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log go func() { defer wg.Done() triggerWorkflowV4Start := time.Now() - if err = TriggerWorkflowV4ByGitlabEvent(tagEvent, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGitlabEvent(tagEvent, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } log.Infof("gitlab webhook TriggerWorkflowV4ByGitlabEvent tag cost %s", time.Since(triggerWorkflowV4Start)) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go index 652199458d..ef6cc34126 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go @@ -102,7 +102,9 @@ func (gmem *gitlabMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: gmem.event.ObjectAttributes.TargetBranch, PR: gmem.event.ObjectAttributes.IID, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -219,6 +221,8 @@ func (gpem *gitlabPushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -261,12 +265,14 @@ func (gpem *gitlabTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, Tag: hookRepo.Tag, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } -func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string, log *zap.SugaredLogger) error { +func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requestID string, log *zap.SugaredLogger) error { // TODO: cache workflow // 1. find configured workflow workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0) @@ -363,6 +369,7 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string CommitID: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, + RawPayload: rawPayload, } case *gitlab.PushEvent: eventType = EventTypePush @@ -380,11 +387,13 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string CommitID: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, + RawPayload: rawPayload, } case *gitlab.TagEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go index 4f8550be86..10b208235d 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go @@ -86,30 +86,42 @@ func (j NotificationJobController) Update(useUserInput bool, ticket *commonmodel j.jobSpec.Source = currJobSpec.Source if currJobSpec.Source == "runtime" { + if currJobSpec.LarkHookNotificationConfig != nil && j.jobSpec.LarkHookNotificationConfig != nil { + currJobSpec.LarkHookNotificationConfig.AtUsers = j.jobSpec.LarkHookNotificationConfig.AtUsers + currJobSpec.LarkHookNotificationConfig.DynamicRecipients = j.jobSpec.LarkHookNotificationConfig.DynamicRecipients + currJobSpec.LarkHookNotificationConfig.IsAtAll = j.jobSpec.LarkHookNotificationConfig.IsAtAll + } if currJobSpec.LarkGroupNotificationConfig != nil && j.jobSpec.LarkGroupNotificationConfig != nil { currJobSpec.LarkGroupNotificationConfig.AtUsers = j.jobSpec.LarkGroupNotificationConfig.AtUsers + currJobSpec.LarkGroupNotificationConfig.DynamicRecipients = j.jobSpec.LarkGroupNotificationConfig.DynamicRecipients currJobSpec.LarkGroupNotificationConfig.IsAtAll = j.jobSpec.LarkGroupNotificationConfig.IsAtAll } if currJobSpec.LarkPersonNotificationConfig != nil && j.jobSpec.LarkPersonNotificationConfig != nil { currJobSpec.LarkPersonNotificationConfig.TargetUsers = j.jobSpec.LarkPersonNotificationConfig.TargetUsers + currJobSpec.LarkPersonNotificationConfig.DynamicRecipients = j.jobSpec.LarkPersonNotificationConfig.DynamicRecipients } if currJobSpec.WechatNotificationConfig != nil && j.jobSpec.WechatNotificationConfig != nil { currJobSpec.WechatNotificationConfig.AtUsers = j.jobSpec.WechatNotificationConfig.AtUsers + currJobSpec.WechatNotificationConfig.DynamicRecipients = j.jobSpec.WechatNotificationConfig.DynamicRecipients currJobSpec.WechatNotificationConfig.IsAtAll = j.jobSpec.WechatNotificationConfig.IsAtAll } if currJobSpec.DingDingNotificationConfig != nil && j.jobSpec.DingDingNotificationConfig != nil { currJobSpec.DingDingNotificationConfig.AtMobiles = j.jobSpec.DingDingNotificationConfig.AtMobiles + currJobSpec.DingDingNotificationConfig.DynamicRecipients = j.jobSpec.DingDingNotificationConfig.DynamicRecipients currJobSpec.DingDingNotificationConfig.IsAtAll = j.jobSpec.DingDingNotificationConfig.IsAtAll } if currJobSpec.MSTeamsNotificationConfig != nil && j.jobSpec.MSTeamsNotificationConfig != nil { currJobSpec.MSTeamsNotificationConfig.AtEmails = j.jobSpec.MSTeamsNotificationConfig.AtEmails + currJobSpec.MSTeamsNotificationConfig.DynamicRecipients = j.jobSpec.MSTeamsNotificationConfig.DynamicRecipients } if currJobSpec.MailNotificationConfig != nil && j.jobSpec.MailNotificationConfig != nil { currJobSpec.MailNotificationConfig.TargetUsers = j.jobSpec.MailNotificationConfig.TargetUsers + currJobSpec.MailNotificationConfig.DynamicRecipients = j.jobSpec.MailNotificationConfig.DynamicRecipients } } // use the latest webhook settings, except for title and content + j.jobSpec.LarkHookNotificationConfig = currJobSpec.LarkHookNotificationConfig j.jobSpec.LarkGroupNotificationConfig = currJobSpec.LarkGroupNotificationConfig j.jobSpec.LarkPersonNotificationConfig = currJobSpec.LarkPersonNotificationConfig j.jobSpec.WechatNotificationConfig = currJobSpec.WechatNotificationConfig @@ -218,6 +230,7 @@ func generateNotificationJobSpec(spec *commonmodels.NotificationJobSpec) (*commo return nil, err } + resp.LarkHookNotificationConfig = spec.LarkHookNotificationConfig resp.MailNotificationConfig = spec.MailNotificationConfig resp.WechatNotificationConfig = spec.WechatNotificationConfig resp.LarkPersonNotificationConfig = spec.LarkPersonNotificationConfig diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go index a8f7d1110a..0fd259184d 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go @@ -513,64 +513,9 @@ func generateKeyValsFromWorkflowParam(params []*commonmodels.Param) []*commonmod return resp } -func repoNameToRepoIndex(repoName string) string { - words := map[rune]string{ - '0': "A", '1': "B", '2': "C", '3': "D", '4': "E", - '5': "F", '6': "G", '7': "H", '8': "I", '9': "J", - } - result := "" - for i, digit := range repoName { - if word, ok := words[digit]; ok { - result += word - } else { - result += repoName[i:] - break - } - } - - result = strings.Replace(result, "-", "_", -1) - result = strings.Replace(result, ".", "_", -1) - - return result -} - func getReposVariables(repos []*types.Repository) []*commonmodels.KeyVal { - ret := make([]*commonmodels.KeyVal, 0) - for index, repo := range repos { - repoNameIndex := fmt.Sprintf("REPONAME_%d", index) - ret = append(ret, &commonmodels.KeyVal{Key: repoNameIndex, Value: repo.RepoName, IsCredential: false}) - - repoIndex := fmt.Sprintf("REPO_%d", index) - repoName := repoNameToRepoIndex(repo.RepoName) - ret = append(ret, &commonmodels.KeyVal{Key: repoIndex, Value: repoName, IsCredential: false}) - - if len(repo.Branch) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_BRANCH", repoName), Value: repo.Branch, IsCredential: false}) - } - - if len(repo.Tag) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TAG", repoName), Value: repo.Tag, IsCredential: false}) - } - - if repo.PR > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strconv.Itoa(repo.PR), IsCredential: false}) - } - - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PRE_MERGE_BRANCHES", repoName), Value: repo.GetPreMergeBranches(), IsCredential: false}) - - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_ORG", repoName), Value: repo.RepoOwner, IsCredential: false}) - - if len(repo.PRs) > 0 { - prStrs := []string{} - for _, pr := range repo.PRs { - prStrs = append(prStrs, strconv.Itoa(pr)) - } - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strings.Join(prStrs, ","), IsCredential: false}) - } - - if len(repo.CommitID) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_ID", repoName), Value: repo.CommitID, IsCredential: false}) - } + ret := commonutil.RepoVariableKVs(repos) + for _, repo := range repos { ret = append(ret, getEnvFromCommitMsg(repo.CommitMessage)...) } return ret diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index 7d6854e97c..dad86da0b5 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -399,6 +399,16 @@ func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid } resp = append(resp, newParam) } + if w.HookPayload != nil { + for _, kv := range commonutil.BuildPayloadVariables(w.HookPayload.RawPayload) { + resp = append(resp, &commonmodels.Param{ + Name: kv.Key, + Value: kv.Value, + ParamsType: "string", + IsCredential: kv.IsCredential, + }) + } + } return resp, nil } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/types.go b/pkg/microservice/aslan/core/workflow/service/workflow/types.go index ce74991774..35f8cd5c3f 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go @@ -160,6 +160,11 @@ type CreateCustomTaskNotifyInput struct { MailNotificationConfig *CreateCustomTaskMailNotificationConfig `json:"mail_notification_config"` } +type CreateCustomTaskDynamicRecipient struct { + Value string `json:"value"` + IdentityType string `json:"identity_type"` +} + type CreateCustomTaskLarkUserInfo struct { ID string `json:"id"` // 支持 open_id、user_id @@ -171,36 +176,43 @@ type CreateCustomTaskLarkUserInfo struct { } type CreateCustomTaskLarkGroupNotificationConfig struct { - ChatID string `json:"chat_id"` - AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"` + ChatID string `json:"chat_id"` + AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` } type CreateCustomTaskLarkPersonNotificationConfig struct { - Users []CreateCustomTaskLarkUserInfo `json:"users"` + Users []CreateCustomTaskLarkUserInfo `json:"users"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` } type CreateCustomTaskLarkHookNotificationConfig struct { - AtUsers []string `json:"at_users"` - IsAtAll bool `json:"is_at_all"` + AtUsers []string `json:"at_users"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskWechatNotificationConfig struct { - AtUsers []string `json:"at_users"` - IsAtAll bool `json:"is_at_all"` + AtUsers []string `json:"at_users"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskDingDingNotificationConfig struct { - AtMobiles []string `json:"at_mobiles"` - IsAtAll bool `json:"is_at_all"` + AtMobiles []string `json:"at_mobiles"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskMSTeamsNotificationConfig struct { - AtEmails []string `json:"at_emails"` + AtEmails []string `json:"at_emails"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` } type CreateCustomTaskMailNotificationConfig struct { - UserIDs []string `json:"user_ids"` - Users []*commonmodels.User `json:"users"` + UserIDs []string `json:"user_ids"` + Users []*commonmodels.User `json:"users"` + DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` } type CreateCustomTaskParam struct { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index 5dae8fa0df..df544834c2 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -708,6 +708,7 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels log.Errorf("fill serviceModules to jobs error: %v", err) return resp, e.ErrCreateTask.AddDesc(err.Error()) } + workflowTask.GlobalContext = buildWorkflowTaskRuntimeContext(workflowTask) if err := instantmessage.NewWeChatClient().SendWorkflowTaskNotifications(workflowTask); err != nil { log.Errorf("send workflow task notification failed, error: %v", err) @@ -737,6 +738,20 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea notifyInputsMap[notifyInput.ID] = notifyInput } + toDynamicRecipients := func(inputs []CreateCustomTaskDynamicRecipient) []*commonmodels.DynamicRecipient { + resp := make([]*commonmodels.DynamicRecipient, 0, len(inputs)) + for _, input := range inputs { + if input.Value == "" { + continue + } + resp = append(resp, &commonmodels.DynamicRecipient{ + Value: input.Value, + IdentityType: input.IdentityType, + }) + } + return resp + } + for i, notifyCtl := range notifyCtls { notifyInput, ok := notifyInputsMap[i] if ok && notifyCtl.WebHookType == notifyInput.Type { @@ -752,9 +767,10 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.LarkHookNotificationConfig{ - HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress, - AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers, - IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll, + HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress, + AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers, + DynamicRecipients: toDynamicRecipients(notifyInput.LarkHookNotificationConfig.DynamicRecipients), + IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll, } notifyCtl.LarkHookNotificationConfig = config @@ -765,7 +781,8 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.LarkPersonNotificationConfig{ - AppID: notifyCtl.LarkPersonNotificationConfig.AppID, + AppID: notifyCtl.LarkPersonNotificationConfig.AppID, + DynamicRecipients: toDynamicRecipients(notifyInput.LarkPersonNotificationConfig.DynamicRecipients), } targetUsers := make([]*larktool.UserInfo, 0) @@ -787,7 +804,8 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.LarkGroupNotificationConfig{ - AppID: notifyCtl.LarkGroupNotificationConfig.AppID, + AppID: notifyCtl.LarkGroupNotificationConfig.AppID, + DynamicRecipients: toDynamicRecipients(notifyInput.LarkGroupNotificationConfig.DynamicRecipients), Chat: &commonmodels.LarkChat{ ChatID: notifyInput.LarkGroupNotificationConfig.ChatID, }, @@ -810,9 +828,10 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.WechatNotificationConfig{ - HookAddress: notifyCtl.WechatNotificationConfig.HookAddress, - AtUsers: notifyInput.WechatNotificationConfig.AtUsers, - IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll, + HookAddress: notifyCtl.WechatNotificationConfig.HookAddress, + AtUsers: notifyInput.WechatNotificationConfig.AtUsers, + DynamicRecipients: toDynamicRecipients(notifyInput.WechatNotificationConfig.DynamicRecipients), + IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll, } notifyCtl.WechatNotificationConfig = config @@ -823,9 +842,10 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.DingDingNotificationConfig{ - HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress, - AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles, - IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll, + HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress, + AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles, + DynamicRecipients: toDynamicRecipients(notifyInput.DingDingNotificationConfig.DynamicRecipients), + IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll, } notifyCtl.DingDingNotificationConfig = config @@ -836,8 +856,9 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.MSTeamsNotificationConfig{ - HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress, - AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails, + HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress, + AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails, + DynamicRecipients: toDynamicRecipients(notifyInput.MSTeamsNotificationConfig.DynamicRecipients), } notifyCtl.MSTeamsNotificationConfig = config @@ -848,7 +869,8 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } config := &commonmodels.MailNotificationConfig{ - TargetUsers: make([]*commonmodels.User, 0), + TargetUsers: make([]*commonmodels.User, 0), + DynamicRecipients: toDynamicRecipients(notifyInput.MailNotificationConfig.DynamicRecipients), } if len(notifyInput.MailNotificationConfig.Users) > 0 { @@ -882,6 +904,34 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea return notifyCtls } +func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string]string { + if task == nil || task.WorkflowArgs == nil { + return nil + } + + keyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + task.WorkflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) + + resp := make(map[string]string, len(keyMap)) + for key, value := range keyMap { + // Payload variables are resolved at task creation time and stored in RawPayload; + // they don't need to be persisted in GlobalContext (which would duplicate them in MongoDB). + if strings.HasPrefix(key, "payload.") { + continue + } + resp[runtimeWorkflowController.GetContextKey(fmt.Sprintf("{{.%s}}", key))] = value + } + return resp +} + func GetManualExecWorkflowTaskV4Info(workflowName string, taskID int64, logger *zap.SugaredLogger) (*commonmodels.WorkflowV4, error) { originWorkflow, err := commonrepo.NewWorkflowV4Coll().Find(workflowName) if err != nil { @@ -959,7 +1009,16 @@ func RetryWorkflowTaskV4(workflowName string, taskID int64, logger *zap.SugaredL task.RetryNum++ - globalKeyMap := make(map[string]string) + globalKeyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + task.WorkflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) jobTaskMap := make(map[string]*commonmodels.JobTask) for _, stage := range task.WorkflowArgs.Stages { for _, job := range stage.Jobs { @@ -1011,6 +1070,7 @@ func RetryWorkflowTaskV4(workflowName string, taskID int64, logger *zap.SugaredL globalKeyMap[key] = item.Value } } + task.GlobalContext = buildWorkflowTaskRuntimeContext(task) for _, stage := range task.Stages { if stage.Status == config.StatusPassed || stage.Status == config.StatusSkipped { @@ -1092,7 +1152,16 @@ func ManualExecWorkflowTaskV4(workflowName string, taskID int64, stageName strin return e.ErrCreateTask.AddErr(fmt.Errorf("save original jobs error: %v", err)) } - globalKeyMap := make(map[string]string) + globalKeyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + task.WorkflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) for _, stage := range task.WorkflowArgs.Stages { if stage.Name == stageName { @@ -1165,6 +1234,7 @@ func ManualExecWorkflowTaskV4(workflowName string, taskID int64, stageName strin globalKeyMap[key] = item.Value } } + task.GlobalContext = buildWorkflowTaskRuntimeContext(task) for _, stage := range task.OriginWorkflowArgs.Stages { if stage.Name == stageName { diff --git a/pkg/types/repo.go b/pkg/types/repo.go index 794451353c..8d27a32efe 100644 --- a/pkg/types/repo.go +++ b/pkg/types/repo.go @@ -49,10 +49,12 @@ type Repository struct { IsPrimary bool `bson:"is_primary" json:"is_primary" yaml:"is_primary"` CodehostID int `bson:"codehost_id" json:"codehost_id" yaml:"codehost_id"` // add - OauthToken string `bson:"oauth_token" json:"oauth_token" yaml:"oauth_token"` - Address string `bson:"address" json:"address" yaml:"address"` - AuthorName string `bson:"author_name,omitempty" json:"author_name,omitempty" yaml:"author_name,omitempty"` - CheckoutRef string `bson:"checkout_ref,omitempty" json:"checkout_ref,omitempty" yaml:"checkout_ref,omitempty"` + OauthToken string `bson:"oauth_token" json:"oauth_token" yaml:"oauth_token"` + Address string `bson:"address" json:"address" yaml:"address"` + AuthorName string `bson:"author_name,omitempty" json:"author_name,omitempty" yaml:"author_name,omitempty"` + Committer string `bson:"committer,omitempty" json:"committer,omitempty" yaml:"committer,omitempty"` + TargetBranch string `bson:"target_branch,omitempty" json:"target_branch,omitempty" yaml:"target_branch,omitempty"` + CheckoutRef string `bson:"checkout_ref,omitempty" json:"checkout_ref,omitempty" yaml:"checkout_ref,omitempty"` // username/password authorization for git/perforce Username string `bson:"username,omitempty" json:"username,omitempty" yaml:"username,omitempty"` Password string `bson:"password,omitempty" json:"password,omitempty" yaml:"password,omitempty"` From ec35e664a2094c32015958d8e6e7d0621aae636e Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Mon, 11 May 2026 09:55:52 +0800 Subject: [PATCH 02/22] fix(workflow): preserve runtime context job outputs Signed-off-by: huanghongbo-hhb --- .../workflow/service/workflow/workflow_task_v4.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index df544834c2..8d4e55f064 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -905,10 +905,19 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string]string { - if task == nil || task.WorkflowArgs == nil { + if task == nil { return nil } + resp := make(map[string]string) + for key, value := range task.GlobalContext { + resp[key] = value + } + + if task.WorkflowArgs == nil { + return resp + } + keyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( task.WorkflowArgs, task.ProjectName, @@ -920,7 +929,6 @@ func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string time.Unix(task.StartTime, 0), )) - resp := make(map[string]string, len(keyMap)) for key, value := range keyMap { // Payload variables are resolved at task creation time and stored in RawPayload; // they don't need to be persisted in GlobalContext (which would duplicate them in MongoDB). From ab09ea37c93a99b90a68d98934b5317a156af1db Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Mon, 11 May 2026 15:21:51 +0800 Subject: [PATCH 03/22] fix(workflow): preserve webhook repo context fields Signed-off-by: huanghongbo-hhb --- .../core/workflow/service/workflow/controller/job/utils.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go index 0fd259184d..1b865f8541 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go @@ -248,6 +248,10 @@ func applyRepos(base, input []*types.Repository) []*types.Repository { item.FilterRegexp = cv.FilterRegexp item.CommitID = cv.CommitID item.CommitMessage = cv.CommitMessage + item.AuthorName = cv.AuthorName + item.Committer = cv.Committer + item.TargetBranch = cv.TargetBranch + item.CheckoutRef = cv.CheckoutRef item.SSHKey = cv.SSHKey item.PrivateAccessToken = cv.PrivateAccessToken From 2596a28958fd367ff66f7d01753dab321bd6f133 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Mon, 11 May 2026 17:47:20 +0800 Subject: [PATCH 04/22] refactor(notification): reuse key map and lark at builder Signed-off-by: huanghongbo-hhb --- .../jobcontroller/job_notification.go | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index 5c42464f8a..f24e7bd455 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -246,19 +246,7 @@ func (c *NotificationJobCtl) prepareRuntimeNotificationFields() error { } func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string { - keyMap := make(map[string]string) - - insertKVs := func(kvs []*commonmodels.KeyVal) { - for _, kv := range kvs { - if kv == nil || kv.Key == "" || kv.GetValue() == "" { - continue - } - keyMap[kv.Key] = kv.GetValue() - } - } - - insertKVs(c.workflowCtx.WorkflowKeyVals) - return keyMap + return util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) } func renderNotificationStrings(inputs []string, keyMap map[string]string) []string { @@ -537,6 +525,19 @@ func uniqMailUsers(users []*commonmodels.User) []*commonmodels.User { return resp } +func buildLarkAtMessage(idList []string, isAtAll bool) string { + idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) + atUserList := make([]string, 0, len(idList)) + for _, userID := range idList { + atUserList = append(atUserList, fmt.Sprintf("", userID)) + } + atMessage := strings.Join(atUserList, " ") + if isAtAll { + atMessage += "" + } + return atMessage +} + func renderNotificationString(input string, keyMap map[string]string) string { if len(keyMap) == 0 || !strings.Contains(input, "{{.") { return input @@ -581,18 +582,8 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis // then send @ message if len(idList) > 0 || isAtAll { - atUserList := []string{} - idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) - for _, userID := range idList { - atUserList = append(atUserList, fmt.Sprintf("", userID)) - } - atMessage := strings.Join(atUserList, " ") - if isAtAll { - atMessage += "" - } - larkAtMessage := &instantmessage.FeiShuMessage{ - Text: atMessage, + Text: buildLarkAtMessage(idList, isAtAll), } atMessageContent, err := json.Marshal(larkAtMessage) @@ -636,15 +627,7 @@ func sendLarkHookMessage(productName, workflowName, workflowDisplayName string, return nil } - atUserList := make([]string, 0, len(idList)) - idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) - for _, userID := range idList { - atUserList = append(atUserList, fmt.Sprintf("", userID)) - } - atMessage := strings.Join(atUserList, " ") - if isAtAll { - atMessage += "" - } + atMessage := buildLarkAtMessage(idList, isAtAll) if strings.Contains(uri, "bot/v2/hook") { _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessageV2{ From 9501566df2b95d953cb9bce2c486a29ffc3d03a2 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Mon, 11 May 2026 18:25:57 +0800 Subject: [PATCH 05/22] refactor(notification): reuse feishu sender and escape urls Signed-off-by: huanghongbo-hhb --- .../common/service/instantmessage/lark.go | 8 ++++++ .../jobcontroller/job_notification.go | 26 ++++--------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/lark.go b/pkg/microservice/aslan/core/common/service/instantmessage/lark.go index 0d39b9a447..0e8f733f67 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/lark.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/lark.go @@ -198,6 +198,10 @@ func (w *Service) sendFeishuMessage(uri string, lcMsg *LarkCard) error { return err } +func (w *Service) SendFeishuHookCard(uri string, lcMsg *LarkCard) error { + return w.sendFeishuMessage(uri, lcMsg) +} + func (w *Service) sendFeishuMessageFromClient(client *lark.Client, receiverType, receiverID, messageType, messageBody string) error { err := client.SendMessage(receiverType, messageType, receiverID, messageBody) @@ -229,6 +233,10 @@ func (w *Service) sendFeishuMessageOfSingleType(title, uri, content string) erro return err } +func (w *Service) SendFeishuHookText(uri, content string) error { + return w.sendFeishuMessageOfSingleType("", uri, content) +} + func getColorTemplateWithStatus(status config.Status) string { if status == config.StatusPassed || status == config.StatusCreated { return feishuHeaderTemplateGreen diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index f24e7bd455..144db954ec 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -565,7 +565,7 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis productName, workflowName, taskID, - workflowDisplayName, + url.QueryEscape(workflowDisplayName), ) card.AddI18NElementsZhcnAction("点击查看更多信息", url) @@ -611,15 +611,12 @@ func sendLarkHookMessage(productName, workflowName, workflowDisplayName string, productName, workflowName, taskID, - workflowDisplayName, + url.QueryEscape(workflowDisplayName), ) card.AddI18NElementsZhcnAction("点击查看更多信息", detailURL) - messageReq := instantmessage.LarkCardReq{ - MsgType: "interactive", - Card: card, - } - if _, err := httpclient.New().Post(uri, httpclient.SetBody(messageReq)); err != nil { + imService := instantmessage.NewWeChatClient() + if err := imService.SendFeishuHookCard(uri, card); err != nil { return err } @@ -628,20 +625,7 @@ func sendLarkHookMessage(productName, workflowName, workflowDisplayName string, } atMessage := buildLarkAtMessage(idList, isAtAll) - - if strings.Contains(uri, "bot/v2/hook") { - _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessageV2{ - MsgType: "text", - Content: instantmessage.FeiShuContentV2{Text: atMessage}, - })) - return err - } - - _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessage{ - Title: "", - Text: atMessage, - })) - return err + return imService.SendFeishuHookText(uri, atMessage) } func sendDingDingMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error { From 91454da7f2285f5868ad8bf4bc3ac2cde921f59c Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 12 May 2026 11:40:30 +0800 Subject: [PATCH 06/22] fix(workflow): preserve webhook repo fields across scm providers Signed-off-by: huanghongbo-hhb --- .../workflow/service/webhook/gerrit_workflowv4_task.go | 4 ++++ .../workflow/service/webhook/gitee_workflowv4_task.go | 8 ++++++++ .../workflow/service/webhook/gitlab_workflowv4_task.go | 8 ++++++++ .../workflow/service/workflow/controller/job/job_build.go | 4 ++++ 4 files changed, 24 insertions(+) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go index f13cab28fb..f16aa4464f 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go @@ -165,6 +165,8 @@ func (gruem *gerritChangeMergedEventMatcherForWorkflowV4) GetHookRepo(hookRepo * RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, TargetBranch: hookRepo.Branch, + CommitID: gruem.Event.NewRev, + CommitMessage: gruem.Event.Change.CommitMessage, Committer: hookRepo.Committer, Source: hookRepo.Source, } @@ -210,6 +212,8 @@ func (gpcem *gerritPatchsetCreatedEventMatcherForWorkflowV4) GetHookRepo(hookRep Branch: hookRepo.Branch, TargetBranch: hookRepo.Branch, PR: gpcem.Event.Change.Number, + CommitID: gpcem.Event.PatchSet.Revision, + CommitMessage: gpcem.Event.Change.CommitMessage, Committer: hookRepo.Committer, Source: hookRepo.Source, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go index 656fbd6373..fcc288f0fd 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go @@ -73,6 +73,10 @@ func (gpem *giteePushEventMatcherForWorkflowV4) Match(hookRepo *commonmodels.Mai } func (gpem *giteePushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodels.MainHookRepo) *types.Repository { + commitMessage := "" + if len(gpem.event.Commits) > 0 { + commitMessage = gpem.event.Commits[len(gpem.event.Commits)-1].Message + } return &types.Repository{ CodehostID: hookRepo.CodehostID, RepoName: hookRepo.RepoName, @@ -80,6 +84,8 @@ func (gpem *giteePushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, Branch: hookRepo.Branch, TargetBranch: hookRepo.Branch, + CommitID: gpem.event.After, + CommitMessage: commitMessage, Committer: hookRepo.Committer, Source: hookRepo.Source, } @@ -129,6 +135,8 @@ func (gmem *giteeMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod Branch: hookRepo.Branch, TargetBranch: gmem.event.PullRequest.Base.Ref, PR: gmem.event.PullRequest.Number, + CommitID: gmem.event.PullRequest.Head.Sha, + CommitMessage: gmem.event.PullRequest.Title, Committer: hookRepo.Committer, Source: hookRepo.Source, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go index ef6cc34126..90c33a2e1b 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go @@ -104,6 +104,8 @@ func (gmem *gitlabMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo Branch: hookRepo.Branch, TargetBranch: gmem.event.ObjectAttributes.TargetBranch, PR: gmem.event.ObjectAttributes.IID, + CommitID: gmem.event.ObjectAttributes.LastCommit.ID, + CommitMessage: gmem.event.ObjectAttributes.Title, Committer: hookRepo.Committer, Source: hookRepo.Source, } @@ -215,6 +217,10 @@ func (gpem *gitlabPushEventMatcherForWorkflowV4) Match(hookRepo *commonmodels.Ma } func (gpem *gitlabPushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodels.MainHookRepo) *types.Repository { + commitMessage := "" + if len(gpem.event.Commits) > 0 { + commitMessage = gpem.event.Commits[len(gpem.event.Commits)-1].Message + } return &types.Repository{ CodehostID: hookRepo.CodehostID, RepoName: hookRepo.RepoName, @@ -222,6 +228,8 @@ func (gpem *gitlabPushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, TargetBranch: hookRepo.Branch, + CommitID: gpem.event.After, + CommitMessage: commitMessage, Committer: hookRepo.Committer, Source: hookRepo.Source, } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go index 05bba925a4..2a94d9090e 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go @@ -1131,6 +1131,10 @@ func mergeRepos(targetRepos, sourceRepos []*types.Repository) []*types.Repositor targetRepo.PRs = sourceRepo.PRs targetRepo.CommitID = sourceRepo.CommitID targetRepo.CommitMessage = sourceRepo.CommitMessage + targetRepo.AuthorName = sourceRepo.AuthorName + targetRepo.Committer = sourceRepo.Committer + targetRepo.TargetBranch = sourceRepo.TargetBranch + targetRepo.CheckoutRef = sourceRepo.CheckoutRef } else { // Add new repo from source repos targetRepos = append(targetRepos, sourceRepo) From 394b634a67808b8897f8ea877d5768cb1abd5ab8 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Fri, 5 Jun 2026 10:48:14 +0800 Subject: [PATCH 07/22] fix(workflow): keep only payload runtime variables Signed-off-by: huanghongbo-hhb --- .../core/common/util/workflow_variables.go | 138 +----------------- .../service/workflow/controller/job/utils.go | 59 +++++++- 2 files changed, 58 insertions(+), 139 deletions(-) diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go index b66f703ac0..ff22a350ff 100644 --- a/pkg/microservice/aslan/core/common/util/workflow_variables.go +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -10,7 +10,6 @@ import ( configbase "github.com/koderover/zadig/v2/pkg/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" - "github.com/koderover/zadig/v2/pkg/types" ) func BuildPayloadVariables(rawPayload string) []*commonmodels.KeyVal { @@ -51,135 +50,6 @@ func flattenPayloadValue(prefix string, value interface{}, resp *[]*commonmodels } } -func RepoVariableKVs(repos []*types.Repository) []*commonmodels.KeyVal { - ret := make([]*commonmodels.KeyVal, 0) - for index, repo := range repos { - repoNameIndex := fmt.Sprintf("REPONAME_%d", index) - ret = append(ret, &commonmodels.KeyVal{Key: repoNameIndex, Value: repo.RepoName, IsCredential: false}) - - repoIndex := fmt.Sprintf("REPO_%d", index) - repoName := RepoNameToRepoIndex(repo.RepoName) - ret = append(ret, &commonmodels.KeyVal{Key: repoIndex, Value: repoName, IsCredential: false}) - - if len(repo.Branch) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_BRANCH", repoName), Value: repo.Branch, IsCredential: false}) - } - - if len(repo.Tag) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TAG", repoName), Value: repo.Tag, IsCredential: false}) - } - - if repo.PR > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strconv.Itoa(repo.PR), IsCredential: false}) - } - - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PRE_MERGE_BRANCHES", repoName), Value: repo.GetPreMergeBranches(), IsCredential: false}) - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_ORG", repoName), Value: repo.RepoOwner, IsCredential: false}) - - if len(repo.PRs) > 0 { - prStrs := []string{} - for _, pr := range repo.PRs { - prStrs = append(prStrs, strconv.Itoa(pr)) - } - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strings.Join(prStrs, ","), IsCredential: false}) - } - - if len(repo.CommitID) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_ID", repoName), Value: repo.CommitID, IsCredential: false}) - } - if len(repo.AuthorName) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_AUTHOR", repoName), Value: repo.AuthorName, IsCredential: false}) - } - if len(repo.Committer) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMITTER", repoName), Value: repo.Committer, IsCredential: false}) - } - if len(repo.CommitMessage) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_MESSAGE", repoName), Value: repo.CommitMessage, IsCredential: false}) - } - if len(repo.TargetBranch) > 0 { - ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TARGET_BRANCH", repoName), Value: repo.TargetBranch, IsCredential: false}) - } - } - return ret -} - -func RepoNameToRepoIndex(repoName string) string { - words := map[rune]string{ - '0': "A", '1': "B", '2': "C", '3': "D", '4': "E", - '5': "F", '6': "G", '7': "H", '8': "I", '9': "J", - } - result := "" - for i, digit := range repoName { - if word, ok := words[digit]; ok { - result += word - } else { - result += repoName[i:] - break - } - } - - result = strings.ReplaceAll(result, "-", "_") - result = strings.ReplaceAll(result, ".", "_") - return result -} - -func CollectWorkflowRepos(workflow *commonmodels.WorkflowV4) []*types.Repository { - if workflow == nil { - return nil - } - - resp := make([]*types.Repository, 0) - repoKeySet := make(map[string]struct{}) - appendRepo := func(repo *types.Repository) { - if repo == nil { - return - } - key := fmt.Sprintf("%d/%s/%s/%s/%s/%d", repo.CodehostID, repo.RepoOwner, repo.RepoNamespace, repo.RepoName, repo.Branch, repo.PR) - if _, ok := repoKeySet[key]; ok { - return - } - repoKeySet[key] = struct{}{} - resp = append(resp, repo) - } - - for _, stage := range workflow.Stages { - for _, jobInfo := range stage.Jobs { - switch spec := jobInfo.Spec.(type) { - case *commonmodels.ZadigBuildJobSpec: - for _, build := range spec.ServiceAndBuilds { - for _, repo := range build.Repos { - appendRepo(repo) - } - } - case *commonmodels.ZadigTestingJobSpec: - for _, testModule := range spec.TestModules { - for _, repo := range testModule.Repos { - appendRepo(repo) - } - } - for _, serviceTest := range spec.ServiceAndTests { - for _, repo := range serviceTest.Repos { - appendRepo(repo) - } - } - case *commonmodels.ZadigScanningJobSpec: - for _, scanning := range spec.Scannings { - for _, repo := range scanning.Repos { - appendRepo(repo) - } - } - for _, serviceScanning := range spec.ServiceAndScannings { - for _, repo := range serviceScanning.Repos { - appendRepo(repo) - } - } - } - } - } - - return resp -} - func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { if workflow == nil { return nil @@ -228,13 +98,7 @@ func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectNa } func BuildWorkflowRuntimeVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { - resp := BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now) - if workflow == nil { - return resp - } - resp = append(resp, RepoVariableKVs(CollectWorkflowRepos(workflow))...) - - return resp + return BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now) } func KeyValsToMap(kvs []*commonmodels.KeyVal) map[string]string { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go index 1b865f8541..0ab13488b2 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go @@ -517,9 +517,64 @@ func generateKeyValsFromWorkflowParam(params []*commonmodels.Param) []*commonmod return resp } +func repoNameToRepoIndex(repoName string) string { + words := map[rune]string{ + '0': "A", '1': "B", '2': "C", '3': "D", '4': "E", + '5': "F", '6': "G", '7': "H", '8': "I", '9': "J", + } + result := "" + for i, digit := range repoName { + if word, ok := words[digit]; ok { + result += word + } else { + result += repoName[i:] + break + } + } + + result = strings.Replace(result, "-", "_", -1) + result = strings.Replace(result, ".", "_", -1) + + return result +} + func getReposVariables(repos []*types.Repository) []*commonmodels.KeyVal { - ret := commonutil.RepoVariableKVs(repos) - for _, repo := range repos { + ret := make([]*commonmodels.KeyVal, 0) + for index, repo := range repos { + repoNameIndex := fmt.Sprintf("REPONAME_%d", index) + ret = append(ret, &commonmodels.KeyVal{Key: repoNameIndex, Value: repo.RepoName, IsCredential: false}) + + repoIndex := fmt.Sprintf("REPO_%d", index) + repoName := repoNameToRepoIndex(repo.RepoName) + ret = append(ret, &commonmodels.KeyVal{Key: repoIndex, Value: repoName, IsCredential: false}) + + if len(repo.Branch) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_BRANCH", repoName), Value: repo.Branch, IsCredential: false}) + } + + if len(repo.Tag) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TAG", repoName), Value: repo.Tag, IsCredential: false}) + } + + if repo.PR > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strconv.Itoa(repo.PR), IsCredential: false}) + } + + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PRE_MERGE_BRANCHES", repoName), Value: repo.GetPreMergeBranches(), IsCredential: false}) + + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_ORG", repoName), Value: repo.RepoOwner, IsCredential: false}) + + if len(repo.PRs) > 0 { + prStrs := []string{} + for _, pr := range repo.PRs { + prStrs = append(prStrs, strconv.Itoa(pr)) + } + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strings.Join(prStrs, ","), IsCredential: false}) + } + + if len(repo.CommitID) > 0 { + ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_ID", repoName), Value: repo.CommitID, IsCredential: false}) + } ret = append(ret, getEnvFromCommitMsg(repo.CommitMessage)...) } return ret From d8a006fdadb851f60291cc1f55d445b0bf430760 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 16 Jun 2026 11:38:28 +0800 Subject: [PATCH 08/22] fix dynamic notification recipients Signed-off-by: huanghongbo-hhb --- pkg/cli/upgradeassistant/cmd/migrate/430.go | 35 +- .../internal/repository/models/migration.go | 1 + .../repository/models/dynamic_recipient.go | 63 ++ .../common/repository/models/workflow_v4.go | 55 +- .../jobcontroller/job_notification.go | 177 +----- .../job_notification_dynamic_recipient.go | 567 ++++++++++++++++++ .../core/workflow/service/workflow/types.go | 43 +- .../service/workflow/workflow_task_v4.go | 68 ++- .../user/core/handler/user/user.go | 8 + pkg/microservice/user/core/init/dm_mysql.sql | 2 + pkg/microservice/user/core/init/mysql.sql | 2 + .../user/core/repository/models/user.go | 4 +- .../user/core/repository/orm/user.go | 27 + .../user/core/service/permission/user.go | 159 ++--- pkg/shared/client/user/user.go | 2 + 15 files changed, 895 insertions(+), 318 deletions(-) create mode 100644 pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go create mode 100644 pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go diff --git a/pkg/cli/upgradeassistant/cmd/migrate/430.go b/pkg/cli/upgradeassistant/cmd/migrate/430.go index 99cbb550be..142298fb3f 100644 --- a/pkg/cli/upgradeassistant/cmd/migrate/430.go +++ b/pkg/cli/upgradeassistant/cmd/migrate/430.go @@ -117,15 +117,21 @@ func V421ToV430() error { updateMigrationError(migrationInfo.ID, err) }() - // 这次迁移分三段: + // 这次迁移分四段: // 1. MySQL: user 表新增 api_token_enabled - // 2. MySQL: permission action + role/template 绑定 - // 3. Mongo: collaboration mode / instance verbs + // 2. MySQL: user 表新增 email/phone 索引 + // 3. MySQL: permission action + role/template 绑定 + // 4. Mongo: collaboration mode / instance verbs err = migrateUserAPITokenEnabledColumn(ctx, migrationInfo) if err != nil { return err } + err = migrateUserContactIndexes(migrationInfo) + if err != nil { + return err + } + err = migrateGlobalReadOnlyRole(ctx, migrationInfo) if err != nil { return err @@ -144,6 +150,29 @@ func V421ToV430() error { return nil } +// migrateUserContactIndexes adds indexes for dynamic notification recipient lookups. +func migrateUserContactIndexes(migrationInfo *internalmodels.Migration) error { + if !migrationInfo.Migration430UserContactIndexes { + if !repository.DB.Migrator().HasIndex(&usermodels.User{}, "idx_email") { + if err := repository.DB.Migrator().CreateIndex(&usermodels.User{}, "idx_email"); err != nil { + return fmt.Errorf("failed to add idx_email index for user table, err: %s", err) + } + } + + if !repository.DB.Migrator().HasIndex(&usermodels.User{}, "idx_phone") { + if err := repository.DB.Migrator().CreateIndex(&usermodels.User{}, "idx_phone"); err != nil { + return fmt.Errorf("failed to add idx_phone index for user table, err: %s", err) + } + } + } + + _ = internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{ + getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430UserContactIndexes): true, + }) + + return nil +} + // migrateUserAPITokenEnabledColumn adds api_token_enabled column for user table. func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo *internalmodels.Migration) error { if !migrationInfo.Migration430UserAPITokenEnabled { diff --git a/pkg/cli/upgradeassistant/internal/repository/models/migration.go b/pkg/cli/upgradeassistant/internal/repository/models/migration.go index 9cb27d1062..a8e675e7e3 100644 --- a/pkg/cli/upgradeassistant/internal/repository/models/migration.go +++ b/pkg/cli/upgradeassistant/internal/repository/models/migration.go @@ -41,6 +41,7 @@ type Migration struct { Migration421CollaborationRollbackPermission bool `bson:"migration_421_collaboration_rollback_permission"` Migration421WorkflowDeploySpec bool `bson:"migration_421_workflow_deploy_spec"` Migration430UserAPITokenEnabled bool `bson:"migration_430_user_api_token_enabled"` + Migration430UserContactIndexes bool `bson:"migration_430_user_contact_indexes"` Migration430GlobalReadOnlyRole bool `bson:"migration_430_global_read_only_role"` Migration430ScalePermission bool `bson:"migration_430_scale_permission"` Migration430CollaborationScalePermission bool `bson:"migration_430_collaboration_scale_permission"` diff --git a/pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go b/pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go new file mode 100644 index 0000000000..bf68712ce7 --- /dev/null +++ b/pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go @@ -0,0 +1,63 @@ +package models + +import ( + "encoding/json" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// DynamicRecipients keeps compatibility with the temporary PR schema +// [{"value":"{{.payload.user.email}}","identity_type":"email"}]. +type DynamicRecipients []string + +type legacyDynamicRecipient struct { + Value string `bson:"value" json:"value"` + IdentityType string `bson:"identity_type" json:"identity_type"` +} + +func (r *DynamicRecipients) UnmarshalJSON(data []byte) error { + var recipients []string + if err := json.Unmarshal(data, &recipients); err == nil { + *r = recipients + return nil + } + + var legacyRecipients []legacyDynamicRecipient + if err := json.Unmarshal(data, &legacyRecipients); err != nil { + return fmt.Errorf("failed to unmarshal dynamic recipients: %w", err) + } + + *r = legacyDynamicRecipientsToStrings(legacyRecipients) + return nil +} + +func (r *DynamicRecipients) UnmarshalBSONValue(t bsontype.Type, data []byte) error { + if t == bsontype.Null || t == bsontype.Undefined { + *r = nil + return nil + } + + var recipients []string + if err := (bson.RawValue{Type: t, Value: data}).Unmarshal(&recipients); err == nil { + *r = recipients + return nil + } + + var legacyRecipients []legacyDynamicRecipient + if err := (bson.RawValue{Type: t, Value: data}).Unmarshal(&legacyRecipients); err != nil { + return fmt.Errorf("failed to unmarshal dynamic recipients: %w", err) + } + + *r = legacyDynamicRecipientsToStrings(legacyRecipients) + return nil +} + +func legacyDynamicRecipientsToStrings(recipients []legacyDynamicRecipient) []string { + resp := make([]string, 0, len(recipients)) + for _, recipient := range recipients { + resp = append(resp, recipient.Value) + } + return resp +} diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go index 08b4147927..a946ada75a 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1274,56 +1274,51 @@ func (n *NotificationJobSpec) GenerateNewNotifyConfigWithOldData() error { return nil } -type DynamicRecipient struct { - Value string `bson:"value" json:"value" yaml:"value"` - IdentityType string `bson:"identity_type" json:"identity_type" yaml:"identity_type"` -} - // TODO: why is_at_all? it could be done in backend type LarkGroupNotificationConfig struct { - AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` - Chat *LarkChat `bson:"chat" json:"chat" yaml:"chat"` - AtUsers []*lark.UserInfo `bson:"at_users" json:"at_users" yaml:"at_users"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + Chat *LarkChat `bson:"chat" json:"chat" yaml:"chat"` + AtUsers []*lark.UserInfo `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type LarkPersonNotificationConfig struct { - AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` - TargetUsers []*lark.UserInfo `bson:"target_users" json:"target_users" yaml:"target_users"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + TargetUsers []*lark.UserInfo `bson:"target_users" json:"target_users" yaml:"target_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type LarkHookNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type WechatNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type DingDingNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtMobiles []string `bson:"at_mobiles" json:"at_mobiles" yaml:"at_mobiles"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtMobiles []string `bson:"at_mobiles" json:"at_mobiles" yaml:"at_mobiles"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type MSTeamsNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtEmails []string `bson:"at_emails" json:"at_emails" yaml:"at_emails"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtEmails []string `bson:"at_emails" json:"at_emails" yaml:"at_emails"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type MailNotificationConfig struct { - TargetUsers []*User `bson:"target_users" json:"target_users" yaml:"target_users"` - DynamicRecipients []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + TargetUsers []*User `bson:"target_users" json:"target_users" yaml:"target_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type WebhookNotificationConfig struct { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index 144db954ec..7154d19a23 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -267,206 +267,59 @@ func renderNotificationStrings(inputs []string, keyMap map[string]string) []stri } func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) error { + resolver := newDynamicRecipientResolver(keyMap) + if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { - users := c.resolveDynamicRecipientsToDirectValues(cfg.DynamicRecipients, keyMap, "open_id", "user_id", "id") + users, err := resolver.resolveDirectValues([]string(cfg.DynamicRecipients), dynamicRecipientKindUserID) + if err != nil { + return err + } cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) } if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil { - users, err := c.resolveDynamicRecipientsToLarkUsers(cfg.DynamicRecipients, cfg.AppID, keyMap) + users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) if err != nil { return err } cfg.AtUsers = uniqLarkUsers(append(cfg.AtUsers, users...)) } if cfg := c.jobTaskSpec.LarkPersonNotificationConfig; cfg != nil { - users, err := c.resolveDynamicRecipientsToLarkUsers(cfg.DynamicRecipients, cfg.AppID, keyMap) + users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) if err != nil { return err } cfg.TargetUsers = uniqLarkUsers(append(cfg.TargetUsers, users...)) } if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { - emails, err := c.resolveDynamicRecipientsToEmails(cfg.DynamicRecipients, keyMap) + emails, err := resolver.resolveEmails([]string(cfg.DynamicRecipients)) if err != nil { return err } cfg.AtEmails = lo.Uniq(append(cfg.AtEmails, emails...)) } if cfg := c.jobTaskSpec.MailNotificationConfig; cfg != nil { - emails, err := c.resolveDynamicRecipientsToEmails(cfg.DynamicRecipients, keyMap) + emails, err := resolver.resolveEmails([]string(cfg.DynamicRecipients)) if err != nil { return err } cfg.TargetUsers = uniqMailUsers(append(cfg.TargetUsers, buildMailUsersFromEmails(emails)...)) } if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { - mobiles, err := c.resolveDynamicRecipientsToMobiles(cfg.DynamicRecipients, keyMap) + mobiles, err := resolver.resolveMobiles([]string(cfg.DynamicRecipients)) if err != nil { return err } cfg.AtMobiles = lo.Uniq(append(cfg.AtMobiles, mobiles...)) } if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { - users := c.resolveDynamicRecipientsToDirectValues(cfg.DynamicRecipients, keyMap, "user_id", "userid", "id") - cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) - } - - return nil -} - -func (c *NotificationJobCtl) resolveDynamicRecipientsToLarkUsers(recipients []*commonmodels.DynamicRecipient, appID string, keyMap map[string]string) ([]*lark.UserInfo, error) { - if len(recipients) == 0 { - return nil, nil - } - - client, err := larkservice.GetLarkClientByIMAppID(appID) - if err != nil { - return nil, err - } - - resp := make([]*lark.UserInfo, 0) - for _, recipient := range recipients { - value := renderNotificationString(recipient.Value, keyMap) - if value == "" { - continue - } - - idType, id, err := resolveLarkRecipient(client, recipient.IdentityType, value) + users, err := resolver.resolveDirectValues([]string(cfg.DynamicRecipients), dynamicRecipientKindUserID) if err != nil { - return nil, err - } - if id == "" { - continue - } - resp = append(resp, &lark.UserInfo{ID: id, IDType: idType}) - } - - return uniqLarkUsers(resp), nil -} - -func (c *NotificationJobCtl) resolveDynamicRecipientsToEmails(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string) ([]string, error) { - resp := make([]string, 0) - for _, recipient := range recipients { - value := renderNotificationString(recipient.Value, keyMap) - if value == "" { - continue - } - - switch recipient.IdentityType { - case "", "email": - resp = append(resp, value) - case "account": - userInfo, err := searchUserByAccount(value) - if err != nil { - return nil, err - } - if userInfo != nil && userInfo.Email != "" { - resp = append(resp, userInfo.Email) - } - } - } - return lo.Uniq(resp), nil -} - -func (c *NotificationJobCtl) resolveDynamicRecipientsToMobiles(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string) ([]string, error) { - resp := make([]string, 0) - for _, recipient := range recipients { - value := renderNotificationString(recipient.Value, keyMap) - if value == "" { - continue - } - - switch recipient.IdentityType { - case "mobile": - resp = append(resp, value) - case "account": - userInfo, err := searchUserByAccount(value) - if err != nil { - return nil, err - } - if userInfo != nil && userInfo.Phone != "" { - resp = append(resp, userInfo.Phone) - } - } - } - return lo.Uniq(resp), nil -} - -func (c *NotificationJobCtl) resolveDynamicRecipientsToDirectValues(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string, supportedTypes ...string) []string { - if len(recipients) == 0 { - return nil - } - supported := make(map[string]struct{}, len(supportedTypes)) - for _, identityType := range supportedTypes { - supported[identityType] = struct{}{} - } - resp := make([]string, 0) - for _, recipient := range recipients { - if _, ok := supported[recipient.IdentityType]; !ok { - continue - } - value := renderNotificationString(recipient.Value, keyMap) - if value == "" { - continue - } - resp = append(resp, value) - } - return lo.Uniq(resp) -} - -func resolveLarkRecipient(client *lark.Client, identityType, value string) (string, string, error) { - switch identityType { - case "", "email": - userInfo, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeEmail, value, setting.LarkUserID) - if err != nil { - return "", "", err - } - return setting.LarkUserID, util2.GetStringFromPointer(userInfo.UserId), nil - case "mobile": - userInfo, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, value, setting.LarkUserID) - if err != nil { - return "", "", err - } - return setting.LarkUserID, util2.GetStringFromPointer(userInfo.UserId), nil - case "account": - userInfo, err := searchUserByAccount(value) - if err != nil { - return "", "", err - } - if userInfo == nil { - return "", "", nil - } - if userInfo.Email != "" { - larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeEmail, userInfo.Email, setting.LarkUserID) - if err == nil { - return setting.LarkUserID, util2.GetStringFromPointer(larkUser.UserId), nil - } - } - if userInfo.Phone != "" { - larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, userInfo.Phone, setting.LarkUserID) - if err == nil { - return setting.LarkUserID, util2.GetStringFromPointer(larkUser.UserId), nil - } + return err } - return "", "", nil - default: - return "", "", fmt.Errorf("unsupported lark dynamic recipient identity type: %s", identityType) + cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) } -} -func searchUserByAccount(account string) (*user.User, error) { - resp, err := user.New().SearchUser(&user.SearchUserArgs{ - Account: account, - Page: 1, - PerPage: 1, - }) - if err != nil { - return nil, err - } - if resp == nil || len(resp.Users) == 0 { - return nil, nil - } - return resp.Users[0], nil + return nil } func uniqLarkUsers(users []*lark.UserInfo) []*lark.UserInfo { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go new file mode 100644 index 0000000000..c05195d232 --- /dev/null +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go @@ -0,0 +1,567 @@ +package jobcontroller + +import ( + "fmt" + "strings" + + "github.com/samber/lo" + + larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" + "github.com/koderover/zadig/v2/pkg/setting" + userclient "github.com/koderover/zadig/v2/pkg/shared/client/user" + larktool "github.com/koderover/zadig/v2/pkg/tool/lark" + util2 "github.com/koderover/zadig/v2/pkg/util" +) + +type dynamicRecipientKind string + +const ( + dynamicRecipientKindEmail dynamicRecipientKind = "email" + dynamicRecipientKindMobile dynamicRecipientKind = "mobile" + dynamicRecipientKindAccount dynamicRecipientKind = "account" + dynamicRecipientKindUserID dynamicRecipientKind = "user_id" + dynamicRecipientKindOpenID dynamicRecipientKind = "open_id" + + searchAllIdentityType = "*" +) + +var supportedDynamicRecipientKinds = map[setting.NotifyWebHookType]map[dynamicRecipientKind]struct{}{ + setting.NotifyWebhookTypeFeishuApp: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + dynamicRecipientKindUserID: {}, + }, + setting.NotifyWebHookTypeFeishuPerson: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + dynamicRecipientKindUserID: {}, + dynamicRecipientKindOpenID: {}, + }, + setting.NotifyWebHookTypeFeishu: { + dynamicRecipientKindUserID: {}, + }, + setting.NotifyWebHookTypeWechatWork: { + dynamicRecipientKindUserID: {}, + }, + setting.NotifyWebHookTypeDingDing: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + }, + setting.NotifyWebHookTypeMSTeam: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + }, + setting.NotifyWebHookTypeMail: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + }, +} + +type dynamicRecipientSpec struct { + raw string + key string + kind dynamicRecipientKind +} + +type dynamicRecipientResolver struct { + keyMap map[string]string + + lookupUsersByAccount func(account string) ([]*userclient.User, error) + lookupUsersByEmail func(email string) ([]*userclient.User, error) + lookupUsersByPhone func(phone string) ([]*userclient.User, error) + + accountUsersCache map[string][]*userclient.User + emailUsersCache map[string][]*userclient.User + phoneUsersCache map[string][]*userclient.User + + larkClientCache map[string]*larktool.Client + larkUserIDCache map[string]string + larkUserMissCache map[string]bool +} + +func ValidateDynamicRecipientsForNotifyType(notifyType setting.NotifyWebHookType, recipients []string) error { + if len(recipients) == 0 { + return nil + } + + supportedKinds, ok := supportedDynamicRecipientKinds[notifyType] + if !ok { + return nil + } + + for _, recipient := range recipients { + spec, err := parseDynamicRecipient(recipient) + if err != nil { + return err + } + if _, ok := supportedKinds[spec.kind]; !ok { + return fmt.Errorf("dynamic recipient %s is not supported for notification type %s", recipient, notifyType) + } + } + + return nil +} + +func newDynamicRecipientResolver(keyMap map[string]string) *dynamicRecipientResolver { + return &dynamicRecipientResolver{ + keyMap: keyMap, + lookupUsersByAccount: func(account string) ([]*userclient.User, error) { + return searchUsersByAccount(account) + }, + lookupUsersByEmail: func(email string) ([]*userclient.User, error) { + return searchUsersByEmail(email) + }, + lookupUsersByPhone: func(phone string) ([]*userclient.User, error) { + return searchUsersByPhone(phone) + }, + accountUsersCache: make(map[string][]*userclient.User), + emailUsersCache: make(map[string][]*userclient.User), + phoneUsersCache: make(map[string][]*userclient.User), + larkClientCache: make(map[string]*larktool.Client), + larkUserIDCache: make(map[string]string), + larkUserMissCache: make(map[string]bool), + } +} + +func (r *dynamicRecipientResolver) resolveEmails(recipients []string) ([]string, error) { + resp := make([]string, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + + switch spec.kind { + case dynamicRecipientKindEmail: + resp = append(resp, value) + case dynamicRecipientKindMobile: + users, err := r.getUsersByPhone(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Email != "" { + resp = append(resp, user.Email) + } + } + case dynamicRecipientKindAccount: + users, err := r.getUsersByAccount(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Email != "" { + resp = append(resp, user.Email) + } + } + default: + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to email", recipient) + } + } + return uniqStrings(resp), nil +} + +func (r *dynamicRecipientResolver) resolveMobiles(recipients []string) ([]string, error) { + resp := make([]string, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + + switch spec.kind { + case dynamicRecipientKindMobile: + resp = append(resp, value) + case dynamicRecipientKindEmail: + users, err := r.getUsersByEmail(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Phone != "" { + resp = append(resp, user.Phone) + } + } + case dynamicRecipientKindAccount: + users, err := r.getUsersByAccount(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Phone != "" { + resp = append(resp, user.Phone) + } + } + default: + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to mobile", recipient) + } + } + return uniqStrings(resp), nil +} + +func (r *dynamicRecipientResolver) resolveDirectValues(recipients []string, supportedKinds ...dynamicRecipientKind) ([]string, error) { + supported := make(map[dynamicRecipientKind]struct{}, len(supportedKinds)) + for _, kind := range supportedKinds { + supported[kind] = struct{}{} + } + + resp := make([]string, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + if _, ok := supported[spec.kind]; !ok { + return nil, fmt.Errorf("dynamic recipient %s is not supported in this notification channel", recipient) + } + resp = append(resp, value) + } + return uniqStrings(resp), nil +} + +func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID string, allowOpenID bool) ([]*larktool.UserInfo, error) { + if len(recipients) == 0 { + return nil, nil + } + + client, err := r.getLarkClient(appID) + if err != nil { + return nil, err + } + + resp := make([]*larktool.UserInfo, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + + switch spec.kind { + case dynamicRecipientKindUserID: + resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserID}) + case dynamicRecipientKindOpenID: + if !allowOpenID { + return nil, fmt.Errorf("dynamic recipient %s is not supported in this notification channel", recipient) + } + resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserOpenID}) + case dynamicRecipientKindEmail: + ids, err := r.resolveLarkUserIDsByEmail(client, appID, value) + if err != nil { + return nil, err + } + for _, id := range ids { + resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) + } + case dynamicRecipientKindMobile: + ids, err := r.resolveLarkUserIDsByPhone(client, appID, value) + if err != nil { + return nil, err + } + for _, id := range ids { + resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) + } + case dynamicRecipientKindAccount: + ids, err := r.resolveLarkUserIDsByAccount(client, appID, value) + if err != nil { + return nil, err + } + for _, id := range ids { + resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) + } + default: + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to lark user", recipient) + } + } + + return uniqLarkUsers(resp), nil +} + +func (r *dynamicRecipientResolver) resolveLarkUserIDsByEmail(client *larktool.Client, appID, email string) ([]string, error) { + if id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, email); err != nil { + return nil, err + } else if found { + return []string{id}, nil + } + + users, err := r.getUsersByEmail(email) + if err != nil { + return nil, err + } + + resp := make([]string, 0) + for _, user := range users { + if user == nil || user.Phone == "" { + continue + } + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, user.Phone) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + } + } + return uniqStrings(resp), nil +} + +func (r *dynamicRecipientResolver) resolveLarkUserIDsByPhone(client *larktool.Client, appID, phone string) ([]string, error) { + if id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, phone); err != nil { + return nil, err + } else if found { + return []string{id}, nil + } + + users, err := r.getUsersByPhone(phone) + if err != nil { + return nil, err + } + + resp := make([]string, 0) + for _, user := range users { + if user == nil || user.Email == "" { + continue + } + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, user.Email) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + } + } + return uniqStrings(resp), nil +} + +func (r *dynamicRecipientResolver) resolveLarkUserIDsByAccount(client *larktool.Client, appID, account string) ([]string, error) { + users, err := r.getUsersByAccount(account) + if err != nil { + return nil, err + } + + resp := make([]string, 0) + for _, user := range users { + if user == nil { + continue + } + if user.Email != "" { + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, user.Email) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + continue + } + } + if user.Phone != "" { + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, user.Phone) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + } + } + } + return uniqStrings(resp), nil +} + +func (r *dynamicRecipientResolver) lookupLarkUserID(client *larktool.Client, appID, queryType, value string) (string, bool, error) { + cacheKey := strings.Join([]string{appID, queryType, value}, ":") + if cached, ok := r.larkUserIDCache[cacheKey]; ok { + return cached, true, nil + } + if r.larkUserMissCache[cacheKey] { + return "", false, nil + } + + userInfo, err := client.GetUserIDByEmailOrMobile(queryType, value, setting.LarkUserID) + if err != nil { + if isLarkUserNotFoundErr(err) { + r.larkUserMissCache[cacheKey] = true + return "", false, nil + } + return "", false, err + } + + userID := util2.GetStringFromPointer(userInfo.UserId) + if userID == "" { + r.larkUserMissCache[cacheKey] = true + return "", false, nil + } + + r.larkUserIDCache[cacheKey] = userID + return userID, true, nil +} + +func (r *dynamicRecipientResolver) getUsersByAccount(account string) ([]*userclient.User, error) { + if users, ok := r.accountUsersCache[account]; ok { + return users, nil + } + users, err := r.lookupUsersByAccount(account) + if err != nil { + return nil, err + } + r.accountUsersCache[account] = users + return users, nil +} + +func (r *dynamicRecipientResolver) getUsersByEmail(email string) ([]*userclient.User, error) { + if users, ok := r.emailUsersCache[email]; ok { + return users, nil + } + users, err := r.lookupUsersByEmail(email) + if err != nil { + return nil, err + } + r.emailUsersCache[email] = users + return users, nil +} + +func (r *dynamicRecipientResolver) getUsersByPhone(phone string) ([]*userclient.User, error) { + if users, ok := r.phoneUsersCache[phone]; ok { + return users, nil + } + users, err := r.lookupUsersByPhone(phone) + if err != nil { + return nil, err + } + r.phoneUsersCache[phone] = users + return users, nil +} + +func (r *dynamicRecipientResolver) getLarkClient(appID string) (*larktool.Client, error) { + if client, ok := r.larkClientCache[appID]; ok { + return client, nil + } + + client, err := larkservice.GetLarkClientByIMAppID(appID) + if err != nil { + return nil, err + } + + r.larkClientCache[appID] = client + return client, nil +} + +func (r *dynamicRecipientResolver) resolveRecipient(raw string) (*dynamicRecipientSpec, string, bool, error) { + spec, err := parseDynamicRecipient(raw) + if err != nil { + return nil, "", false, err + } + + value := strings.TrimSpace(renderNotificationString(spec.raw, r.keyMap)) + if value == "" || strings.Contains(value, "{{.") { + return spec, "", false, nil + } + + return spec, value, true, nil +} + +func parseDynamicRecipient(input string) (*dynamicRecipientSpec, error) { + input = strings.TrimSpace(input) + if !strings.HasPrefix(input, "{{.") || !strings.HasSuffix(input, "}}") { + return nil, fmt.Errorf("dynamic recipient must be a single template variable, got %s", input) + } + + key := strings.TrimSuffix(strings.TrimPrefix(input, "{{."), "}}") + if key == "" { + return nil, fmt.Errorf("dynamic recipient %s is invalid", input) + } + + parts := strings.Split(strings.ToLower(key), ".") + suffix := parts[len(parts)-1] + + var kind dynamicRecipientKind + switch suffix { + case "email": + kind = dynamicRecipientKindEmail + case "mobile", "phone": + kind = dynamicRecipientKindMobile + case "account": + kind = dynamicRecipientKindAccount + case "user_id", "userid": + kind = dynamicRecipientKindUserID + case "open_id": + kind = dynamicRecipientKindOpenID + default: + return nil, fmt.Errorf("dynamic recipient %s is not supported, only email/mobile(phone)/account/user_id(userid)/open_id are allowed", input) + } + + return &dynamicRecipientSpec{ + raw: input, + key: key, + kind: kind, + }, nil +} + +func searchUsersByAccount(account string) ([]*userclient.User, error) { + resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ + Account: account, + IdentityType: searchAllIdentityType, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Users, nil +} + +func searchUsersByEmail(email string) ([]*userclient.User, error) { + resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ + Email: email, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Users, nil +} + +func searchUsersByPhone(phone string) ([]*userclient.User, error) { + resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ + Phone: phone, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Users, nil +} + +func uniqStrings(items []string) []string { + items = lo.Filter(items, func(item string, _ int) bool { + return item != "" + }) + return lo.Uniq(items) +} + +func isLarkUserNotFoundErr(err error) bool { + if err == nil { + return false + } + return strings.Contains(strings.ToLower(err.Error()), "user not found") +} diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/types.go b/pkg/microservice/aslan/core/workflow/service/workflow/types.go index 35f8cd5c3f..57033f0d74 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go @@ -160,11 +160,6 @@ type CreateCustomTaskNotifyInput struct { MailNotificationConfig *CreateCustomTaskMailNotificationConfig `json:"mail_notification_config"` } -type CreateCustomTaskDynamicRecipient struct { - Value string `json:"value"` - IdentityType string `json:"identity_type"` -} - type CreateCustomTaskLarkUserInfo struct { ID string `json:"id"` // 支持 open_id、user_id @@ -176,43 +171,43 @@ type CreateCustomTaskLarkUserInfo struct { } type CreateCustomTaskLarkGroupNotificationConfig struct { - ChatID string `json:"chat_id"` - AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + ChatID string `json:"chat_id"` + AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskLarkPersonNotificationConfig struct { - Users []CreateCustomTaskLarkUserInfo `json:"users"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + Users []CreateCustomTaskLarkUserInfo `json:"users"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskLarkHookNotificationConfig struct { - AtUsers []string `json:"at_users"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` - IsAtAll bool `json:"is_at_all"` + AtUsers []string `json:"at_users"` + DynamicRecipients []string `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskWechatNotificationConfig struct { - AtUsers []string `json:"at_users"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` - IsAtAll bool `json:"is_at_all"` + AtUsers []string `json:"at_users"` + DynamicRecipients []string `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskDingDingNotificationConfig struct { - AtMobiles []string `json:"at_mobiles"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` - IsAtAll bool `json:"is_at_all"` + AtMobiles []string `json:"at_mobiles"` + DynamicRecipients []string `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskMSTeamsNotificationConfig struct { - AtEmails []string `json:"at_emails"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + AtEmails []string `json:"at_emails"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskMailNotificationConfig struct { - UserIDs []string `json:"user_ids"` - Users []*commonmodels.User `json:"users"` - DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"` + UserIDs []string `json:"user_ids"` + Users []*commonmodels.User `json:"users"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskParam struct { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index 8d4e55f064..19cd6535b8 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -598,7 +598,10 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels // use the latest workflow's notification settings workflow.NotifyCtls = originalWorkflow.NotifyCtls } else { - workflow.NotifyCtls = updateNotifyCtls(workflow.NotifyCtls, args.NotifyInput) + workflow.NotifyCtls, err = updateNotifyCtls(workflow.NotifyCtls, args.NotifyInput) + if err != nil { + return resp, e.ErrCreateTask.AddDesc(err.Error()) + } } workflowTask.Hash = originalWorkflow.Hash } else { @@ -732,24 +735,25 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels return resp, nil } -func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*CreateCustomTaskNotifyInput) []*commonmodels.NotifyCtl { +func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*CreateCustomTaskNotifyInput) ([]*commonmodels.NotifyCtl, error) { notifyInputsMap := make(map[int]*CreateCustomTaskNotifyInput) for _, notifyInput := range notifyInputs { notifyInputsMap[notifyInput.ID] = notifyInput } - toDynamicRecipients := func(inputs []CreateCustomTaskDynamicRecipient) []*commonmodels.DynamicRecipient { - resp := make([]*commonmodels.DynamicRecipient, 0, len(inputs)) + toDynamicRecipients := func(notifyType setting.NotifyWebHookType, inputs []string) ([]string, error) { + resp := make([]string, 0, len(inputs)) for _, input := range inputs { - if input.Value == "" { + input = strings.TrimSpace(input) + if input == "" { continue } - resp = append(resp, &commonmodels.DynamicRecipient{ - Value: input.Value, - IdentityType: input.IdentityType, - }) + resp = append(resp, input) } - return resp + if err := runtimeJobController.ValidateDynamicRecipientsForNotifyType(notifyType, resp); err != nil { + return nil, err + } + return resp, nil } for i, notifyCtl := range notifyCtls { @@ -765,11 +769,15 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark hook notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.LarkHookNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.LarkHookNotificationConfig{ HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress, AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers, - DynamicRecipients: toDynamicRecipients(notifyInput.LarkHookNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll, } @@ -779,10 +787,14 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark person notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.LarkPersonNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.LarkPersonNotificationConfig{ AppID: notifyCtl.LarkPersonNotificationConfig.AppID, - DynamicRecipients: toDynamicRecipients(notifyInput.LarkPersonNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), } targetUsers := make([]*larktool.UserInfo, 0) @@ -802,10 +814,14 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark group notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.LarkGroupNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.LarkGroupNotificationConfig{ AppID: notifyCtl.LarkGroupNotificationConfig.AppID, - DynamicRecipients: toDynamicRecipients(notifyInput.LarkGroupNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), Chat: &commonmodels.LarkChat{ ChatID: notifyInput.LarkGroupNotificationConfig.ChatID, }, @@ -826,11 +842,15 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("wechat notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.WechatNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.WechatNotificationConfig{ HookAddress: notifyCtl.WechatNotificationConfig.HookAddress, AtUsers: notifyInput.WechatNotificationConfig.AtUsers, - DynamicRecipients: toDynamicRecipients(notifyInput.WechatNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll, } @@ -840,11 +860,15 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("dingding notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.DingDingNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.DingDingNotificationConfig{ HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress, AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles, - DynamicRecipients: toDynamicRecipients(notifyInput.DingDingNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll, } @@ -854,11 +878,15 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("msteams notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.MSTeamsNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.MSTeamsNotificationConfig{ HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress, AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails, - DynamicRecipients: toDynamicRecipients(notifyInput.MSTeamsNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), } notifyCtl.MSTeamsNotificationConfig = config @@ -867,10 +895,14 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("mail notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.MailNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.MailNotificationConfig{ TargetUsers: make([]*commonmodels.User, 0), - DynamicRecipients: toDynamicRecipients(notifyInput.MailNotificationConfig.DynamicRecipients), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), } if len(notifyInput.MailNotificationConfig.Users) > 0 { @@ -901,7 +933,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } } } - return notifyCtls + return notifyCtls, nil } func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string]string { diff --git a/pkg/microservice/user/core/handler/user/user.go b/pkg/microservice/user/core/handler/user/user.go index 0f037b9cce..eb2d42d3aa 100644 --- a/pkg/microservice/user/core/handler/user/user.go +++ b/pkg/microservice/user/core/handler/user/user.go @@ -275,6 +275,10 @@ func ListUsers(c *gin.Context) { if len(args.UIDs) > 0 { ctx.Resp, ctx.RespErr = permission.SearchUsersByUIDs(args.UIDs, ctx.Logger) + } else if len(args.Email) > 0 { + ctx.Resp, ctx.RespErr = permission.SearchUsersByEmail(args, ctx.Logger) + } else if len(args.Phone) > 0 { + ctx.Resp, ctx.RespErr = permission.SearchUsersByPhone(args, ctx.Logger) } else if len(args.Account) > 0 { if len(args.IdentityType) == 0 { args.IdentityType = config.SystemIdentityType @@ -384,6 +388,10 @@ func ListUsersBrief(c *gin.Context) { var resp *types.UsersResp if len(args.UIDs) > 0 { resp, err = permission.SearchUsersByUIDs(args.UIDs, ctx.Logger) + } else if len(args.Email) > 0 { + resp, err = permission.SearchUsersByEmail(args, ctx.Logger) + } else if len(args.Phone) > 0 { + resp, err = permission.SearchUsersByPhone(args, ctx.Logger) } else if len(args.Account) > 0 { if len(args.IdentityType) == 0 { args.IdentityType = config.SystemIdentityType diff --git a/pkg/microservice/user/core/init/dm_mysql.sql b/pkg/microservice/user/core/init/dm_mysql.sql index a06dee9bf2..1eed155595 100644 --- a/pkg/microservice/user/core/init/dm_mysql.sql +++ b/pkg/microservice/user/core/init/dm_mysql.sql @@ -42,6 +42,8 @@ CREATE TABLE IF NOT EXISTS "user"( ) ; CREATE UNIQUE INDEX IF NOT EXISTS account ON "user"(account,identity_type); +CREATE INDEX IF NOT EXISTS idx_email ON "user"(email); +CREATE INDEX IF NOT EXISTS idx_phone ON "user"(phone); CREATE TABLE IF NOT EXISTS user_group ( group_id varchar(64) NOT NULL COMMENT '用户组ID', diff --git a/pkg/microservice/user/core/init/mysql.sql b/pkg/microservice/user/core/init/mysql.sql index 68d491b991..14a926951a 100644 --- a/pkg/microservice/user/core/init/mysql.sql +++ b/pkg/microservice/user/core/init/mysql.sql @@ -38,6 +38,8 @@ CREATE TABLE IF NOT EXISTS `user`( `created_at` int(11) unsigned NOT NULL COMMENT '创建时间', `updated_at` int(11) unsigned NOT NULL COMMENT '修改时间', UNIQUE KEY `account` (`account`,`identity_type`), + KEY `idx_email` (`email`) USING BTREE, + KEY `idx_phone` (`phone`) USING BTREE, PRIMARY KEY (`uid`) ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Compact; diff --git a/pkg/microservice/user/core/repository/models/user.go b/pkg/microservice/user/core/repository/models/user.go index 24f87967ec..f93dcb9cca 100644 --- a/pkg/microservice/user/core/repository/models/user.go +++ b/pkg/microservice/user/core/repository/models/user.go @@ -21,8 +21,8 @@ type User struct { UID string `gorm:"primary" json:"uid"` Name string `json:"name"` IdentityType string `gorm:"default:'unknown'" json:"identity_type"` - Email string `json:"email"` - Phone string `json:"phone"` + Email string `gorm:"index:idx_email" json:"email"` + Phone string `gorm:"index:idx_phone" json:"phone"` Account string `json:"account"` APIToken string `gorm:"api_token" json:"api_token"` APITokenEnabled bool `gorm:"column:api_token_enabled;default:0" json:"api_token_enabled"` diff --git a/pkg/microservice/user/core/repository/orm/user.go b/pkg/microservice/user/core/repository/orm/user.go index c45755a5e3..eb9658b325 100644 --- a/pkg/microservice/user/core/repository/orm/user.go +++ b/pkg/microservice/user/core/repository/orm/user.go @@ -46,6 +46,33 @@ func GetUser(account string, identityType string, db *gorm.DB) (*models.User, er return &user, nil } +func ListUsersByAccount(account string, db *gorm.DB) ([]models.User, error) { + users := make([]models.User, 0) + err := db.Where("account = ?", account).Find(&users).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return users, nil +} + +func ListUsersByEmail(email string, db *gorm.DB) ([]models.User, error) { + users := make([]models.User, 0) + err := db.Where("email = ?", email).Find(&users).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return users, nil +} + +func ListUsersByPhone(phone string, db *gorm.DB) ([]models.User, error) { + users := make([]models.User, 0) + err := db.Where("phone = ?", phone).Find(&users).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return users, nil +} + // GetUserByUid Get a user based on uid func GetUserByUid(uid string, db *gorm.DB) (*models.User, error) { var user models.User diff --git a/pkg/microservice/user/core/service/permission/user.go b/pkg/microservice/user/core/service/permission/user.go index 27510fa060..330b020d8d 100644 --- a/pkg/microservice/user/core/service/permission/user.go +++ b/pkg/microservice/user/core/service/permission/user.go @@ -79,6 +79,8 @@ type OpenAPIQueryArgs struct { type QueryArgs struct { Name string `json:"name,omitempty"` Account string `json:"account,omitempty" form:"account"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` IdentityType string `json:"identity_type,omitempty"` UIDs []string `json:"uids,omitempty"` PerPage int `json:"per_page,omitempty" form:"perPage"` @@ -89,6 +91,8 @@ type QueryArgs struct { Order setting.ListUserOrder `json:"order,omitempty" form:"order"` } +const allIdentityTypes = "*" + type Password struct { Uid string `json:"uid"` OldPassword string `json:"oldPassword"` @@ -424,6 +428,15 @@ func GetUserSetting(uid string, logger *zap.SugaredLogger) (*types.UserSetting, } func SearchUserByAccount(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) { + if args.IdentityType == allIdentityTypes { + users, err := orm.ListUsersByAccount(args.Account, repository.DB) + if err != nil { + logger.Errorf("SearchUserByAccount ListUsersByAccount account:%s error, error msg:%s", args.Account, err.Error()) + return nil, err + } + return buildUsersRespFromModels(users, logger) + } + user, err := orm.GetUser(args.Account, args.IdentityType, repository.DB) if err != nil { logger.Errorf("SearchUserByAccount GetUser By account:%s error, error msg:%s", args.Account, err.Error()) @@ -435,34 +448,49 @@ func SearchUserByAccount(args *QueryArgs, logger *zap.SugaredLogger) (*types.Use TotalCount: 0, }, nil } - userLogins, err := orm.ListUserLogins([]string{user.UID}, repository.DB) + + return buildUsersRespFromModels([]models.User{*user}, logger) +} + +func SearchUsersByEmail(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) { + users, err := orm.ListUsersByEmail(args.Email, repository.DB) if err != nil { - logger.Errorf("SearchUserByAccount ListUserLogins By uid:%s error, error msg:%s", user.UID, err.Error()) + logger.Errorf("SearchUsersByEmail ListUsersByEmail email:%s error, error msg:%s", args.Email, err.Error()) return nil, err } - usersInfo := mergeUserLogin([]models.User{*user}, *userLogins, logger) + return buildUsersRespFromModels(users, logger) +} - for _, uInfo := range usersInfo { - roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) - if err != nil { - logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) - return nil, err - } - rolebindings := make([]*types.RoleBinding, 0) - for _, role := range roles { - rolebindings = append(rolebindings, &types.RoleBinding{ - UID: uInfo.Uid, - Role: role.Name, - }) - if role.Name == string(setting.SystemAdmin) { - uInfo.Admin = true - uInfo.APITokenEnabled = true - } - } - uInfo.SystemRoleBindings = rolebindings +func SearchUsersByPhone(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) { + users, err := orm.ListUsersByPhone(args.Phone, repository.DB) + if err != nil { + logger.Errorf("SearchUsersByPhone ListUsersByPhone phone:%s error, error msg:%s", args.Phone, err.Error()) + return nil, err } - if err := fillUsersMFAEnabled(usersInfo); err != nil { - logger.Errorf("SearchUserByAccount fillUsersMFAEnabled error, error msg:%s", err.Error()) + return buildUsersRespFromModels(users, logger) +} + +func buildUsersRespFromModels(users []models.User, logger *zap.SugaredLogger) (*types.UsersResp, error) { + if len(users) == 0 { + return &types.UsersResp{ + Users: nil, + TotalCount: 0, + }, nil + } + + uids := make([]string, 0, len(users)) + for _, user := range users { + uids = append(uids, user.UID) + } + + userLogins, err := orm.ListUserLogins(uids, repository.DB) + if err != nil { + logger.Errorf("buildUsersRespFromModels ListUserLogins By uids:%s error, error msg:%s", uids, err.Error()) + return nil, err + } + + usersInfo := mergeUserLogin(users, *userLogins, logger) + if err := enrichUsersInfo(usersInfo, logger); err != nil { return nil, err } @@ -566,27 +594,7 @@ func SearchUsers(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, usersInfo = mergeUserLoginWithLoginTime(users, *userLogins, logger) } - for _, uInfo := range usersInfo { - roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) - if err != nil { - logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) - return nil, err - } - rolebindings := make([]*types.RoleBinding, 0) - for _, role := range roles { - rolebindings = append(rolebindings, &types.RoleBinding{ - UID: uInfo.Uid, - Role: role.Name, - }) - if role.Name == string(setting.SystemAdmin) { - uInfo.Admin = true - uInfo.APITokenEnabled = true - } - } - uInfo.SystemRoleBindings = rolebindings - } - if err := fillUsersMFAEnabled(usersInfo); err != nil { - logger.Errorf("SearchUsers fillUsersMFAEnabled error, error msg:%s", err.Error()) + if err := enrichUsersInfo(usersInfo, logger); err != nil { return nil, err } @@ -634,6 +642,33 @@ func fillUsersMFAEnabled(usersInfo []*types.UserInfo) error { return nil } +func enrichUsersInfo(usersInfo []*types.UserInfo, logger *zap.SugaredLogger) error { + for _, uInfo := range usersInfo { + roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) + if err != nil { + logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) + return err + } + rolebindings := make([]*types.RoleBinding, 0) + for _, role := range roles { + rolebindings = append(rolebindings, &types.RoleBinding{ + UID: uInfo.Uid, + Role: role.Name, + }) + if role.Name == string(setting.SystemAdmin) { + uInfo.Admin = true + uInfo.APITokenEnabled = true + } + } + uInfo.SystemRoleBindings = rolebindings + } + if err := fillUsersMFAEnabled(usersInfo); err != nil { + logger.Errorf("enrichUsersInfo fillUsersMFAEnabled error, error msg:%s", err.Error()) + return err + } + return nil +} + func mergeUserLoginWithLoginTime(users []models.UserWithLoginTime, userLogins []models.UserLogin, logger *zap.SugaredLogger) []*types.UserInfo { userLoginMap := make(map[string]models.UserLogin) for _, userLogin := range userLogins { @@ -691,41 +726,7 @@ func SearchUsersByUIDs(uids []string, logger *zap.SugaredLogger) (*types.UsersRe logger.Errorf("SearchUsersByUIDs SeachUsers By uids:%s error, error msg:%s", uids, err.Error()) return nil, err } - userLogins, err := orm.ListUserLogins(uids, repository.DB) - if err != nil { - logger.Errorf("SearchUsersByUIDs ListUserLogins By uids:%s error, error msg:%s", uids, err.Error()) - return nil, err - } - usersInfo := mergeUserLogin(users, *userLogins, logger) - - for _, uInfo := range usersInfo { - roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) - if err != nil { - logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) - return nil, err - } - rolebindings := make([]*types.RoleBinding, 0) - for _, role := range roles { - rolebindings = append(rolebindings, &types.RoleBinding{ - UID: uInfo.Uid, - Role: role.Name, - }) - if role.Name == string(setting.SystemAdmin) { - uInfo.Admin = true - uInfo.APITokenEnabled = true - } - } - uInfo.SystemRoleBindings = rolebindings - } - if err := fillUsersMFAEnabled(usersInfo); err != nil { - logger.Errorf("SearchUsersByUIDs fillUsersMFAEnabled error, error msg:%s", err.Error()) - return nil, err - } - - return &types.UsersResp{ - Users: usersInfo, - TotalCount: int64(len(usersInfo)), - }, nil + return buildUsersRespFromModels(users, logger) } func getLoginId(user *models.User, loginType config.LoginType) string { diff --git a/pkg/shared/client/user/user.go b/pkg/shared/client/user/user.go index 167d00cfd7..801c9d0a0a 100644 --- a/pkg/shared/client/user/user.go +++ b/pkg/shared/client/user/user.go @@ -82,6 +82,8 @@ func (c *Client) CreateUser(args *CreateUserArgs) (*CreateUserResp, error) { type SearchUserArgs struct { Name string `json:"name,omitempty"` Account string `json:"account,omitempty"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` IdentityType string `json:"identity_type,omitempty"` UIDs []string `json:"uids,omitempty"` PerPage int `json:"per_page,omitempty"` From 1e275bd95be70ea2b5f125f6bb79c713f211ea25 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 16 Jun 2026 16:28:25 +0800 Subject: [PATCH 09/22] support workflow trigger runtime variables Signed-off-by: huanghongbo-hhb --- .../core/common/repository/models/workflow.go | 3 + .../core/common/util/workflow_variables.go | 25 ++++ .../service/webhook/gerrit_workflowv4_task.go | 34 +++-- .../service/webhook/gitee_workflowv4_task.go | 31 +++-- .../service/webhook/github_workflowv4_task.go | 14 +- .../service/webhook/gitlab_workflowv4_task.go | 31 +++-- .../service/workflow/controller/workflow.go | 124 ++++++++++-------- .../workflow/service/workflow/workflow_v4.go | 7 + 8 files changed, 177 insertions(+), 92 deletions(-) diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow.go b/pkg/microservice/aslan/core/common/repository/models/workflow.go index fbb17ba28b..12f7118a85 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -485,11 +485,14 @@ type HookPayload struct { Owner string `bson:"owner" json:"owner,omitempty"` Repo string `bson:"repo" json:"repo,omitempty"` Branch string `bson:"branch" json:"branch,omitempty"` + TargetBranch string `bson:"target_branch" json:"target_branch,omitempty"` Ref string `bson:"ref" json:"ref,omitempty"` IsPr bool `bson:"is_pr" json:"is_pr,omitempty"` CheckRunID int64 `bson:"check_run_id" json:"check_run_id,omitempty"` MergeRequestID string `bson:"merge_request_id" json:"merge_request_id,omitempty"` CommitID string `bson:"commit_id" json:"commit_id,omitempty"` + CommitMessage string `bson:"commit_message" json:"commit_message,omitempty"` + Committer string `bson:"committer" json:"committer,omitempty"` DeliveryID string `bson:"delivery_id" json:"delivery_id,omitempty"` CodehostID int `bson:"codehost_id" json:"codehost_id"` EventType string `bson:"event_type" json:"event_type"` diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go index ff22a350ff..22c35f96f6 100644 --- a/pkg/microservice/aslan/core/common/util/workflow_variables.go +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -91,12 +91,37 @@ func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectNa }) } if workflow.HookPayload != nil { + resp = append(resp, BuildWorkflowTriggerVariableKVs(workflow.HookPayload)...) resp = append(resp, BuildPayloadVariables(workflow.HookPayload.RawPayload)...) } return resp } +func BuildWorkflowTriggerVariableKVs(hookPayload *commonmodels.HookPayload) []*commonmodels.KeyVal { + if hookPayload == nil { + return nil + } + + resp := make([]*commonmodels.KeyVal, 0, 7) + appendIfNotEmpty := func(key, value string) { + if value == "" { + return + } + resp = append(resp, &commonmodels.KeyVal{Key: key, Value: value, IsCredential: false}) + } + + appendIfNotEmpty("workflow.trigger.branch", hookPayload.Branch) + appendIfNotEmpty("workflow.trigger.target_branch", hookPayload.TargetBranch) + appendIfNotEmpty("workflow.trigger.pr", hookPayload.MergeRequestID) + appendIfNotEmpty("workflow.trigger.commit_id", hookPayload.CommitID) + appendIfNotEmpty("workflow.trigger.commit_message", hookPayload.CommitMessage) + appendIfNotEmpty("workflow.trigger.committer", hookPayload.Committer) + appendIfNotEmpty("workflow.trigger.event", hookPayload.EventType) + + return resp +} + func BuildWorkflowRuntimeVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { return BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now) } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go index f16aa4464f..c63ad2579f 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go @@ -165,6 +165,7 @@ func (gruem *gerritChangeMergedEventMatcherForWorkflowV4) GetHookRepo(hookRepo * RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, TargetBranch: hookRepo.Branch, + PR: gruem.Event.Change.Number, CommitID: gruem.Event.NewRev, CommitMessage: gruem.Event.Change.CommitMessage, Committer: hookRepo.Committer, @@ -258,7 +259,6 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba return fmt.Errorf(errMsg) } var errorList = &multierror.Error{} - var hookPayload *commonmodels.HookPayload var notification *commonmodels.Notification for _, workflow := range workflows { gitHooks, err := commonrepo.NewWorkflowV4GitHookColl().List(internalhandler.NewBackgroupContext(), workflow.Name) @@ -281,6 +281,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba continue } for _, item := range gitHooks { + var hookPayload *commonmodels.HookPayload if !item.Enabled { continue } @@ -312,7 +313,8 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba eventRepo := matcher.GetHookRepo(item.MainRepo) var mergeRequestID, commitID string - if m, ok := matcher.(*gerritPatchsetCreatedEventMatcherForWorkflowV4); ok { + switch m := matcher.(type) { + case *gerritPatchsetCreatedEventMatcherForWorkflowV4: if item.CheckPatchSetChange { // for different patch sets under the same pr, if the updated contents of the two patch sets are exactly the same, and the task triggered by the previous patch set is executed successfully, the new patch set will no longer trigger the task. // TODO THE OLD IMPLEMENTATION DOES NOT WORK FOR A LONG TIME, SO I DELETED THEM. REWITE IT WHEN NECESSARY. @@ -343,17 +345,23 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba mainRepo, m.Event.Change.Number, baseURI, false, false, false, true, log, ) } - - hookPayload = &commonmodels.HookPayload{ - Owner: eventRepo.RepoOwner, - Repo: eventRepo.RepoName, - Branch: eventRepo.Branch, - IsPr: true, - CodehostID: item.MainRepo.CodehostID, - MergeRequestID: mergeRequestID, - CommitID: commitID, - RawPayload: string(body), - } + case *gerritChangeMergedEventMatcherForWorkflowV4: + mergeRequestID = strconv.Itoa(m.Event.Change.Number) + commitID = eventRepo.CommitID + } + hookPayload = &commonmodels.HookPayload{ + Owner: eventRepo.RepoOwner, + Repo: eventRepo.RepoName, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + IsPr: true, + CodehostID: item.MainRepo.CodehostID, + MergeRequestID: mergeRequestID, + CommitID: commitID, + CommitMessage: eventRepo.CommitMessage, + Committer: eventRepo.Committer, + EventType: event.Type, + RawPayload: string(body), } workflowController := controller.CreateWorkflowController(item.WorkflowArg) if err := workflowController.UpdateWithLatestWorkflow(nil); err != nil { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go index fcc288f0fd..3522e95d7b 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go @@ -281,9 +281,12 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque Repo: eventRepo.RepoName, CodehostID: item.MainRepo.CodehostID, Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitMessage: ev.PullRequest.Title, + Committer: ev.PullRequest.User.Login, EventType: eventType, RawPayload: rawPayload, } @@ -295,21 +298,27 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque autoCancelOpt.CommitID = commitID autoCancelOpt.Ref = ref hookPayload = &commonmodels.HookPayload{ - Owner: eventRepo.RepoOwner, - Repo: eventRepo.RepoName, - CodehostID: item.MainRepo.CodehostID, - Branch: eventRepo.Branch, - Ref: ref, - IsPr: false, - CommitID: commitID, - EventType: eventType, - RawPayload: rawPayload, + Owner: eventRepo.RepoOwner, + Repo: eventRepo.RepoName, + CodehostID: item.MainRepo.CodehostID, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Ref: ref, + IsPr: false, + CommitID: commitID, + CommitMessage: eventRepo.CommitMessage, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } case *gitee.TagPushEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, - RawPayload: rawPayload, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go index 60f192b5e5..57adabc14d 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go @@ -276,12 +276,15 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deli Owner: *ev.Repo.Owner.Login, Repo: *ev.Repo.Name, Branch: *ev.PullRequest.Base.Ref, + TargetBranch: *ev.PullRequest.Base.Ref, Ref: *ev.PullRequest.Head.SHA, IsPr: true, CodehostID: item.MainRepo.CodehostID, DeliveryID: deliveryID, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitMessage: *ev.PullRequest.Title, + Committer: *ev.PullRequest.User.Login, EventType: eventType, RawPayload: rawPayload, } @@ -296,11 +299,15 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deli hookPayload = &commonmodels.HookPayload{ Owner: *ev.Repo.Owner.Login, Repo: *ev.Repo.Name, + Branch: getBranchFromRef(ref), + TargetBranch: getBranchFromRef(ref), Ref: ref, IsPr: false, CodehostID: item.MainRepo.CodehostID, DeliveryID: deliveryID, CommitID: commitID, + CommitMessage: ev.GetHeadCommit().GetMessage(), + Committer: ev.GetPusher().GetName(), EventType: eventType, RawPayload: rawPayload, } @@ -308,8 +315,11 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deli case *github.CreateEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, - RawPayload: rawPayload, + Branch: item.MainRepo.Branch, + TargetBranch: item.MainRepo.Branch, + Committer: item.MainRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go index 90c33a2e1b..8458ced3b9 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go @@ -372,9 +372,12 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requ Owner: eventRepo.RepoOwner, Repo: eventRepo.RepoName, Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitMessage: ev.ObjectAttributes.Title, + Committer: ev.User.Username, CodehostID: eventRepo.CodehostID, EventType: eventType, RawPayload: rawPayload, @@ -387,21 +390,27 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requ autoCancelOpt.Ref = ref autoCancelOpt.CommitID = commitID hookPayload = &commonmodels.HookPayload{ - Owner: eventRepo.RepoOwner, - Repo: eventRepo.RepoName, - Branch: eventRepo.Branch, - Ref: ref, - IsPr: false, - CommitID: commitID, - CodehostID: eventRepo.CodehostID, - EventType: eventType, - RawPayload: rawPayload, + Owner: eventRepo.RepoOwner, + Repo: eventRepo.RepoName, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Ref: ref, + IsPr: false, + CommitID: commitID, + CommitMessage: eventRepo.CommitMessage, + Committer: eventRepo.Committer, + CodehostID: eventRepo.CodehostID, + EventType: eventType, + RawPayload: rawPayload, } case *gitlab.TagEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, - RawPayload: rawPayload, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index dad86da0b5..b01249bdda 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -400,6 +400,14 @@ func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid resp = append(resp, newParam) } if w.HookPayload != nil { + for _, kv := range commonutil.BuildWorkflowTriggerVariableKVs(w.HookPayload) { + resp = append(resp, &commonmodels.Param{ + Name: kv.Key, + Value: kv.Value, + ParamsType: "string", + IsCredential: kv.IsCredential, + }) + } for _, kv := range commonutil.BuildPayloadVariables(w.HookPayload.RawPayload) { resp = append(resp, &commonmodels.Param{ Name: kv.Key, @@ -527,6 +535,66 @@ func (w *Workflow) GetWorkflowParamDynamicValues(taskID int64, creator, account, return nil, fmt.Errorf("workflow param %s not found", key) } +func buildRuntimeReferableVariables(workflow *commonmodels.WorkflowV4) []*commonmodels.KeyVal { + resp := make([]*commonmodels.KeyVal, 0) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.creator", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.creator.id", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.creator.userId", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.is_release_plan_trigger", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.timestamp", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.datetime", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.id", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.url", + Value: workflow.Name, + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.branch", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.target_branch", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.pr", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_id", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_message", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.committer", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.event", Type: "string", IsCredential: false}) + return resp +} + func (w *Workflow) Validate(isExecution bool) error { if w.Project == "" { err := fmt.Errorf("project should not be empty") @@ -812,61 +880,7 @@ func (w *Workflow) GetReferableVariables(currentJobName string, option GetWorkfl }) if option.GetRuntimeVariables { - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.creator", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.creator.id", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.creator.userId", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.is_release_plan_trigger", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.timestamp", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.datetime", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.id", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.url", - Value: w.workflowID(), - Type: "string", - IsCredential: false, - }) + resp = append(resp, buildRuntimeReferableVariables(w.WorkflowV4)...) } for _, param := range w.Params { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go index faff2421f8..a7d903606c 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go @@ -2619,6 +2619,13 @@ func getDefaultVars(workflow *commonmodels.WorkflowV4, currentJobName string) [] vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.task.timestamp")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.task.datetime")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.task.id")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.branch")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.target_branch")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.pr")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_id")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_message")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.committer")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.event")) for _, param := range workflow.Params { if param.ParamsType == "repo" || param.ParamsType == "file" { continue From c9da4696a4c2eac0855b4a6091c41b4f2e001a00 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 16 Jun 2026 18:18:30 +0800 Subject: [PATCH 10/22] feat(workflow): add commit sha runtime variable Signed-off-by: huanghongbo-hhb --- .../core/common/repository/models/workflow.go | 1 + .../core/common/util/workflow_variables.go | 3 +- .../service/webhook/gerrit_workflowv4_task.go | 4 +++ .../service/webhook/gitee_workflowv4_task.go | 12 ++++--- .../service/webhook/github_workflowv4_task.go | 36 ++++++++++--------- .../service/webhook/gitlab_workflowv4_task.go | 12 ++++--- .../service/workflow/controller/workflow.go | 1 + .../workflow/service/workflow/workflow_v4.go | 1 + 8 files changed, 42 insertions(+), 28 deletions(-) diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow.go b/pkg/microservice/aslan/core/common/repository/models/workflow.go index 12f7118a85..57d56b7845 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -491,6 +491,7 @@ type HookPayload struct { CheckRunID int64 `bson:"check_run_id" json:"check_run_id,omitempty"` MergeRequestID string `bson:"merge_request_id" json:"merge_request_id,omitempty"` CommitID string `bson:"commit_id" json:"commit_id,omitempty"` + CommitSHA string `bson:"commit_sha" json:"commit_sha,omitempty"` CommitMessage string `bson:"commit_message" json:"commit_message,omitempty"` Committer string `bson:"committer" json:"committer,omitempty"` DeliveryID string `bson:"delivery_id" json:"delivery_id,omitempty"` diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go index 22c35f96f6..96c4369fa1 100644 --- a/pkg/microservice/aslan/core/common/util/workflow_variables.go +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -103,7 +103,7 @@ func BuildWorkflowTriggerVariableKVs(hookPayload *commonmodels.HookPayload) []*c return nil } - resp := make([]*commonmodels.KeyVal, 0, 7) + resp := make([]*commonmodels.KeyVal, 0, 8) appendIfNotEmpty := func(key, value string) { if value == "" { return @@ -115,6 +115,7 @@ func BuildWorkflowTriggerVariableKVs(hookPayload *commonmodels.HookPayload) []*c appendIfNotEmpty("workflow.trigger.target_branch", hookPayload.TargetBranch) appendIfNotEmpty("workflow.trigger.pr", hookPayload.MergeRequestID) appendIfNotEmpty("workflow.trigger.commit_id", hookPayload.CommitID) + appendIfNotEmpty("workflow.trigger.commit_sha", hookPayload.CommitSHA) appendIfNotEmpty("workflow.trigger.commit_message", hookPayload.CommitMessage) appendIfNotEmpty("workflow.trigger.committer", hookPayload.Committer) appendIfNotEmpty("workflow.trigger.event", hookPayload.EventType) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go index c63ad2579f..aaf293761a 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go @@ -313,6 +313,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba eventRepo := matcher.GetHookRepo(item.MainRepo) var mergeRequestID, commitID string + var commitSHA string switch m := matcher.(type) { case *gerritPatchsetCreatedEventMatcherForWorkflowV4: if item.CheckPatchSetChange { @@ -322,6 +323,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba mergeRequestID = strconv.Itoa(m.Event.Change.Number) commitID = strconv.Itoa(m.Event.PatchSet.Number) + commitSHA = m.Event.PatchSet.Revision autoCancelOpt := &AutoCancelOpt{ MergeRequestID: mergeRequestID, CommitID: commitID, @@ -348,6 +350,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba case *gerritChangeMergedEventMatcherForWorkflowV4: mergeRequestID = strconv.Itoa(m.Event.Change.Number) commitID = eventRepo.CommitID + commitSHA = m.Event.NewRev } hookPayload = &commonmodels.HookPayload{ Owner: eventRepo.RepoOwner, @@ -358,6 +361,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba CodehostID: item.MainRepo.CodehostID, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitSHA, CommitMessage: eventRepo.CommitMessage, Committer: eventRepo.Committer, EventType: event.Type, diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go index 3522e95d7b..285ef503b5 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go @@ -285,6 +285,7 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, CommitMessage: ev.PullRequest.Title, Committer: ev.PullRequest.User.Login, EventType: eventType, @@ -306,6 +307,7 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CommitMessage: eventRepo.CommitMessage, Committer: eventRepo.Committer, EventType: eventType, @@ -314,11 +316,11 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque case *gitee.TagPushEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - Branch: eventRepo.Branch, - TargetBranch: eventRepo.TargetBranch, - Committer: eventRepo.Committer, - EventType: eventType, - RawPayload: rawPayload, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go index 57adabc14d..4895a8c39b 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go @@ -283,6 +283,7 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deli DeliveryID: deliveryID, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, CommitMessage: *ev.PullRequest.Title, Committer: *ev.PullRequest.User.Login, EventType: eventType, @@ -297,29 +298,30 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deli autoCancelOpt.Ref = ref autoCancelOpt.CommitID = commitID hookPayload = &commonmodels.HookPayload{ - Owner: *ev.Repo.Owner.Login, - Repo: *ev.Repo.Name, - Branch: getBranchFromRef(ref), - TargetBranch: getBranchFromRef(ref), - Ref: ref, - IsPr: false, - CodehostID: item.MainRepo.CodehostID, - DeliveryID: deliveryID, - CommitID: commitID, + Owner: *ev.Repo.Owner.Login, + Repo: *ev.Repo.Name, + Branch: getBranchFromRef(ref), + TargetBranch: getBranchFromRef(ref), + Ref: ref, + IsPr: false, + CodehostID: item.MainRepo.CodehostID, + DeliveryID: deliveryID, + CommitID: commitID, + CommitSHA: commitID, CommitMessage: ev.GetHeadCommit().GetMessage(), - Committer: ev.GetPusher().GetName(), - EventType: eventType, - RawPayload: rawPayload, + Committer: ev.GetPusher().GetName(), + EventType: eventType, + RawPayload: rawPayload, } } case *github.CreateEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - Branch: item.MainRepo.Branch, - TargetBranch: item.MainRepo.Branch, - Committer: item.MainRepo.Committer, - EventType: eventType, - RawPayload: rawPayload, + Branch: item.MainRepo.Branch, + TargetBranch: item.MainRepo.Branch, + Committer: item.MainRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go index 8458ced3b9..5f69afcb63 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go @@ -376,6 +376,7 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requ IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, CommitMessage: ev.ObjectAttributes.Title, Committer: ev.User.Username, CodehostID: eventRepo.CodehostID, @@ -397,6 +398,7 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requ Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CommitMessage: eventRepo.CommitMessage, Committer: eventRepo.Committer, CodehostID: eventRepo.CodehostID, @@ -406,11 +408,11 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requ case *gitlab.TagEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - Branch: eventRepo.Branch, - TargetBranch: eventRepo.TargetBranch, - Committer: eventRepo.Committer, - EventType: eventType, - RawPayload: rawPayload, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index b01249bdda..8c274c9863 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -589,6 +589,7 @@ func buildRuntimeReferableVariables(workflow *commonmodels.WorkflowV4) []*common resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.target_branch", Type: "string", IsCredential: false}) resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.pr", Type: "string", IsCredential: false}) resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_id", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_sha", Type: "string", IsCredential: false}) resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_message", Type: "string", IsCredential: false}) resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.committer", Type: "string", IsCredential: false}) resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.event", Type: "string", IsCredential: false}) diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go index a7d903606c..5ede3a77ba 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go @@ -2623,6 +2623,7 @@ func getDefaultVars(workflow *commonmodels.WorkflowV4, currentJobName string) [] vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.target_branch")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.pr")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_id")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_sha")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_message")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.committer")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.event")) From 714a28bbf25956c7421f5ccdb921e8f0e044b2c0 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 17 Jun 2026 11:23:08 +0800 Subject: [PATCH 11/22] fix trigger vars and lark hook recipients Signed-off-by: huanghongbo-hhb --- .../common/repository/models/workflow_v4.go | 1 + .../jobcontroller/job_notification.go | 10 ++- .../job_notification_dynamic_recipient.go | 57 +++++++++++-- .../core/common/util/workflow_variables.go | 81 +++++++++---------- .../service/webhook/gitee_testing_task.go | 2 + .../service/webhook/github_scanning_task.go | 2 + .../service/webhook/github_testing_task.go | 2 + .../service/webhook/gitlab_scanning_task.go | 2 + .../service/webhook/gitlab_testing_task.go | 2 + .../controller/job/job_notification.go | 11 +++ .../service/workflow/controller/workflow.go | 8 -- .../service/workflow/workflow_task_v4.go | 24 +++--- 12 files changed, 132 insertions(+), 70 deletions(-) diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go index a946ada75a..4db3aae9ad 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1290,6 +1290,7 @@ type LarkPersonNotificationConfig struct { } type LarkHookNotificationConfig struct { + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index 7154d19a23..86f675b6f9 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -270,11 +270,17 @@ func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) resolver := newDynamicRecipientResolver(keyMap) if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { - users, err := resolver.resolveDirectValues([]string(cfg.DynamicRecipients), dynamicRecipientKindUserID) + users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) if err != nil { return err } - cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + cfg.AtUsers = append(cfg.AtUsers, user.ID) + } + cfg.AtUsers = lo.Uniq(cfg.AtUsers) } if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil { users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go index c05195d232..2ae4c20d1a 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go @@ -40,7 +40,10 @@ var supportedDynamicRecipientKinds = map[setting.NotifyWebHookType]map[dynamicRe dynamicRecipientKindOpenID: {}, }, setting.NotifyWebHookTypeFeishu: { - dynamicRecipientKindUserID: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + dynamicRecipientKindUserID: {}, }, setting.NotifyWebHookTypeWechatWork: { dynamicRecipientKindUserID: {}, @@ -107,6 +110,27 @@ func ValidateDynamicRecipientsForNotifyType(notifyType setting.NotifyWebHookType return nil } +func ValidateDynamicRecipientsForNotifyConfig(notifyType setting.NotifyWebHookType, appID string, recipients []string) error { + if err := ValidateDynamicRecipientsForNotifyType(notifyType, recipients); err != nil { + return err + } + if notifyType != setting.NotifyWebHookTypeFeishu || strings.TrimSpace(appID) != "" { + return nil + } + + for _, recipient := range recipients { + spec, err := parseDynamicRecipient(recipient) + if err != nil { + return err + } + if spec.kind != dynamicRecipientKindUserID { + return fmt.Errorf("dynamic recipient %s requires app_id for notification type %s", recipient, notifyType) + } + } + + return nil +} + func newDynamicRecipientResolver(keyMap map[string]string) *dynamicRecipientResolver { return &dynamicRecipientResolver{ keyMap: keyMap, @@ -238,12 +262,23 @@ func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID s return nil, nil } - client, err := r.getLarkClient(appID) - if err != nil { - return nil, err + resp := make([]*larktool.UserInfo, 0) + var client *larktool.Client + getClient := func() (*larktool.Client, error) { + if client != nil { + return client, nil + } + if strings.TrimSpace(appID) == "" { + return nil, fmt.Errorf("app_id is required to resolve lark dynamic recipients by email/mobile/account") + } + var err error + client, err = r.getLarkClient(appID) + if err != nil { + return nil, err + } + return client, nil } - resp := make([]*larktool.UserInfo, 0) for _, recipient := range recipients { spec, value, ok, err := r.resolveRecipient(recipient) if err != nil { @@ -262,6 +297,10 @@ func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID s } resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserOpenID}) case dynamicRecipientKindEmail: + client, err := getClient() + if err != nil { + return nil, err + } ids, err := r.resolveLarkUserIDsByEmail(client, appID, value) if err != nil { return nil, err @@ -270,6 +309,10 @@ func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID s resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) } case dynamicRecipientKindMobile: + client, err := getClient() + if err != nil { + return nil, err + } ids, err := r.resolveLarkUserIDsByPhone(client, appID, value) if err != nil { return nil, err @@ -278,6 +321,10 @@ func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID s resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) } case dynamicRecipientKindAccount: + client, err := getClient() + if err != nil { + return nil, err + } ids, err := r.resolveLarkUserIDsByAccount(client, appID, value) if err != nil { return nil, err diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go index 96c4369fa1..c2fef1505f 100644 --- a/pkg/microservice/aslan/core/common/util/workflow_variables.go +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "net/url" - "strconv" + "regexp" "strings" "time" @@ -12,44 +12,6 @@ import ( commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" ) -func BuildPayloadVariables(rawPayload string) []*commonmodels.KeyVal { - if rawPayload == "" { - return nil - } - - var payload interface{} - if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil { - return nil - } - - resp := make([]*commonmodels.KeyVal, 0) - flattenPayloadValue("payload", payload, &resp) - return resp -} - -func flattenPayloadValue(prefix string, value interface{}, resp *[]*commonmodels.KeyVal) { - switch val := value.(type) { - case map[string]interface{}: - for key, item := range val { - flattenPayloadValue(prefix+"."+key, item, resp) - } - case []interface{}: - for index, item := range val { - flattenPayloadValue(fmt.Sprintf("%s.%d", prefix, index), item, resp) - } - case string: - *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: val, IsCredential: false}) - case float64: - *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatFloat(val, 'f', -1, 64), IsCredential: false}) - case bool: - *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatBool(val), IsCredential: false}) - case nil: - return - default: - *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: fmt.Sprint(val), IsCredential: false}) - } -} - func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { if workflow == nil { return nil @@ -92,7 +54,6 @@ func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectNa } if workflow.HookPayload != nil { resp = append(resp, BuildWorkflowTriggerVariableKVs(workflow.HookPayload)...) - resp = append(resp, BuildPayloadVariables(workflow.HookPayload.RawPayload)...) } return resp @@ -115,7 +76,7 @@ func BuildWorkflowTriggerVariableKVs(hookPayload *commonmodels.HookPayload) []*c appendIfNotEmpty("workflow.trigger.target_branch", hookPayload.TargetBranch) appendIfNotEmpty("workflow.trigger.pr", hookPayload.MergeRequestID) appendIfNotEmpty("workflow.trigger.commit_id", hookPayload.CommitID) - appendIfNotEmpty("workflow.trigger.commit_sha", hookPayload.CommitSHA) + appendIfNotEmpty("workflow.trigger.commit_sha", inferWorkflowTriggerCommitSHA(hookPayload)) appendIfNotEmpty("workflow.trigger.commit_message", hookPayload.CommitMessage) appendIfNotEmpty("workflow.trigger.committer", hookPayload.Committer) appendIfNotEmpty("workflow.trigger.event", hookPayload.EventType) @@ -123,6 +84,44 @@ func BuildWorkflowTriggerVariableKVs(hookPayload *commonmodels.HookPayload) []*c return resp } +var commitSHARegex = regexp.MustCompile(`^[0-9a-fA-F]{40}$`) + +func inferWorkflowTriggerCommitSHA(hookPayload *commonmodels.HookPayload) string { + if hookPayload == nil { + return "" + } + if hookPayload.CommitSHA != "" { + return hookPayload.CommitSHA + } + if commitSHARegex.MatchString(hookPayload.CommitID) { + return hookPayload.CommitID + } + + type patchSetPayload struct { + Revision string `json:"revision"` + } + type gerritPayload struct { + NewRev string `json:"newRev"` + PatchSet patchSetPayload `json:"patchSet"` + } + + if hookPayload.RawPayload == "" { + return "" + } + + payload := new(gerritPayload) + if err := json.Unmarshal([]byte(hookPayload.RawPayload), payload); err != nil { + return "" + } + if commitSHARegex.MatchString(payload.NewRev) { + return payload.NewRev + } + if commitSHARegex.MatchString(payload.PatchSet.Revision) { + return payload.PatchSet.Revision + } + return "" +} + func BuildWorkflowRuntimeVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { return BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now) } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go index 3cd08ad6d7..f8546c59c9 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go @@ -280,6 +280,7 @@ func TriggerTestByGiteeEvent(event interface{}, baseURI, requestID string, log * IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *gitee.PushEvent: @@ -298,6 +299,7 @@ func TriggerTestByGiteeEvent(event interface{}, baseURI, requestID string, log * Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *gitee.TagPushEvent: diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go index 97ac352835..73ba35b070 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go @@ -95,6 +95,7 @@ func TriggerScanningByGithubEvent(event interface{}, requestID string, log *zap. CodehostID: mainRepo.CodehostID, MergeRequestID: strconv.Itoa(mergeRequestID), CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *github.PushEvent: @@ -112,6 +113,7 @@ func TriggerScanningByGithubEvent(event interface{}, requestID string, log *zap. Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, CodehostID: mainRepo.CodehostID, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go index 0f03e48546..3d41514b1e 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go @@ -86,6 +86,7 @@ func TriggerTestByGithubEvent(event interface{}, requestID string, log *zap.Suga CodehostID: item.MainRepo.CodehostID, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *github.PushEvent: @@ -103,6 +104,7 @@ func TriggerTestByGithubEvent(event interface{}, requestID string, log *zap.Suga Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, CodehostID: item.MainRepo.CodehostID, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go index 8544abe263..712f0f294f 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go @@ -92,6 +92,7 @@ func TriggerScanningByGitlabEvent(event interface{}, baseURI, requestID string, IsPr: true, MergeRequestID: strconv.Itoa(mergeRequestID), CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } @@ -110,6 +111,7 @@ func TriggerScanningByGitlabEvent(event interface{}, baseURI, requestID string, Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go index 43e735e4ad..1ad3a22ae3 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go @@ -215,6 +215,7 @@ func TriggerTestByGitlabEvent(event interface{}, baseURI, requestID string, log IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } @@ -233,6 +234,7 @@ func TriggerTestByGitlabEvent(event interface{}, baseURI, requestID string, log Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go index 10b208235d..8d27b58c20 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go @@ -22,7 +22,9 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + runtimeJobController "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" + "github.com/koderover/zadig/v2/pkg/setting" e "github.com/koderover/zadig/v2/pkg/tool/errors" "github.com/koderover/zadig/v2/pkg/types" ) @@ -65,6 +67,15 @@ func (j NotificationJobController) Validate(isExecution bool) error { if err := util.CheckZadigProfessionalLicense(); err != nil { return e.ErrLicenseInvalid.AddDesc("") } + if j.jobSpec.WebHookType == setting.NotifyWebHookTypeFeishu && j.jobSpec.LarkHookNotificationConfig != nil { + if err := runtimeJobController.ValidateDynamicRecipientsForNotifyConfig( + j.jobSpec.WebHookType, + j.jobSpec.LarkHookNotificationConfig.AppID, + []string(j.jobSpec.LarkHookNotificationConfig.DynamicRecipients), + ); err != nil { + return e.ErrLintWorkflow.AddDesc(err.Error()) + } + } return nil } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index 8c274c9863..3792b6c1b8 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -408,14 +408,6 @@ func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid IsCredential: kv.IsCredential, }) } - for _, kv := range commonutil.BuildPayloadVariables(w.HookPayload.RawPayload) { - resp = append(resp, &commonmodels.Param{ - Name: kv.Key, - Value: kv.Value, - ParamsType: "string", - IsCredential: kv.IsCredential, - }) - } } return resp, nil } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index 19cd6535b8..7e0ec8bed7 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -741,7 +741,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea notifyInputsMap[notifyInput.ID] = notifyInput } - toDynamicRecipients := func(notifyType setting.NotifyWebHookType, inputs []string) ([]string, error) { + toDynamicRecipients := func(notifyType setting.NotifyWebHookType, appID string, inputs []string) ([]string, error) { resp := make([]string, 0, len(inputs)) for _, input := range inputs { input = strings.TrimSpace(input) @@ -750,7 +750,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } resp = append(resp, input) } - if err := runtimeJobController.ValidateDynamicRecipientsForNotifyType(notifyType, resp); err != nil { + if err := runtimeJobController.ValidateDynamicRecipientsForNotifyConfig(notifyType, appID, resp); err != nil { return nil, err } return resp, nil @@ -769,12 +769,13 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark hook notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.LarkHookNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyCtl.LarkHookNotificationConfig.AppID, notifyInput.LarkHookNotificationConfig.DynamicRecipients) if err != nil { return nil, err } config := &commonmodels.LarkHookNotificationConfig{ + AppID: notifyCtl.LarkHookNotificationConfig.AppID, HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress, AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers, DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), @@ -787,7 +788,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark person notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.LarkPersonNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyCtl.LarkPersonNotificationConfig.AppID, notifyInput.LarkPersonNotificationConfig.DynamicRecipients) if err != nil { return nil, err } @@ -814,7 +815,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark group notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.LarkGroupNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyCtl.LarkGroupNotificationConfig.AppID, notifyInput.LarkGroupNotificationConfig.DynamicRecipients) if err != nil { return nil, err } @@ -842,7 +843,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("wechat notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.WechatNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.WechatNotificationConfig.DynamicRecipients) if err != nil { return nil, err } @@ -860,7 +861,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("dingding notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.DingDingNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.DingDingNotificationConfig.DynamicRecipients) if err != nil { return nil, err } @@ -878,7 +879,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("msteams notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.MSTeamsNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.MSTeamsNotificationConfig.DynamicRecipients) if err != nil { return nil, err } @@ -895,7 +896,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("mail notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } - dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyInput.MailNotificationConfig.DynamicRecipients) + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.MailNotificationConfig.DynamicRecipients) if err != nil { return nil, err } @@ -962,11 +963,6 @@ func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string )) for key, value := range keyMap { - // Payload variables are resolved at task creation time and stored in RawPayload; - // they don't need to be persisted in GlobalContext (which would duplicate them in MongoDB). - if strings.HasPrefix(key, "payload.") { - continue - } resp[runtimeWorkflowController.GetContextKey(fmt.Sprintf("{{.%s}}", key))] = value } return resp From 68566c342de5870b6cd3dff58a45560f6d91cc08 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 17 Jun 2026 11:41:12 +0800 Subject: [PATCH 12/22] fix payload scope for workflow runtime Signed-off-by: huanghongbo-hhb --- .../jobcontroller/job_notification.go | 8 +++- .../core/common/util/workflow_variables.go | 40 +++++++++++++++++++ .../service/workflow/controller/workflow.go | 8 ++++ .../service/workflow/workflow_task_v4.go | 5 +++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index 86f675b6f9..eae4e2afc9 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -246,7 +246,13 @@ func (c *NotificationJobCtl) prepareRuntimeNotificationFields() error { } func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string { - return util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) + keyMap := util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) + for key := range keyMap { + if strings.HasPrefix(key, "payload.") { + delete(keyMap, key) + } + } + return keyMap } func renderNotificationStrings(inputs []string, keyMap map[string]string) []string { diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go index c2fef1505f..8dc18e17e0 100644 --- a/pkg/microservice/aslan/core/common/util/workflow_variables.go +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "regexp" + "strconv" "strings" "time" @@ -12,6 +13,44 @@ import ( commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" ) +func BuildPayloadVariables(rawPayload string) []*commonmodels.KeyVal { + if rawPayload == "" { + return nil + } + + var payload interface{} + if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil { + return nil + } + + resp := make([]*commonmodels.KeyVal, 0) + flattenPayloadValue("payload", payload, &resp) + return resp +} + +func flattenPayloadValue(prefix string, value interface{}, resp *[]*commonmodels.KeyVal) { + switch val := value.(type) { + case map[string]interface{}: + for key, item := range val { + flattenPayloadValue(prefix+"."+key, item, resp) + } + case []interface{}: + for index, item := range val { + flattenPayloadValue(fmt.Sprintf("%s.%d", prefix, index), item, resp) + } + case string: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: val, IsCredential: false}) + case float64: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatFloat(val, 'f', -1, 64), IsCredential: false}) + case bool: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatBool(val), IsCredential: false}) + case nil: + return + default: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: fmt.Sprint(val), IsCredential: false}) + } +} + func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { if workflow == nil { return nil @@ -54,6 +93,7 @@ func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectNa } if workflow.HookPayload != nil { resp = append(resp, BuildWorkflowTriggerVariableKVs(workflow.HookPayload)...) + resp = append(resp, BuildPayloadVariables(workflow.HookPayload.RawPayload)...) } return resp diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index 3792b6c1b8..8c274c9863 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -408,6 +408,14 @@ func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid IsCredential: kv.IsCredential, }) } + for _, kv := range commonutil.BuildPayloadVariables(w.HookPayload.RawPayload) { + resp = append(resp, &commonmodels.Param{ + Name: kv.Key, + Value: kv.Value, + ParamsType: "string", + IsCredential: kv.IsCredential, + }) + } } return resp, nil } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index 7e0ec8bed7..178da98483 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -963,6 +963,11 @@ func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string )) for key, value := range keyMap { + // Payload variables are resolved from HookPayload.RawPayload on demand; + // they don't need to be duplicated into GlobalContext. + if strings.HasPrefix(key, "payload.") { + continue + } resp[runtimeWorkflowController.GetContextKey(fmt.Sprintf("{{.%s}}", key))] = value } return resp From acf362ee9dc38e647733fac6a27dcc641c7eccf9 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 17 Jun 2026 16:14:37 +0800 Subject: [PATCH 13/22] fix: allow payload variables in notification recipients Signed-off-by: huanghongbo-hhb --- .../jobcontroller/job_notification.go | 15 ++- .../jobcontroller/job_notification_test.go | 94 +++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index eae4e2afc9..29050d4577 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -226,23 +226,24 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { func (c *NotificationJobCtl) prepareRuntimeNotificationFields() error { keyMap := c.buildRuntimeNotificationKeyMap() + recipientKeyMap := c.buildRuntimeNotificationRecipientKeyMap() c.jobTaskSpec.Title = renderNotificationString(c.jobTaskSpec.Title, keyMap) c.jobTaskSpec.Content = renderNotificationString(c.jobTaskSpec.Content, keyMap) if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { - cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, keyMap) + cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, recipientKeyMap) } if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { - cfg.AtMobiles = renderNotificationStrings(cfg.AtMobiles, keyMap) + cfg.AtMobiles = renderNotificationStrings(cfg.AtMobiles, recipientKeyMap) } if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { - cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, keyMap) + cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, recipientKeyMap) } if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { - cfg.AtEmails = renderNotificationStrings(cfg.AtEmails, keyMap) + cfg.AtEmails = renderNotificationStrings(cfg.AtEmails, recipientKeyMap) } - return c.resolveDynamicRecipients(keyMap) + return c.resolveDynamicRecipients(recipientKeyMap) } func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string { @@ -255,6 +256,10 @@ func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string return keyMap } +func (c *NotificationJobCtl) buildRuntimeNotificationRecipientKeyMap() map[string]string { + return util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) +} + func renderNotificationStrings(inputs []string, keyMap map[string]string) []string { if len(keyMap) == 0 { return inputs diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go new file mode 100644 index 0000000000..daa6428319 --- /dev/null +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go @@ -0,0 +1,94 @@ +package jobcontroller + +import ( + "testing" + + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + "github.com/koderover/zadig/v2/pkg/setting" +) + +func TestPrepareRuntimeNotificationFieldsSupportsPayloadDynamicRecipients(t *testing.T) { + ctl := &NotificationJobCtl{ + workflowCtx: &commonmodels.WorkflowTaskCtx{ + WorkflowKeyVals: []*commonmodels.KeyVal{ + {Key: "payload.user.email", Value: "dev@example.com"}, + }, + }, + jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ + WebHookType: setting.NotifyWebHookTypeMail, + MailNotificationConfig: &commonmodels.MailNotificationConfig{ + DynamicRecipients: commonmodels.DynamicRecipients{ + "{{.payload.user.email}}", + }, + }, + }, + } + + if err := ctl.prepareRuntimeNotificationFields(); err != nil { + t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) + } + + if len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers) != 1 { + t.Fatalf("expected 1 target user, got %d", len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers)) + } + + got := ctl.jobTaskSpec.MailNotificationConfig.TargetUsers[0] + if got == nil || got.Type != "email" || got.UserName != "dev@example.com" { + t.Fatalf("expected resolved payload email target user, got %#v", got) + } +} + +func TestPrepareRuntimeNotificationFieldsSupportsPayloadStaticRecipients(t *testing.T) { + ctl := &NotificationJobCtl{ + workflowCtx: &commonmodels.WorkflowTaskCtx{ + WorkflowKeyVals: []*commonmodels.KeyVal{ + {Key: "payload.reviewer.email", Value: "reviewer@example.com"}, + }, + }, + jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ + WebHookType: setting.NotifyWebHookTypeMSTeam, + MSTeamsNotificationConfig: &commonmodels.MSTeamsNotificationConfig{ + AtEmails: []string{"{{.payload.reviewer.email}}"}, + }, + }, + } + + if err := ctl.prepareRuntimeNotificationFields(); err != nil { + t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) + } + + if len(ctl.jobTaskSpec.MSTeamsNotificationConfig.AtEmails) != 1 { + t.Fatalf("expected 1 rendered email, got %d", len(ctl.jobTaskSpec.MSTeamsNotificationConfig.AtEmails)) + } + + if got := ctl.jobTaskSpec.MSTeamsNotificationConfig.AtEmails[0]; got != "reviewer@example.com" { + t.Fatalf("expected rendered payload email, got %q", got) + } +} + +func TestPrepareRuntimeNotificationFieldsDoesNotRenderPayloadInTitleOrContent(t *testing.T) { + ctl := &NotificationJobCtl{ + workflowCtx: &commonmodels.WorkflowTaskCtx{ + WorkflowKeyVals: []*commonmodels.KeyVal{ + {Key: "payload.user.email", Value: "dev@example.com"}, + {Key: "workflow.trigger.branch", Value: "feature/demo"}, + }, + }, + jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ + Title: "branch={{.workflow.trigger.branch}} payload={{.payload.user.email}}", + Content: "branch={{.workflow.trigger.branch}} payload={{.payload.user.email}}", + }, + } + + if err := ctl.prepareRuntimeNotificationFields(); err != nil { + t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) + } + + want := "branch=feature/demo payload={{.payload.user.email}}" + if ctl.jobTaskSpec.Title != want { + t.Fatalf("expected title %q, got %q", want, ctl.jobTaskSpec.Title) + } + if ctl.jobTaskSpec.Content != want { + t.Fatalf("expected content %q, got %q", want, ctl.jobTaskSpec.Content) + } +} From 83903fe037ad24c17938e81d4039c0ce849cb204 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 18 Jun 2026 10:41:15 +0800 Subject: [PATCH 14/22] fix: preserve notification dynamic recipients runtime rendering Signed-off-by: huanghongbo-hhb --- .../jobcontroller/job_notification_test.go | 35 +++ .../service/workflow/controller/workflow.go | 285 ++++++++++++++++-- .../workflow/controller/workflow_test.go | 176 +++++++++++ .../service/workflow/workflow_task_v4.go | 22 +- 4 files changed, 483 insertions(+), 35 deletions(-) create mode 100644 pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go index daa6428319..042e7cdae8 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go @@ -92,3 +92,38 @@ func TestPrepareRuntimeNotificationFieldsDoesNotRenderPayloadInTitleOrContent(t t.Fatalf("expected content %q, got %q", want, ctl.jobTaskSpec.Content) } } + +func TestPrepareRuntimeNotificationFieldsRendersWorkflowTriggerInTitle(t *testing.T) { + ctl := &NotificationJobCtl{ + workflowCtx: &commonmodels.WorkflowTaskCtx{ + WorkflowKeyVals: []*commonmodels.KeyVal{ + {Key: "workflow.trigger.branch", Value: "feature/demo"}, + {Key: "payload.commits.0.author.email", Value: "dev@example.com"}, + }, + }, + jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ + WebHookType: setting.NotifyWebHookTypeMail, + Title: "notify {{.workflow.trigger.branch}}", + MailNotificationConfig: &commonmodels.MailNotificationConfig{ + DynamicRecipients: commonmodels.DynamicRecipients{"{{.payload.commits.0.author.email}}"}, + }, + }, + } + + if err := ctl.prepareRuntimeNotificationFields(); err != nil { + t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) + } + + if ctl.jobTaskSpec.Title != "notify feature/demo" { + t.Fatalf("expected rendered title, got %q", ctl.jobTaskSpec.Title) + } + + if len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers) != 1 { + t.Fatalf("expected 1 resolved target user, got %d", len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers)) + } + + got := ctl.jobTaskSpec.MailNotificationConfig.TargetUsers[0] + if got == nil || got.Type != "email" || got.UserName != "dev@example.com" { + t.Fatalf("expected runtime dynamic recipient to resolve to email target user, got %#v", got) + } +} diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index 8c274c9863..eca5b95cea 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -48,6 +48,264 @@ func CreateWorkflowController(wf *commonmodels.WorkflowV4) *Workflow { return &Workflow{wf} } +type notificationDynamicRecipients struct { + LarkHook commonmodels.DynamicRecipients + LarkGroup commonmodels.DynamicRecipients + LarkPerson commonmodels.DynamicRecipients + Wechat commonmodels.DynamicRecipients + DingDing commonmodels.DynamicRecipients + MSTeams commonmodels.DynamicRecipients + Mail commonmodels.DynamicRecipients +} + +type workflowNotificationSpecBackup struct { + StageIndex int + JobIndex int + Recipients *notificationDynamicRecipients +} + +func cloneDynamicRecipients(items commonmodels.DynamicRecipients) commonmodels.DynamicRecipients { + if items == nil { + return nil + } + resp := make(commonmodels.DynamicRecipients, len(items)) + copy(resp, items) + return resp +} + +func backupNotificationDynamicRecipients( + larkHook *commonmodels.LarkHookNotificationConfig, + larkGroup *commonmodels.LarkGroupNotificationConfig, + larkPerson *commonmodels.LarkPersonNotificationConfig, + wechat *commonmodels.WechatNotificationConfig, + dingDing *commonmodels.DingDingNotificationConfig, + msTeams *commonmodels.MSTeamsNotificationConfig, + mail *commonmodels.MailNotificationConfig, +) *notificationDynamicRecipients { + resp := ¬ificationDynamicRecipients{} + if larkHook != nil { + resp.LarkHook = cloneDynamicRecipients(larkHook.DynamicRecipients) + } + if larkGroup != nil { + resp.LarkGroup = cloneDynamicRecipients(larkGroup.DynamicRecipients) + } + if larkPerson != nil { + resp.LarkPerson = cloneDynamicRecipients(larkPerson.DynamicRecipients) + } + if wechat != nil { + resp.Wechat = cloneDynamicRecipients(wechat.DynamicRecipients) + } + if dingDing != nil { + resp.DingDing = cloneDynamicRecipients(dingDing.DynamicRecipients) + } + if msTeams != nil { + resp.MSTeams = cloneDynamicRecipients(msTeams.DynamicRecipients) + } + if mail != nil { + resp.Mail = cloneDynamicRecipients(mail.DynamicRecipients) + } + return resp +} + +func restoreNotificationDynamicRecipients( + recipients *notificationDynamicRecipients, + larkHook *commonmodels.LarkHookNotificationConfig, + larkGroup *commonmodels.LarkGroupNotificationConfig, + larkPerson *commonmodels.LarkPersonNotificationConfig, + wechat *commonmodels.WechatNotificationConfig, + dingDing *commonmodels.DingDingNotificationConfig, + msTeams *commonmodels.MSTeamsNotificationConfig, + mail *commonmodels.MailNotificationConfig, +) { + if recipients == nil { + return + } + if larkHook != nil { + larkHook.DynamicRecipients = cloneDynamicRecipients(recipients.LarkHook) + } + if larkGroup != nil { + larkGroup.DynamicRecipients = cloneDynamicRecipients(recipients.LarkGroup) + } + if larkPerson != nil { + larkPerson.DynamicRecipients = cloneDynamicRecipients(recipients.LarkPerson) + } + if wechat != nil { + wechat.DynamicRecipients = cloneDynamicRecipients(recipients.Wechat) + } + if dingDing != nil { + dingDing.DynamicRecipients = cloneDynamicRecipients(recipients.DingDing) + } + if msTeams != nil { + msTeams.DynamicRecipients = cloneDynamicRecipients(recipients.MSTeams) + } + if mail != nil { + mail.DynamicRecipients = cloneDynamicRecipients(recipients.Mail) + } +} + +func backupNotificationDynamicRecipientsFromWorkflowSpec(spec *commonmodels.NotificationJobSpec) *notificationDynamicRecipients { + if spec == nil { + return nil + } + return backupNotificationDynamicRecipients( + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func restoreNotificationDynamicRecipientsToWorkflowSpec(spec *commonmodels.NotificationJobSpec, recipients *notificationDynamicRecipients) { + if spec == nil || recipients == nil { + return + } + restoreNotificationDynamicRecipients( + recipients, + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func backupWorkflowNotificationRuntimeRenderFields(workflow *commonmodels.WorkflowV4) ([]*workflowNotificationSpecBackup, error) { + if workflow == nil { + return nil, nil + } + + resp := make([]*workflowNotificationSpecBackup, 0) + for stageIndex, stage := range workflow.Stages { + if stage == nil { + continue + } + for jobIndex, job := range stage.Jobs { + if job == nil || job.JobType != config.JobNotification { + continue + } + spec := &commonmodels.NotificationJobSpec{} + if err := commonmodels.IToi(job.Spec, spec); err != nil { + return nil, fmt.Errorf("failed to decode notification job spec for job %s, error: %w", job.Name, err) + } + resp = append(resp, &workflowNotificationSpecBackup{ + StageIndex: stageIndex, + JobIndex: jobIndex, + Recipients: backupNotificationDynamicRecipientsFromWorkflowSpec(spec), + }) + } + } + return resp, nil +} + +func restoreWorkflowNotificationRuntimeRenderFields(workflow *commonmodels.WorkflowV4, backups []*workflowNotificationSpecBackup) error { + if workflow == nil { + return nil + } + + for _, backup := range backups { + if backup == nil || backup.Recipients == nil { + continue + } + if backup.StageIndex >= len(workflow.Stages) || workflow.Stages[backup.StageIndex] == nil { + continue + } + stage := workflow.Stages[backup.StageIndex] + if backup.JobIndex >= len(stage.Jobs) || stage.Jobs[backup.JobIndex] == nil { + continue + } + job := stage.Jobs[backup.JobIndex] + spec := &commonmodels.NotificationJobSpec{} + if err := commonmodels.IToi(job.Spec, spec); err != nil { + return fmt.Errorf("failed to restore notification job spec for job %s, error: %w", job.Name, err) + } + restoreNotificationDynamicRecipientsToWorkflowSpec(spec, backup.Recipients) + job.Spec = spec + } + return nil +} + +func backupNotificationDynamicRecipientsFromTaskSpec(spec *commonmodels.JobTaskNotificationSpec) *notificationDynamicRecipients { + if spec == nil { + return nil + } + return backupNotificationDynamicRecipients( + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func restoreNotificationDynamicRecipientsToTaskSpec(spec *commonmodels.JobTaskNotificationSpec, recipients *notificationDynamicRecipients) { + if spec == nil || recipients == nil { + return + } + restoreNotificationDynamicRecipients( + recipients, + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func RenderJobTaskWithGlobalVariables(task *commonmodels.JobTask, globalKeyMap map[string]string) error { + if task == nil { + return nil + } + + var notificationRecipients *notificationDynamicRecipients + if task.JobType == string(config.JobNotification) { + spec := &commonmodels.JobTaskNotificationSpec{} + if err := commonmodels.IToi(task.Spec, spec); err != nil { + return fmt.Errorf("failed to decode notification task spec for task %s, error: %w", task.Name, err) + } + notificationRecipients = backupNotificationDynamicRecipientsFromTaskSpec(spec) + } + + taskBytes, err := json.Marshal(task) + if err != nil { + return fmt.Errorf("failed to marshal task %s, error: %w", task.Name, err) + } + taskString := string(taskBytes) + for k, v := range globalKeyMap { + // Use json.Marshal to properly escape the value as it would appear in JSON. + escapedValueBytes, _ := json.Marshal(v) + escapedValue := string(escapedValueBytes) + // Remove the surrounding quotes since we're replacing within a JSON string. + escapedValue = strings.Trim(escapedValue, `"`) + + taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), escapedValue) + } + + if err := json.Unmarshal([]byte(taskString), task); err != nil { + return fmt.Errorf("failed to replace input variable for task: %s, error: %w", task.Name, err) + } + + if notificationRecipients == nil { + return nil + } + + spec := &commonmodels.JobTaskNotificationSpec{} + if err := commonmodels.IToi(task.Spec, spec); err != nil { + return fmt.Errorf("failed to restore notification task spec for task %s, error: %w", task.Name, err) + } + restoreNotificationDynamicRecipientsToTaskSpec(spec, notificationRecipients) + task.Spec = spec + return nil +} + func (w *Workflow) SetPreset(ticket *commonmodels.ApprovalTicket) error { for _, stage := range w.Stages { for _, job := range stage.Jobs { @@ -209,22 +467,8 @@ func (w *Workflow) ToJobTasks(taskID int64, creator, account, uid string, releas } for _, task := range tasks { - taskBytes, _ := json.Marshal(task) - taskString := string(taskBytes) - for k, v := range globalKeyMap { - // Use json.Marshal to properly escape the value as it would appear in JSON - escapedValueBytes, _ := json.Marshal(v) - escapedValue := string(escapedValueBytes) - // Remove the surrounding quotes since we're replacing within a JSON string - escapedValue = strings.Trim(escapedValue, `"`) - - taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), escapedValue) - log.Debugf("replacing key %s with value: %s", fmt.Sprintf("{{.%s}}", k), v) - } - - err := json.Unmarshal([]byte(taskString), &task) - if err != nil { - return nil, fmt.Errorf("failed to replace input variable for task: %s, error: %s", task.Name, err) + if err := RenderJobTaskWithGlobalVariables(task, globalKeyMap); err != nil { + return nil, err } } @@ -346,12 +590,19 @@ func (w *Workflow) RenderWorkflowDefaultParams(taskID int64, creator, account, u if err != nil { return fmt.Errorf("marshal workflow error: %v", err) } + notificationBackups, err := backupWorkflowNotificationRuntimeRenderFields(w.WorkflowV4) + if err != nil { + return err + } globalParams, err := w.getWorkflowDefaultParams(taskID, creator, account, uid, releasePlan) if err != nil { return fmt.Errorf("get workflow default params error: %v", err) } replacedString := renderMultiLineString(string(b), globalParams) - return json.Unmarshal([]byte(replacedString), &w.WorkflowV4) + if err := json.Unmarshal([]byte(replacedString), &w.WorkflowV4); err != nil { + return err + } + return restoreWorkflowNotificationRuntimeRenderFields(w.WorkflowV4, notificationBackups) } func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid string, releasePlan *commonmodels.ReleasePlanRef) ([]*commonmodels.Param, error) { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go new file mode 100644 index 0000000000..e4d93f003c --- /dev/null +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go @@ -0,0 +1,176 @@ +package controller + +import ( + "math" + "strings" + "testing" + + "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + "github.com/koderover/zadig/v2/pkg/setting" +) + +func TestBuildRuntimeReferableVariablesIncludesTriggerRuntimeVariables(t *testing.T) { + workflow := &commonmodels.WorkflowV4{ + Name: "workflow-demo", + DisplayName: "Workflow Demo", + } + + variables := buildRuntimeReferableVariables(workflow) + keySet := make(map[string]struct{}, len(variables)) + for _, variable := range variables { + keySet[variable.Key] = struct{}{} + } + + expectedKeys := []string{ + "workflow.task.creator", + "workflow.trigger.branch", + "workflow.trigger.target_branch", + "workflow.trigger.pr", + "workflow.trigger.commit_id", + "workflow.trigger.commit_sha", + "workflow.trigger.commit_message", + "workflow.trigger.committer", + "workflow.trigger.event", + } + + for _, key := range expectedKeys { + if _, ok := keySet[key]; !ok { + t.Fatalf("expected runtime variable %s to be exposed", key) + } + } +} + +func TestRenderJobTaskPreservesNotificationDynamicRecipients(t *testing.T) { + task := &commonmodels.JobTask{ + JobType: string(config.JobNotification), + Spec: &commonmodels.JobTaskNotificationSpec{ + WebHookType: setting.NotifyWebHookTypeMSTeam, + Title: "notify {{.workflow.trigger.branch}}", + Content: "reviewer {{.workflow.params.reviewer}}", + MSTeamsNotificationConfig: &commonmodels.MSTeamsNotificationConfig{ + AtEmails: []string{"{{.workflow.params.reviewer}}"}, + DynamicRecipients: commonmodels.DynamicRecipients{ + "{{.payload.commits.0.author.email}}", + }, + }, + }, + } + + err := RenderJobTaskWithGlobalVariables(task, map[string]string{ + "workflow.trigger.branch": "feature/demo", + "workflow.params.reviewer": "reviewer@example.com", + "payload.commits.0.author.email": "dev@example.com", + }) + if err != nil { + t.Fatalf("RenderJobTaskWithGlobalVariables returned error: %v", err) + } + + spec, ok := task.Spec.(*commonmodels.JobTaskNotificationSpec) + if !ok { + t.Fatalf("expected notification spec type, got %T", task.Spec) + } + + if spec.Title != "notify feature/demo" { + t.Fatalf("expected notification title to be rendered, got %q", spec.Title) + } + + if spec.Content != "reviewer reviewer@example.com" { + t.Fatalf("expected notification content to be rendered, got %q", spec.Content) + } + + if len(spec.MSTeamsNotificationConfig.AtEmails) != 1 { + t.Fatalf("expected 1 rendered static recipient, got %d", len(spec.MSTeamsNotificationConfig.AtEmails)) + } + + if got := spec.MSTeamsNotificationConfig.AtEmails[0]; got != "reviewer@example.com" { + t.Fatalf("expected rendered static recipient, got %q", got) + } + + if len(spec.MSTeamsNotificationConfig.DynamicRecipients) != 1 { + t.Fatalf("expected 1 dynamic recipient, got %d", len(spec.MSTeamsNotificationConfig.DynamicRecipients)) + } + + if got := spec.MSTeamsNotificationConfig.DynamicRecipients[0]; got != "{{.payload.commits.0.author.email}}" { + t.Fatalf("expected dynamic recipient template to be preserved, got %q", got) + } +} + +func TestRestoreWorkflowNotificationRuntimeRenderFieldsRestoresOnlyDynamicRecipients(t *testing.T) { + spec := &commonmodels.NotificationJobSpec{ + WebHookType: setting.NotifyWebHookTypeMSTeam, + Title: "notify {{.workflow.trigger.branch}}", + Content: "reviewer {{.workflow.params.reviewer}}", + MSTeamsNotificationConfig: &commonmodels.MSTeamsNotificationConfig{ + AtEmails: []string{"{{.workflow.params.reviewer}}"}, + DynamicRecipients: commonmodels.DynamicRecipients{ + "{{.payload.commits.0.author.email}}", + }, + }, + } + workflow := &commonmodels.WorkflowV4{ + Stages: []*commonmodels.WorkflowStage{ + { + Jobs: []*commonmodels.Job{ + { + Name: "notify", + JobType: config.JobNotification, + Spec: spec, + }, + }, + }, + }, + } + + backups, err := backupWorkflowNotificationRuntimeRenderFields(workflow) + if err != nil { + t.Fatalf("backupWorkflowNotificationRuntimeRenderFields returned error: %v", err) + } + + spec.Title = "notify feature/demo" + spec.Content = "reviewer reviewer@example.com" + spec.MSTeamsNotificationConfig.AtEmails = []string{"reviewer@example.com"} + spec.MSTeamsNotificationConfig.DynamicRecipients = commonmodels.DynamicRecipients{"dev@example.com"} + + if err := restoreWorkflowNotificationRuntimeRenderFields(workflow, backups); err != nil { + t.Fatalf("restoreWorkflowNotificationRuntimeRenderFields returned error: %v", err) + } + + restoredSpec, ok := workflow.Stages[0].Jobs[0].Spec.(*commonmodels.NotificationJobSpec) + if !ok { + t.Fatalf("expected notification spec type, got %T", workflow.Stages[0].Jobs[0].Spec) + } + + if restoredSpec.Title != "notify feature/demo" { + t.Fatalf("expected title to stay rendered, got %q", restoredSpec.Title) + } + if restoredSpec.Content != "reviewer reviewer@example.com" { + t.Fatalf("expected content to stay rendered, got %q", restoredSpec.Content) + } + if len(restoredSpec.MSTeamsNotificationConfig.AtEmails) != 1 || restoredSpec.MSTeamsNotificationConfig.AtEmails[0] != "reviewer@example.com" { + t.Fatalf("expected static recipients to stay rendered, got %#v", restoredSpec.MSTeamsNotificationConfig.AtEmails) + } + if len(restoredSpec.MSTeamsNotificationConfig.DynamicRecipients) != 1 || restoredSpec.MSTeamsNotificationConfig.DynamicRecipients[0] != "{{.payload.commits.0.author.email}}" { + t.Fatalf("expected dynamic recipients template to be restored, got %#v", restoredSpec.MSTeamsNotificationConfig.DynamicRecipients) + } +} + +func TestRenderJobTaskWithGlobalVariablesReturnsMarshalError(t *testing.T) { + task := &commonmodels.JobTask{ + Name: "notify-task", + JobType: string(config.JobFreestyle), + Spec: map[string]interface{}{ + "invalid": math.NaN(), + }, + } + + err := RenderJobTaskWithGlobalVariables(task, map[string]string{ + "workflow.trigger.branch": "feature/demo", + }) + if err == nil { + t.Fatal("expected marshal error, got nil") + } + if !strings.Contains(err.Error(), "failed to marshal task notify-task") { + t.Fatalf("expected marshal error message, got %v", err) + } +} diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index 178da98483..f2412da01e 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -1131,15 +1131,8 @@ func RetryWorkflowTaskV4(workflowName string, taskID int64, logger *zap.SugaredL jobTask.EndTime = 0 jobTask.Error = "" if t, ok := jobTaskMap[jobTask.Name]; ok { - taskBytes, _ := json.Marshal(t) - taskString := string(taskBytes) - for k, v := range globalKeyMap { - taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), v) - log.Debugf("replacing key %s with value: %s", fmt.Sprintf("{{.%s}}", k), v) - } - err := json.Unmarshal([]byte(taskString), &t) - if err != nil { - return fmt.Errorf("failed to replace input variable for task: %s, error: %s", t.Name, err) + if err := workflowController.RenderJobTaskWithGlobalVariables(t, globalKeyMap); err != nil { + return err } jobTask.Spec = t.Spec } else { @@ -1333,15 +1326,8 @@ func ManualExecWorkflowTaskV4(workflowName string, taskID int64, stageName strin job.Spec = ctrl.GetSpec() for _, task := range jobTasks { - taskBytes, _ := json.Marshal(task) - taskString := string(taskBytes) - for k, v := range globalKeyMap { - taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), v) - log.Debugf("replacing key %s with value: %s", fmt.Sprintf("{{.%s}}", k), v) - } - err := json.Unmarshal([]byte(taskString), &task) - if err != nil { - return fmt.Errorf("failed to replace input variable for task: %s, error: %s", task.Name, err) + if err := workflowController.RenderJobTaskWithGlobalVariables(task, globalKeyMap); err != nil { + return err } } From b391772e4f921f4717546af6fb49c412a5ea74d3 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 18 Jun 2026 10:43:47 +0800 Subject: [PATCH 15/22] chore: drop notification runtime tests from pr Signed-off-by: huanghongbo-hhb --- .../jobcontroller/job_notification_test.go | 35 ---- .../workflow/controller/workflow_test.go | 176 ------------------ 2 files changed, 211 deletions(-) delete mode 100644 pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go index 042e7cdae8..daa6428319 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go @@ -92,38 +92,3 @@ func TestPrepareRuntimeNotificationFieldsDoesNotRenderPayloadInTitleOrContent(t t.Fatalf("expected content %q, got %q", want, ctl.jobTaskSpec.Content) } } - -func TestPrepareRuntimeNotificationFieldsRendersWorkflowTriggerInTitle(t *testing.T) { - ctl := &NotificationJobCtl{ - workflowCtx: &commonmodels.WorkflowTaskCtx{ - WorkflowKeyVals: []*commonmodels.KeyVal{ - {Key: "workflow.trigger.branch", Value: "feature/demo"}, - {Key: "payload.commits.0.author.email", Value: "dev@example.com"}, - }, - }, - jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ - WebHookType: setting.NotifyWebHookTypeMail, - Title: "notify {{.workflow.trigger.branch}}", - MailNotificationConfig: &commonmodels.MailNotificationConfig{ - DynamicRecipients: commonmodels.DynamicRecipients{"{{.payload.commits.0.author.email}}"}, - }, - }, - } - - if err := ctl.prepareRuntimeNotificationFields(); err != nil { - t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) - } - - if ctl.jobTaskSpec.Title != "notify feature/demo" { - t.Fatalf("expected rendered title, got %q", ctl.jobTaskSpec.Title) - } - - if len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers) != 1 { - t.Fatalf("expected 1 resolved target user, got %d", len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers)) - } - - got := ctl.jobTaskSpec.MailNotificationConfig.TargetUsers[0] - if got == nil || got.Type != "email" || got.UserName != "dev@example.com" { - t.Fatalf("expected runtime dynamic recipient to resolve to email target user, got %#v", got) - } -} diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go deleted file mode 100644 index e4d93f003c..0000000000 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package controller - -import ( - "math" - "strings" - "testing" - - "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" - commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" - "github.com/koderover/zadig/v2/pkg/setting" -) - -func TestBuildRuntimeReferableVariablesIncludesTriggerRuntimeVariables(t *testing.T) { - workflow := &commonmodels.WorkflowV4{ - Name: "workflow-demo", - DisplayName: "Workflow Demo", - } - - variables := buildRuntimeReferableVariables(workflow) - keySet := make(map[string]struct{}, len(variables)) - for _, variable := range variables { - keySet[variable.Key] = struct{}{} - } - - expectedKeys := []string{ - "workflow.task.creator", - "workflow.trigger.branch", - "workflow.trigger.target_branch", - "workflow.trigger.pr", - "workflow.trigger.commit_id", - "workflow.trigger.commit_sha", - "workflow.trigger.commit_message", - "workflow.trigger.committer", - "workflow.trigger.event", - } - - for _, key := range expectedKeys { - if _, ok := keySet[key]; !ok { - t.Fatalf("expected runtime variable %s to be exposed", key) - } - } -} - -func TestRenderJobTaskPreservesNotificationDynamicRecipients(t *testing.T) { - task := &commonmodels.JobTask{ - JobType: string(config.JobNotification), - Spec: &commonmodels.JobTaskNotificationSpec{ - WebHookType: setting.NotifyWebHookTypeMSTeam, - Title: "notify {{.workflow.trigger.branch}}", - Content: "reviewer {{.workflow.params.reviewer}}", - MSTeamsNotificationConfig: &commonmodels.MSTeamsNotificationConfig{ - AtEmails: []string{"{{.workflow.params.reviewer}}"}, - DynamicRecipients: commonmodels.DynamicRecipients{ - "{{.payload.commits.0.author.email}}", - }, - }, - }, - } - - err := RenderJobTaskWithGlobalVariables(task, map[string]string{ - "workflow.trigger.branch": "feature/demo", - "workflow.params.reviewer": "reviewer@example.com", - "payload.commits.0.author.email": "dev@example.com", - }) - if err != nil { - t.Fatalf("RenderJobTaskWithGlobalVariables returned error: %v", err) - } - - spec, ok := task.Spec.(*commonmodels.JobTaskNotificationSpec) - if !ok { - t.Fatalf("expected notification spec type, got %T", task.Spec) - } - - if spec.Title != "notify feature/demo" { - t.Fatalf("expected notification title to be rendered, got %q", spec.Title) - } - - if spec.Content != "reviewer reviewer@example.com" { - t.Fatalf("expected notification content to be rendered, got %q", spec.Content) - } - - if len(spec.MSTeamsNotificationConfig.AtEmails) != 1 { - t.Fatalf("expected 1 rendered static recipient, got %d", len(spec.MSTeamsNotificationConfig.AtEmails)) - } - - if got := spec.MSTeamsNotificationConfig.AtEmails[0]; got != "reviewer@example.com" { - t.Fatalf("expected rendered static recipient, got %q", got) - } - - if len(spec.MSTeamsNotificationConfig.DynamicRecipients) != 1 { - t.Fatalf("expected 1 dynamic recipient, got %d", len(spec.MSTeamsNotificationConfig.DynamicRecipients)) - } - - if got := spec.MSTeamsNotificationConfig.DynamicRecipients[0]; got != "{{.payload.commits.0.author.email}}" { - t.Fatalf("expected dynamic recipient template to be preserved, got %q", got) - } -} - -func TestRestoreWorkflowNotificationRuntimeRenderFieldsRestoresOnlyDynamicRecipients(t *testing.T) { - spec := &commonmodels.NotificationJobSpec{ - WebHookType: setting.NotifyWebHookTypeMSTeam, - Title: "notify {{.workflow.trigger.branch}}", - Content: "reviewer {{.workflow.params.reviewer}}", - MSTeamsNotificationConfig: &commonmodels.MSTeamsNotificationConfig{ - AtEmails: []string{"{{.workflow.params.reviewer}}"}, - DynamicRecipients: commonmodels.DynamicRecipients{ - "{{.payload.commits.0.author.email}}", - }, - }, - } - workflow := &commonmodels.WorkflowV4{ - Stages: []*commonmodels.WorkflowStage{ - { - Jobs: []*commonmodels.Job{ - { - Name: "notify", - JobType: config.JobNotification, - Spec: spec, - }, - }, - }, - }, - } - - backups, err := backupWorkflowNotificationRuntimeRenderFields(workflow) - if err != nil { - t.Fatalf("backupWorkflowNotificationRuntimeRenderFields returned error: %v", err) - } - - spec.Title = "notify feature/demo" - spec.Content = "reviewer reviewer@example.com" - spec.MSTeamsNotificationConfig.AtEmails = []string{"reviewer@example.com"} - spec.MSTeamsNotificationConfig.DynamicRecipients = commonmodels.DynamicRecipients{"dev@example.com"} - - if err := restoreWorkflowNotificationRuntimeRenderFields(workflow, backups); err != nil { - t.Fatalf("restoreWorkflowNotificationRuntimeRenderFields returned error: %v", err) - } - - restoredSpec, ok := workflow.Stages[0].Jobs[0].Spec.(*commonmodels.NotificationJobSpec) - if !ok { - t.Fatalf("expected notification spec type, got %T", workflow.Stages[0].Jobs[0].Spec) - } - - if restoredSpec.Title != "notify feature/demo" { - t.Fatalf("expected title to stay rendered, got %q", restoredSpec.Title) - } - if restoredSpec.Content != "reviewer reviewer@example.com" { - t.Fatalf("expected content to stay rendered, got %q", restoredSpec.Content) - } - if len(restoredSpec.MSTeamsNotificationConfig.AtEmails) != 1 || restoredSpec.MSTeamsNotificationConfig.AtEmails[0] != "reviewer@example.com" { - t.Fatalf("expected static recipients to stay rendered, got %#v", restoredSpec.MSTeamsNotificationConfig.AtEmails) - } - if len(restoredSpec.MSTeamsNotificationConfig.DynamicRecipients) != 1 || restoredSpec.MSTeamsNotificationConfig.DynamicRecipients[0] != "{{.payload.commits.0.author.email}}" { - t.Fatalf("expected dynamic recipients template to be restored, got %#v", restoredSpec.MSTeamsNotificationConfig.DynamicRecipients) - } -} - -func TestRenderJobTaskWithGlobalVariablesReturnsMarshalError(t *testing.T) { - task := &commonmodels.JobTask{ - Name: "notify-task", - JobType: string(config.JobFreestyle), - Spec: map[string]interface{}{ - "invalid": math.NaN(), - }, - } - - err := RenderJobTaskWithGlobalVariables(task, map[string]string{ - "workflow.trigger.branch": "feature/demo", - }) - if err == nil { - t.Fatal("expected marshal error, got nil") - } - if !strings.Contains(err.Error(), "failed to marshal task notify-task") { - t.Fatalf("expected marshal error message, got %v", err) - } -} From 5c20ad8520ba19b7bbc5ce69ad1348910a28c96c Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 18 Jun 2026 11:36:51 +0800 Subject: [PATCH 16/22] fix: preserve notification payload templates at runtime Signed-off-by: huanghongbo-hhb --- .../workflowcontroller/jobcontroller/job.go | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go index ef9299e3bf..21dfa3b685 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go @@ -46,6 +46,140 @@ type JobCtl interface { SaveInfo(ctx context.Context) error } +type notificationRuntimeRenderFields struct { + Title string + Content string + + LarkHookAtUsers []string + WechatAtUsers []string + DingDingMobiles []string + MSTeamsAtEmails []string + LarkHookDynamic commonmodels.DynamicRecipients + LarkGroupDynamic commonmodels.DynamicRecipients + LarkPersonDynamic commonmodels.DynamicRecipients + WechatDynamic commonmodels.DynamicRecipients + DingDingDynamic commonmodels.DynamicRecipients + MSTeamsDynamic commonmodels.DynamicRecipients + MailDynamic commonmodels.DynamicRecipients +} + +func cloneNotificationStrings(items []string) []string { + if items == nil { + return nil + } + resp := make([]string, len(items)) + copy(resp, items) + return resp +} + +func cloneNotificationDynamicRecipients(items commonmodels.DynamicRecipients) commonmodels.DynamicRecipients { + if items == nil { + return nil + } + resp := make(commonmodels.DynamicRecipients, len(items)) + copy(resp, items) + return resp +} + +func backupNotificationRuntimeRenderFields(job *commonmodels.JobTask) (*notificationRuntimeRenderFields, error) { + if job == nil || job.JobType != string(config.JobNotification) { + return nil, nil + } + + spec, err := decodeNotificationJobTaskSpec(job.Spec) + if err != nil { + return nil, err + } + + resp := ¬ificationRuntimeRenderFields{ + Title: spec.Title, + Content: spec.Content, + } + + if cfg := spec.LarkHookNotificationConfig; cfg != nil { + resp.LarkHookAtUsers = cloneNotificationStrings(cfg.AtUsers) + resp.LarkHookDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.LarkGroupNotificationConfig; cfg != nil { + resp.LarkGroupDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.LarkPersonNotificationConfig; cfg != nil { + resp.LarkPersonDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.WechatNotificationConfig; cfg != nil { + resp.WechatAtUsers = cloneNotificationStrings(cfg.AtUsers) + resp.WechatDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.DingDingNotificationConfig; cfg != nil { + resp.DingDingMobiles = cloneNotificationStrings(cfg.AtMobiles) + resp.DingDingDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.MSTeamsNotificationConfig; cfg != nil { + resp.MSTeamsAtEmails = cloneNotificationStrings(cfg.AtEmails) + resp.MSTeamsDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.MailNotificationConfig; cfg != nil { + resp.MailDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + + return resp, nil +} + +func restoreNotificationRuntimeRenderFields(job *commonmodels.JobTask, fields *notificationRuntimeRenderFields) (*commonmodels.JobTaskNotificationSpec, error) { + if job == nil || fields == nil || job.JobType != string(config.JobNotification) { + return nil, nil + } + + spec, err := decodeNotificationJobTaskSpec(job.Spec) + if err != nil { + return nil, err + } + + spec.Title = fields.Title + spec.Content = fields.Content + + if cfg := spec.LarkHookNotificationConfig; cfg != nil { + cfg.AtUsers = cloneNotificationStrings(fields.LarkHookAtUsers) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.LarkHookDynamic) + } + if cfg := spec.LarkGroupNotificationConfig; cfg != nil { + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.LarkGroupDynamic) + } + if cfg := spec.LarkPersonNotificationConfig; cfg != nil { + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.LarkPersonDynamic) + } + if cfg := spec.WechatNotificationConfig; cfg != nil { + cfg.AtUsers = cloneNotificationStrings(fields.WechatAtUsers) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.WechatDynamic) + } + if cfg := spec.DingDingNotificationConfig; cfg != nil { + cfg.AtMobiles = cloneNotificationStrings(fields.DingDingMobiles) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.DingDingDynamic) + } + if cfg := spec.MSTeamsNotificationConfig; cfg != nil { + cfg.AtEmails = cloneNotificationStrings(fields.MSTeamsAtEmails) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.MSTeamsDynamic) + } + if cfg := spec.MailNotificationConfig; cfg != nil { + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.MailDynamic) + } + + job.Spec = spec + return spec, nil +} + +func decodeNotificationJobTaskSpec(raw interface{}) (*commonmodels.JobTaskNotificationSpec, error) { + if spec, ok := raw.(*commonmodels.JobTaskNotificationSpec); ok && spec != nil { + return spec, nil + } + + spec := &commonmodels.JobTaskNotificationSpec{} + if err := commonmodels.IToi(raw, spec); err != nil { + return nil, err + } + return spec, nil +} + func initJobCtl(job *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTaskCtx, logger *zap.SugaredLogger, ack func()) JobCtl { var jobCtl JobCtl switch job.JobType { @@ -177,6 +311,14 @@ func runJob(ctx context.Context, job *commonmodels.JobTask, workflowCtx *commonm return true }) + notificationFields, err := backupNotificationRuntimeRenderFields(job) + if err != nil { + logger.Errorf("backup notification runtime fields error: %v", err) + job.Status = config.StatusFailed + job.Error = err.Error() + return + } + // remove all the unrendered variable, replacing then with empty string b, _ := json.Marshal(job) variableRegexp := regexp.MustCompile(config.VariableRegEx) @@ -187,6 +329,17 @@ func runJob(ctx context.Context, job *commonmodels.JobTask, workflowCtx *commonm job.Error = err.Error() return } + if restoredSpec, err := restoreNotificationRuntimeRenderFields(job, notificationFields); err != nil { + logger.Errorf("restore notification runtime fields error: %v", err) + job.Status = config.StatusFailed + job.Error = err.Error() + return + } else if restoredSpec != nil { + if ctl, ok := jobCtl.(*NotificationJobCtl); ok { + ctl.jobTaskSpec = restoredSpec + ctl.job.Spec = restoredSpec + } + } // Check execute policy before running the job if !shouldExecuteJob(job) { From 571de676a9e8e9ebe7f2412ad8cbd3d4b0f402e7 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 23 Jun 2026 11:02:12 +0800 Subject: [PATCH 17/22] fix: send mail to dynamic email recipients Signed-off-by: huanghongbo-hhb --- .../common/service/instantmessage/mail.go | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/mail.go b/pkg/microservice/aslan/core/common/service/instantmessage/mail.go index a02128f19d..b6253eb9a5 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/mail.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/mail.go @@ -42,6 +42,39 @@ func (w *Service) sendMailMessage(title, content string, users []*models.User) e log.Errorf("sendMailMessage GetEmailService error, error msg:%s", err) } + sentEmails := make(map[string]struct{}) + sendToEmail := func(to string) { + if to == "" { + return + } + if _, ok := sentEmails[to]; ok { + return + } + sentEmails[to] = struct{}{} + sendErr := mail.SendEmail(&mail.EmailParams{ + From: emailSvc.Address, + To: to, + Subject: title, + Host: email.Name, + UserName: email.UserName, + Password: email.Password, + Port: email.Port, + TlsSkipVerify: email.TlsSkipVerify, + Body: content, + }) + if sendErr != nil { + err = sendErr + log.Errorf("sendMailMessage SendEmail error, error msg:%s", sendErr) + } + } + + for _, u := range users { + if u == nil || u.Type != "email" { + continue + } + sendToEmail(u.UserName) + } + users, userMap := util.GeneFlatUsers(users) for _, u := range users { info, ok := userMap[u.UserID] @@ -57,21 +90,7 @@ func (w *Service) sendMailMessage(title, content string, users []*models.User) e log.Warnf("sendMailMessage user %s email is empty", info.Name) continue } - err = mail.SendEmail(&mail.EmailParams{ - From: emailSvc.Address, - To: info.Email, - Subject: title, - Host: email.Name, - UserName: email.UserName, - Password: email.Password, - Port: email.Port, - TlsSkipVerify: email.TlsSkipVerify, - Body: content, - }) - if err != nil { - log.Errorf("sendMailMessage SendEmail error, error msg:%s", err) - continue - } + sendToEmail(info.Email) } return err From ae3d56176f740fd56c0517f424e2767e9f442b26 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 23 Jun 2026 11:41:32 +0800 Subject: [PATCH 18/22] fix: resolve workflow notification dynamic recipients Signed-off-by: huanghongbo-hhb --- .../dynamic_recipient.go} | 122 ++++++++++++++---- .../service/instantmessage/workflow_task.go | 95 ++++++++++++++ .../workflow_task_dynamic_recipient_test.go | 57 ++++++++ .../jobcontroller/job_notification.go | 79 ++---------- .../controller/job/job_notification.go | 4 +- .../service/workflow/workflow_task_v4.go | 3 +- 6 files changed, 265 insertions(+), 95 deletions(-) rename pkg/microservice/aslan/core/common/service/{workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go => dynamicrecipient/dynamic_recipient.go} (80%) create mode 100644 pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go similarity index 80% rename from pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go rename to pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go index 2ae4c20d1a..16731d1748 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_dynamic_recipient.go +++ b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go @@ -1,4 +1,4 @@ -package jobcontroller +package dynamicrecipient import ( "fmt" @@ -6,6 +6,7 @@ import ( "github.com/samber/lo" + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/setting" userclient "github.com/koderover/zadig/v2/pkg/shared/client/user" @@ -71,7 +72,7 @@ type dynamicRecipientSpec struct { kind dynamicRecipientKind } -type dynamicRecipientResolver struct { +type Resolver struct { keyMap map[string]string lookupUsersByAccount func(account string) ([]*userclient.User, error) @@ -131,8 +132,8 @@ func ValidateDynamicRecipientsForNotifyConfig(notifyType setting.NotifyWebHookTy return nil } -func newDynamicRecipientResolver(keyMap map[string]string) *dynamicRecipientResolver { - return &dynamicRecipientResolver{ +func NewResolver(keyMap map[string]string) *Resolver { + return &Resolver{ keyMap: keyMap, lookupUsersByAccount: func(account string) ([]*userclient.User, error) { return searchUsersByAccount(account) @@ -152,7 +153,7 @@ func newDynamicRecipientResolver(keyMap map[string]string) *dynamicRecipientReso } } -func (r *dynamicRecipientResolver) resolveEmails(recipients []string) ([]string, error) { +func (r *Resolver) ResolveEmails(recipients []string) ([]string, error) { resp := make([]string, 0) for _, recipient := range recipients { spec, value, ok, err := r.resolveRecipient(recipient) @@ -190,10 +191,10 @@ func (r *dynamicRecipientResolver) resolveEmails(recipients []string) ([]string, return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to email", recipient) } } - return uniqStrings(resp), nil + return UniqStrings(resp), nil } -func (r *dynamicRecipientResolver) resolveMobiles(recipients []string) ([]string, error) { +func (r *Resolver) ResolveMobiles(recipients []string) ([]string, error) { resp := make([]string, 0) for _, recipient := range recipients { spec, value, ok, err := r.resolveRecipient(recipient) @@ -231,10 +232,10 @@ func (r *dynamicRecipientResolver) resolveMobiles(recipients []string) ([]string return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to mobile", recipient) } } - return uniqStrings(resp), nil + return UniqStrings(resp), nil } -func (r *dynamicRecipientResolver) resolveDirectValues(recipients []string, supportedKinds ...dynamicRecipientKind) ([]string, error) { +func (r *Resolver) ResolveDirectValues(recipients []string, supportedKinds ...dynamicRecipientKind) ([]string, error) { supported := make(map[dynamicRecipientKind]struct{}, len(supportedKinds)) for _, kind := range supportedKinds { supported[kind] = struct{}{} @@ -254,10 +255,14 @@ func (r *dynamicRecipientResolver) resolveDirectValues(recipients []string, supp } resp = append(resp, value) } - return uniqStrings(resp), nil + return UniqStrings(resp), nil } -func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID string, allowOpenID bool) ([]*larktool.UserInfo, error) { +func (r *Resolver) ResolveUserIDs(recipients []string) ([]string, error) { + return r.ResolveDirectValues(recipients, dynamicRecipientKindUserID) +} + +func (r *Resolver) ResolveLarkUsers(recipients []string, appID string, allowOpenID bool) ([]*larktool.UserInfo, error) { if len(recipients) == 0 { return nil, nil } @@ -337,10 +342,10 @@ func (r *dynamicRecipientResolver) resolveLarkUsers(recipients []string, appID s } } - return uniqLarkUsers(resp), nil + return UniqLarkUsers(resp), nil } -func (r *dynamicRecipientResolver) resolveLarkUserIDsByEmail(client *larktool.Client, appID, email string) ([]string, error) { +func (r *Resolver) resolveLarkUserIDsByEmail(client *larktool.Client, appID, email string) ([]string, error) { if id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, email); err != nil { return nil, err } else if found { @@ -365,10 +370,10 @@ func (r *dynamicRecipientResolver) resolveLarkUserIDsByEmail(client *larktool.Cl resp = append(resp, id) } } - return uniqStrings(resp), nil + return UniqStrings(resp), nil } -func (r *dynamicRecipientResolver) resolveLarkUserIDsByPhone(client *larktool.Client, appID, phone string) ([]string, error) { +func (r *Resolver) resolveLarkUserIDsByPhone(client *larktool.Client, appID, phone string) ([]string, error) { if id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, phone); err != nil { return nil, err } else if found { @@ -393,10 +398,10 @@ func (r *dynamicRecipientResolver) resolveLarkUserIDsByPhone(client *larktool.Cl resp = append(resp, id) } } - return uniqStrings(resp), nil + return UniqStrings(resp), nil } -func (r *dynamicRecipientResolver) resolveLarkUserIDsByAccount(client *larktool.Client, appID, account string) ([]string, error) { +func (r *Resolver) resolveLarkUserIDsByAccount(client *larktool.Client, appID, account string) ([]string, error) { users, err := r.getUsersByAccount(account) if err != nil { return nil, err @@ -427,10 +432,10 @@ func (r *dynamicRecipientResolver) resolveLarkUserIDsByAccount(client *larktool. } } } - return uniqStrings(resp), nil + return UniqStrings(resp), nil } -func (r *dynamicRecipientResolver) lookupLarkUserID(client *larktool.Client, appID, queryType, value string) (string, bool, error) { +func (r *Resolver) lookupLarkUserID(client *larktool.Client, appID, queryType, value string) (string, bool, error) { cacheKey := strings.Join([]string{appID, queryType, value}, ":") if cached, ok := r.larkUserIDCache[cacheKey]; ok { return cached, true, nil @@ -458,7 +463,7 @@ func (r *dynamicRecipientResolver) lookupLarkUserID(client *larktool.Client, app return userID, true, nil } -func (r *dynamicRecipientResolver) getUsersByAccount(account string) ([]*userclient.User, error) { +func (r *Resolver) getUsersByAccount(account string) ([]*userclient.User, error) { if users, ok := r.accountUsersCache[account]; ok { return users, nil } @@ -470,7 +475,7 @@ func (r *dynamicRecipientResolver) getUsersByAccount(account string) ([]*usercli return users, nil } -func (r *dynamicRecipientResolver) getUsersByEmail(email string) ([]*userclient.User, error) { +func (r *Resolver) getUsersByEmail(email string) ([]*userclient.User, error) { if users, ok := r.emailUsersCache[email]; ok { return users, nil } @@ -482,7 +487,7 @@ func (r *dynamicRecipientResolver) getUsersByEmail(email string) ([]*userclient. return users, nil } -func (r *dynamicRecipientResolver) getUsersByPhone(phone string) ([]*userclient.User, error) { +func (r *Resolver) getUsersByPhone(phone string) ([]*userclient.User, error) { if users, ok := r.phoneUsersCache[phone]; ok { return users, nil } @@ -494,7 +499,7 @@ func (r *dynamicRecipientResolver) getUsersByPhone(phone string) ([]*userclient. return users, nil } -func (r *dynamicRecipientResolver) getLarkClient(appID string) (*larktool.Client, error) { +func (r *Resolver) getLarkClient(appID string) (*larktool.Client, error) { if client, ok := r.larkClientCache[appID]; ok { return client, nil } @@ -508,7 +513,7 @@ func (r *dynamicRecipientResolver) getLarkClient(appID string) (*larktool.Client return client, nil } -func (r *dynamicRecipientResolver) resolveRecipient(raw string) (*dynamicRecipientSpec, string, bool, error) { +func (r *Resolver) resolveRecipient(raw string) (*dynamicRecipientSpec, string, bool, error) { spec, err := parseDynamicRecipient(raw) if err != nil { return nil, "", false, err @@ -522,6 +527,73 @@ func (r *dynamicRecipientResolver) resolveRecipient(raw string) (*dynamicRecipie return spec, value, true, nil } +func BuildMailUsersFromEmails(emails []string) []*commonmodels.User { + resp := make([]*commonmodels.User, 0, len(emails)) + for _, email := range lo.Uniq(emails) { + if email == "" { + continue + } + resp = append(resp, &commonmodels.User{ + Type: "email", + UserName: email, + }) + } + return resp +} + +func UniqMailUsers(users []*commonmodels.User) []*commonmodels.User { + seen := make(map[string]struct{}) + resp := make([]*commonmodels.User, 0, len(users)) + for _, user := range users { + if user == nil { + continue + } + key := user.Type + ":" + switch user.Type { + case "email": + key += user.UserName + case setting.UserTypeGroup: + key += user.GroupID + default: + key += user.UserID + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + resp = append(resp, user) + } + return resp +} + +func UniqLarkUsers(users []*larktool.UserInfo) []*larktool.UserInfo { + seen := make(map[string]struct{}) + resp := make([]*larktool.UserInfo, 0, len(users)) + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + key := user.IDType + ":" + user.ID + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + resp = append(resp, user) + } + return resp +} + +func renderNotificationString(input string, keyMap map[string]string) string { + if len(keyMap) == 0 || !strings.Contains(input, "{{.") { + return input + } + pairs := make([]string, 0, len(keyMap)*2) + for key, value := range keyMap { + pairs = append(pairs, "{{."+key+"}}", value) + } + return strings.NewReplacer(pairs...).Replace(input) +} + func parseDynamicRecipient(input string) (*dynamicRecipientSpec, error) { input = strings.TrimSpace(input) if !strings.HasPrefix(input, "{{.") || !strings.HasSuffix(input, "}}") { @@ -599,7 +671,7 @@ func searchUsersByPhone(phone string) ([]*userclient.User, error) { return resp.Users, nil } -func uniqStrings(items []string) []string { +func UniqStrings(items []string) []string { items = lo.Filter(items, func(item string, _ int) bool { return item != "" }) diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go index dc19bf35bc..b9ee41035b 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go @@ -36,6 +36,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb" templaterepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb/template" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/webhooknotify" commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" @@ -261,6 +262,11 @@ func (w *Service) SendWorkflowTaskApproveNotifications(workflowName string, task return errors.New(errMsg) } + if err := resolveWorkflowNotifyDynamicRecipients(task, notify); err != nil { + log.Errorf("failed to resolve workflow notification dynamic recipients, err: %s", err) + continue + } + if notify.WebHookType == setting.NotifyWebHookTypeMail { if task.TaskCreatorID != "" { for _, user := range notify.MailUsers { @@ -375,6 +381,11 @@ func (w *Service) SendWorkflowTaskNotifications(task *models.WorkflowTask) error return errors.New(errMsg) } + if err := resolveWorkflowNotifyDynamicRecipients(task, notify); err != nil { + log.Errorf("failed to resolve workflow notification dynamic recipients, err: %s", err) + continue + } + if notify.WebHookType == setting.NotifyWebHookTypeMail { if task.TaskCreatorID != "" { for _, user := range notify.MailNotificationConfig.TargetUsers { @@ -465,6 +476,90 @@ func shouldSkipFeishuPersonPauseNotification(task *models.WorkflowTask, notify * return false } +func resolveWorkflowNotifyDynamicRecipients(task *models.WorkflowTask, notify *models.NotifyCtl) error { + if task == nil || notify == nil { + return nil + } + + workflowArgs := task.WorkflowArgs + if workflowArgs == nil { + workflowArgs = task.OriginWorkflowArgs + } + if workflowArgs == nil { + return nil + } + + keyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + workflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) + resolver := dynamicrecipient.NewResolver(keyMap) + + if cfg := notify.LarkHookNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + if err != nil { + return err + } + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + cfg.AtUsers = append(cfg.AtUsers, user.ID) + } + cfg.AtUsers = dynamicrecipient.UniqStrings(cfg.AtUsers) + } + if cfg := notify.LarkGroupNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + if err != nil { + return err + } + cfg.AtUsers = dynamicrecipient.UniqLarkUsers(append(cfg.AtUsers, users...)) + } + if cfg := notify.LarkPersonNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) + if err != nil { + return err + } + cfg.TargetUsers = dynamicrecipient.UniqLarkUsers(append(cfg.TargetUsers, users...)) + } + if cfg := notify.MSTeamsNotificationConfig; cfg != nil { + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtEmails = dynamicrecipient.UniqStrings(append(cfg.AtEmails, emails...)) + } + if cfg := notify.MailNotificationConfig; cfg != nil { + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.TargetUsers = dynamicrecipient.UniqMailUsers(append(cfg.TargetUsers, dynamicrecipient.BuildMailUsersFromEmails(emails)...)) + } + if cfg := notify.DingDingNotificationConfig; cfg != nil { + mobiles, err := resolver.ResolveMobiles([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtMobiles = dynamicrecipient.UniqStrings(append(cfg.AtMobiles, mobiles...)) + } + if cfg := notify.WechatNotificationConfig; cfg != nil { + users, err := resolver.ResolveUserIDs([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtUsers = dynamicrecipient.UniqStrings(append(cfg.AtUsers, users...)) + } + + return nil +} + func (w *Service) SendManualExecStageNotifications(workflowCtx *models.WorkflowTaskCtx, stage *models.StageTask) error { if workflowCtx == nil || stage == nil || stage.ManualExec == nil { return nil diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go new file mode 100644 index 0000000000..faa52c0336 --- /dev/null +++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go @@ -0,0 +1,57 @@ +package instantmessage + +import ( + "testing" + "time" + + "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + "github.com/koderover/zadig/v2/pkg/setting" +) + +func TestResolveWorkflowNotifyDynamicRecipientsSupportsPayloadEmail(t *testing.T) { + task := &commonmodels.WorkflowTask{ + ProjectName: "yaml", + ProjectDisplayName: "yaml", + TaskID: 350, + StartTime: time.Now().Unix(), + WorkflowArgs: &commonmodels.WorkflowV4{ + HookPayload: &commonmodels.HookPayload{ + Branch: "feature-1", + TargetBranch: "feature-1", + CommitID: "14d4e3a44d3a02a2c3e48dfb4aec4d5fe91df31a", + CommitSHA: "14d4e3a44d3a02a2c3e48dfb4aec4d5fe91df31a", + EventType: "push", + RawPayload: `{ + "head_commit": { + "author": { + "email": "huanghongbo@koderover.com" + } + } + }`, + }, + }, + } + + notify := &commonmodels.NotifyCtl{ + Enabled: true, + WebHookType: setting.NotifyWebHookTypeMail, + NotifyTypes: []string{string(config.StatusCreated)}, + MailNotificationConfig: &commonmodels.MailNotificationConfig{ + DynamicRecipients: commonmodels.DynamicRecipients{"{{.payload.head_commit.author.email}}"}, + }, + } + + if err := resolveWorkflowNotifyDynamicRecipients(task, notify); err != nil { + t.Fatalf("resolveWorkflowNotifyDynamicRecipients returned error: %v", err) + } + + if got := len(notify.MailNotificationConfig.TargetUsers); got != 1 { + t.Fatalf("expected 1 resolved mail target user, got %d", got) + } + + target := notify.MailNotificationConfig.TargetUsers[0] + if target == nil || target.Type != "email" || target.UserName != "huanghongbo@koderover.com" { + t.Fatalf("unexpected resolved target: %#v", target) + } +} diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index 29050d4577..fef2d4b127 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -33,6 +33,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/instantmessage" larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" @@ -278,10 +279,10 @@ func renderNotificationStrings(inputs []string, keyMap map[string]string) []stri } func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) error { - resolver := newDynamicRecipientResolver(keyMap) + resolver := dynamicrecipient.NewResolver(keyMap) if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { - users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) if err != nil { return err } @@ -294,42 +295,42 @@ func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) cfg.AtUsers = lo.Uniq(cfg.AtUsers) } if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil { - users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) if err != nil { return err } - cfg.AtUsers = uniqLarkUsers(append(cfg.AtUsers, users...)) + cfg.AtUsers = dynamicrecipient.UniqLarkUsers(append(cfg.AtUsers, users...)) } if cfg := c.jobTaskSpec.LarkPersonNotificationConfig; cfg != nil { - users, err := resolver.resolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) if err != nil { return err } - cfg.TargetUsers = uniqLarkUsers(append(cfg.TargetUsers, users...)) + cfg.TargetUsers = dynamicrecipient.UniqLarkUsers(append(cfg.TargetUsers, users...)) } if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { - emails, err := resolver.resolveEmails([]string(cfg.DynamicRecipients)) + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) if err != nil { return err } cfg.AtEmails = lo.Uniq(append(cfg.AtEmails, emails...)) } if cfg := c.jobTaskSpec.MailNotificationConfig; cfg != nil { - emails, err := resolver.resolveEmails([]string(cfg.DynamicRecipients)) + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) if err != nil { return err } - cfg.TargetUsers = uniqMailUsers(append(cfg.TargetUsers, buildMailUsersFromEmails(emails)...)) + cfg.TargetUsers = dynamicrecipient.UniqMailUsers(append(cfg.TargetUsers, dynamicrecipient.BuildMailUsersFromEmails(emails)...)) } if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { - mobiles, err := resolver.resolveMobiles([]string(cfg.DynamicRecipients)) + mobiles, err := resolver.ResolveMobiles([]string(cfg.DynamicRecipients)) if err != nil { return err } cfg.AtMobiles = lo.Uniq(append(cfg.AtMobiles, mobiles...)) } if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { - users, err := resolver.resolveDirectValues([]string(cfg.DynamicRecipients), dynamicRecipientKindUserID) + users, err := resolver.ResolveUserIDs([]string(cfg.DynamicRecipients)) if err != nil { return err } @@ -339,62 +340,6 @@ func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) return nil } -func uniqLarkUsers(users []*lark.UserInfo) []*lark.UserInfo { - seen := make(map[string]struct{}) - resp := make([]*lark.UserInfo, 0, len(users)) - for _, user := range users { - if user == nil || user.ID == "" { - continue - } - key := user.IDType + ":" + user.ID - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - resp = append(resp, user) - } - return resp -} - -func buildMailUsersFromEmails(emails []string) []*commonmodels.User { - resp := make([]*commonmodels.User, 0, len(emails)) - for _, email := range lo.Uniq(emails) { - if email == "" { - continue - } - resp = append(resp, &commonmodels.User{ - Type: "email", - UserName: email, - }) - } - return resp -} - -func uniqMailUsers(users []*commonmodels.User) []*commonmodels.User { - seen := make(map[string]struct{}) - resp := make([]*commonmodels.User, 0, len(users)) - for _, user := range users { - if user == nil { - continue - } - key := user.Type + ":" - switch user.Type { - case "email": - key += user.UserName - case setting.UserTypeGroup: - key += user.GroupID - default: - key += user.UserID - } - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - resp = append(resp, user) - } - return resp -} - func buildLarkAtMessage(idList []string, isAtAll bool) string { idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) atUserList := make([]string, 0, len(idList)) diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go index 8d27b58c20..c83bb484d6 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go @@ -22,7 +22,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" - runtimeJobController "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" "github.com/koderover/zadig/v2/pkg/setting" e "github.com/koderover/zadig/v2/pkg/tool/errors" @@ -68,7 +68,7 @@ func (j NotificationJobController) Validate(isExecution bool) error { return e.ErrLicenseInvalid.AddDesc("") } if j.jobSpec.WebHookType == setting.NotifyWebHookTypeFeishu && j.jobSpec.LarkHookNotificationConfig != nil { - if err := runtimeJobController.ValidateDynamicRecipientsForNotifyConfig( + if err := dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig( j.jobSpec.WebHookType, j.jobSpec.LarkHookNotificationConfig.AppID, []string(j.jobSpec.LarkHookNotificationConfig.DynamicRecipients), diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index f2412da01e..39a95d8976 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -44,6 +44,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service" commonservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dingtalk" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/instantmessage" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/s3" @@ -750,7 +751,7 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } resp = append(resp, input) } - if err := runtimeJobController.ValidateDynamicRecipientsForNotifyConfig(notifyType, appID, resp); err != nil { + if err := dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig(notifyType, appID, resp); err != nil { return nil, err } return resp, nil From 713db516aad78188b9ed5cbbdb6f3a3695a76c8d Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 24 Jun 2026 14:11:57 +0800 Subject: [PATCH 19/22] fix: make dingding notifications mention recipients Signed-off-by: huanghongbo-hhb --- .../common/service/instantmessage/dingTalk.go | 52 ++++++++++++++++--- .../service/instantmessage/workflow_task.go | 11 +++- .../jobcontroller/job_notification.go | 26 +--------- 3 files changed, 55 insertions(+), 34 deletions(-) diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go b/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go index d5bb4eda40..046b314293 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go @@ -19,6 +19,7 @@ package instantmessage import ( "fmt" "net/url" + "strings" ) type DingDingMessage struct { @@ -55,16 +56,38 @@ type DingDingAt struct { } const ( - DingDingMsgType = "actionCard" + DingDingMsgType = "actionCard" + DingDingMarkdownMsgType = "markdown" + dingDingAtContentPrefix = "##### **相关人员**:" ) func (w *Service) sendDingDingMessage(uri, title, content, actionURL string, atMobiles []string, isAtAll bool) error { + message := BuildDingDingMessage(title, content, actionURL, atMobiles, isAtAll) + _, err := w.SendMessageRequest(uri, message) + return err +} + +func BuildDingDingMessage(title, content, actionURL string, atMobiles []string, isAtAll bool) *DingDingMessage { + if len(atMobiles) > 0 || isAtAll { + return &DingDingMessage{ + MsgType: DingDingMarkdownMsgType, + MarkDown: &DingDingMarkDown{ + Title: title, + Text: buildDingDingMarkdownText(content, actionURL, atMobiles, isAtAll), + }, + At: &DingDingAt{ + AtMobiles: atMobiles, + IsAtAll: isAtAll, + }, + } + } + // reference: https://open.dingtalk.com/document/orgapp/message-link-description dingtalkRedirectURL := fmt.Sprintf("dingtalk://dingtalkclient/page/link?url=%s&pc_slide=false", url.QueryEscape(actionURL), ) - message := &DingDingMessage{ + return &DingDingMessage{ MsgType: DingDingMsgType, ActionCard: &DingDingActionCard{ HideAvatar: "0", @@ -78,12 +101,27 @@ func (w *Service) sendDingDingMessage(uri, title, content, actionURL string, atM }, }, }, + At: &DingDingAt{ + AtMobiles: atMobiles, + IsAtAll: isAtAll, + }, + } +} + +func buildDingDingMarkdownText(content, actionURL string, atMobiles []string, isAtAll bool) string { + text := strings.TrimSpace(content) + if actionURL != "" { + text = strings.TrimSpace(fmt.Sprintf("%s\n\n[点击查看更多信息](%s)", text, actionURL)) } - message.At = &DingDingAt{ - AtMobiles: atMobiles, - IsAtAll: isAtAll, + if strings.Contains(text, dingDingAtContentPrefix) { + return text } - _, err := w.SendMessageRequest(uri, message) - return err + if len(atMobiles) > 0 { + return strings.TrimSpace(fmt.Sprintf("%s\n%s @%s", text, dingDingAtContentPrefix, strings.Join(atMobiles, "@"))) + } + if isAtAll { + return strings.TrimSpace(fmt.Sprintf("%s\n%s @所有人", text, dingDingAtContentPrefix)) + } + return text } diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go index b9ee41035b..087a996541 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go @@ -1029,7 +1029,7 @@ func (w *Service) getApproveNotificationContent(notify *models.NotifyCtl, task * } else if notify.WebHookType != setting.NotifyWebHookTypeFeishu && notify.WebHookType != setting.NotifyWebhookTypeFeishuApp && notify.WebHookType != setting.NotifyWebHookTypeFeishuPerson { tplcontent := strings.Join(tplBaseInfo, "") tplcontent += strings.Join(jobContents, "") - tplcontent = tplcontent + getNotifyAtContent(notify) + tplcontent = appendInlineNotifyAtContent(tplcontent, notify) tplcontent = fmt.Sprintf("%s%s", title, tplcontent) if notify.WebHookType == setting.NotifyWebHookTypeWechatWork { tplcontent = fmt.Sprintf("%s%s", tplcontent, moreInformation) @@ -1289,7 +1289,7 @@ func (w *Service) getNotificationContentWithOptions(notify *models.NotifyCtl, ta } else if notify.WebHookType != setting.NotifyWebHookTypeFeishu && notify.WebHookType != setting.NotifyWebhookTypeFeishuApp && notify.WebHookType != setting.NotifyWebHookTypeFeishuPerson { tplcontent := strings.Join(tplBaseInfo, "") tplcontent += strings.Join(jobContents, "") - tplcontent = tplcontent + getNotifyAtContent(notify) + tplcontent = appendInlineNotifyAtContent(tplcontent, notify) tplcontent = fmt.Sprintf("%s%s", title, tplcontent) if notify.WebHookType == setting.NotifyWebHookTypeWechatWork { tplcontent = fmt.Sprintf("%s%s", tplcontent, moreInformation) @@ -1616,6 +1616,13 @@ func genSonartMetricsText(jobSpec *models.JobTaskFreestyleSpec, language string) return result, mailResult, nil } +func appendInlineNotifyAtContent(content string, notify *models.NotifyCtl) string { + if notify == nil || notify.WebHookType == setting.NotifyWebHookTypeDingDing { + return content + } + return content + getNotifyAtContent(notify) +} + func (w *Service) sendNotification(title, content string, notify *models.NotifyCtl, card *LarkCard, webhookNotify *webhooknotify.WorkflowNotify, taskStatus config.Status) error { link := "" if notify.WebHookType == setting.NotifyWebHookTypeDingDing || notify.WebHookType == setting.NotifyWebHookTypeWechatWork || notify.WebHookType == setting.NotifyWebHookTypeMSTeam { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index fef2d4b127..b3d68815ce 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -454,31 +454,7 @@ func sendDingDingMessage(productName, workflowName, workflowDisplayName string, url.PathEscape(workflowDisplayName), ) - // reference: https://open.dingtalk.com/document/orgapp/message-link-description - dingtalkRedirectURL := fmt.Sprintf("dingtalk://dingtalkclient/page/link?url=%s&pc_slide=false", - url.QueryEscape(actionURL), - ) - - messageReq := instantmessage.DingDingMessage{ - MsgType: instantmessage.DingDingMsgType, - ActionCard: &instantmessage.DingDingActionCard{ - HideAvatar: "0", - ButtonOrientation: "0", - Text: processedMessage, - Title: title, - Buttons: []*instantmessage.DingDingButton{ - { - Title: "点击查看更多信息", - ActionURL: dingtalkRedirectURL, - }, - }, - }, - } - - messageReq.At = &instantmessage.DingDingAt{ - AtMobiles: idList, - IsAtAll: isAtAll, - } + messageReq := instantmessage.BuildDingDingMessage(title, processedMessage, actionURL, idList, isAtAll) // TODO: if required, add proxy to it c := httpclient.New() From 7031482a6b2f3d7cff8f78952e9d23ea35956daa Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Fri, 26 Jun 2026 09:25:33 +0800 Subject: [PATCH 20/22] fix: restrict dynamic notification recipients Signed-off-by: huanghongbo-hhb --- .../dynamicrecipient/dynamic_recipient.go | 213 +++--------------- .../service/instantmessage/workflow_task.go | 6 +- .../jobcontroller/job_notification.go | 6 +- .../controller/job/job_notification.go | 60 ++++- 4 files changed, 92 insertions(+), 193 deletions(-) diff --git a/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go index 16731d1748..3812f925ae 100644 --- a/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go +++ b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go @@ -17,52 +17,35 @@ import ( type dynamicRecipientKind string const ( - dynamicRecipientKindEmail dynamicRecipientKind = "email" - dynamicRecipientKindMobile dynamicRecipientKind = "mobile" - dynamicRecipientKindAccount dynamicRecipientKind = "account" - dynamicRecipientKindUserID dynamicRecipientKind = "user_id" - dynamicRecipientKindOpenID dynamicRecipientKind = "open_id" - - searchAllIdentityType = "*" + dynamicRecipientKindEmail dynamicRecipientKind = "email" + dynamicRecipientKindMobile dynamicRecipientKind = "mobile" ) var supportedDynamicRecipientKinds = map[setting.NotifyWebHookType]map[dynamicRecipientKind]struct{}{ setting.NotifyWebhookTypeFeishuApp: { - dynamicRecipientKindEmail: {}, - dynamicRecipientKindMobile: {}, - dynamicRecipientKindAccount: {}, - dynamicRecipientKindUserID: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, }, setting.NotifyWebHookTypeFeishuPerson: { - dynamicRecipientKindEmail: {}, - dynamicRecipientKindMobile: {}, - dynamicRecipientKindAccount: {}, - dynamicRecipientKindUserID: {}, - dynamicRecipientKindOpenID: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, }, setting.NotifyWebHookTypeFeishu: { - dynamicRecipientKindEmail: {}, - dynamicRecipientKindMobile: {}, - dynamicRecipientKindAccount: {}, - dynamicRecipientKindUserID: {}, - }, - setting.NotifyWebHookTypeWechatWork: { - dynamicRecipientKindUserID: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, }, + setting.NotifyWebHookTypeWechatWork: {}, setting.NotifyWebHookTypeDingDing: { - dynamicRecipientKindEmail: {}, - dynamicRecipientKindMobile: {}, - dynamicRecipientKindAccount: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, }, setting.NotifyWebHookTypeMSTeam: { - dynamicRecipientKindEmail: {}, - dynamicRecipientKindMobile: {}, - dynamicRecipientKindAccount: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, }, setting.NotifyWebHookTypeMail: { - dynamicRecipientKindEmail: {}, - dynamicRecipientKindMobile: {}, - dynamicRecipientKindAccount: {}, + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, }, } @@ -75,13 +58,11 @@ type dynamicRecipientSpec struct { type Resolver struct { keyMap map[string]string - lookupUsersByAccount func(account string) ([]*userclient.User, error) - lookupUsersByEmail func(email string) ([]*userclient.User, error) - lookupUsersByPhone func(phone string) ([]*userclient.User, error) + lookupUsersByEmail func(email string) ([]*userclient.User, error) + lookupUsersByPhone func(phone string) ([]*userclient.User, error) - accountUsersCache map[string][]*userclient.User - emailUsersCache map[string][]*userclient.User - phoneUsersCache map[string][]*userclient.User + emailUsersCache map[string][]*userclient.User + phoneUsersCache map[string][]*userclient.User larkClientCache map[string]*larktool.Client larkUserIDCache map[string]string @@ -115,36 +96,22 @@ func ValidateDynamicRecipientsForNotifyConfig(notifyType setting.NotifyWebHookTy if err := ValidateDynamicRecipientsForNotifyType(notifyType, recipients); err != nil { return err } - if notifyType != setting.NotifyWebHookTypeFeishu || strings.TrimSpace(appID) != "" { + if len(recipients) == 0 || !isLarkNotifyType(notifyType) || strings.TrimSpace(appID) != "" { return nil } - for _, recipient := range recipients { - spec, err := parseDynamicRecipient(recipient) - if err != nil { - return err - } - if spec.kind != dynamicRecipientKindUserID { - return fmt.Errorf("dynamic recipient %s requires app_id for notification type %s", recipient, notifyType) - } - } - - return nil + return fmt.Errorf("app_id is required to resolve dynamic recipients for notification type %s", notifyType) } func NewResolver(keyMap map[string]string) *Resolver { return &Resolver{ keyMap: keyMap, - lookupUsersByAccount: func(account string) ([]*userclient.User, error) { - return searchUsersByAccount(account) - }, lookupUsersByEmail: func(email string) ([]*userclient.User, error) { return searchUsersByEmail(email) }, lookupUsersByPhone: func(phone string) ([]*userclient.User, error) { return searchUsersByPhone(phone) }, - accountUsersCache: make(map[string][]*userclient.User), emailUsersCache: make(map[string][]*userclient.User), phoneUsersCache: make(map[string][]*userclient.User), larkClientCache: make(map[string]*larktool.Client), @@ -177,16 +144,6 @@ func (r *Resolver) ResolveEmails(recipients []string) ([]string, error) { resp = append(resp, user.Email) } } - case dynamicRecipientKindAccount: - users, err := r.getUsersByAccount(value) - if err != nil { - return nil, err - } - for _, user := range users { - if user != nil && user.Email != "" { - resp = append(resp, user.Email) - } - } default: return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to email", recipient) } @@ -218,16 +175,6 @@ func (r *Resolver) ResolveMobiles(recipients []string) ([]string, error) { resp = append(resp, user.Phone) } } - case dynamicRecipientKindAccount: - users, err := r.getUsersByAccount(value) - if err != nil { - return nil, err - } - for _, user := range users { - if user != nil && user.Phone != "" { - resp = append(resp, user.Phone) - } - } default: return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to mobile", recipient) } @@ -235,34 +182,21 @@ func (r *Resolver) ResolveMobiles(recipients []string) ([]string, error) { return UniqStrings(resp), nil } -func (r *Resolver) ResolveDirectValues(recipients []string, supportedKinds ...dynamicRecipientKind) ([]string, error) { - supported := make(map[dynamicRecipientKind]struct{}, len(supportedKinds)) - for _, kind := range supportedKinds { - supported[kind] = struct{}{} - } - - resp := make([]string, 0) +func (r *Resolver) ResolveUserIDs(recipients []string) ([]string, error) { for _, recipient := range recipients { - spec, value, ok, err := r.resolveRecipient(recipient) + _, _, ok, err := r.resolveRecipient(recipient) if err != nil { return nil, err } if !ok { continue } - if _, ok := supported[spec.kind]; !ok { - return nil, fmt.Errorf("dynamic recipient %s is not supported in this notification channel", recipient) - } - resp = append(resp, value) + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to user_id", recipient) } - return UniqStrings(resp), nil + return nil, nil } -func (r *Resolver) ResolveUserIDs(recipients []string) ([]string, error) { - return r.ResolveDirectValues(recipients, dynamicRecipientKindUserID) -} - -func (r *Resolver) ResolveLarkUsers(recipients []string, appID string, allowOpenID bool) ([]*larktool.UserInfo, error) { +func (r *Resolver) ResolveLarkUsers(recipients []string, appID string) ([]*larktool.UserInfo, error) { if len(recipients) == 0 { return nil, nil } @@ -274,7 +208,7 @@ func (r *Resolver) ResolveLarkUsers(recipients []string, appID string, allowOpen return client, nil } if strings.TrimSpace(appID) == "" { - return nil, fmt.Errorf("app_id is required to resolve lark dynamic recipients by email/mobile/account") + return nil, fmt.Errorf("app_id is required to resolve lark dynamic recipients by email/mobile") } var err error client, err = r.getLarkClient(appID) @@ -294,13 +228,6 @@ func (r *Resolver) ResolveLarkUsers(recipients []string, appID string, allowOpen } switch spec.kind { - case dynamicRecipientKindUserID: - resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserID}) - case dynamicRecipientKindOpenID: - if !allowOpenID { - return nil, fmt.Errorf("dynamic recipient %s is not supported in this notification channel", recipient) - } - resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserOpenID}) case dynamicRecipientKindEmail: client, err := getClient() if err != nil { @@ -325,18 +252,6 @@ func (r *Resolver) ResolveLarkUsers(recipients []string, appID string, allowOpen for _, id := range ids { resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) } - case dynamicRecipientKindAccount: - client, err := getClient() - if err != nil { - return nil, err - } - ids, err := r.resolveLarkUserIDsByAccount(client, appID, value) - if err != nil { - return nil, err - } - for _, id := range ids { - resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) - } default: return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to lark user", recipient) } @@ -401,40 +316,6 @@ func (r *Resolver) resolveLarkUserIDsByPhone(client *larktool.Client, appID, pho return UniqStrings(resp), nil } -func (r *Resolver) resolveLarkUserIDsByAccount(client *larktool.Client, appID, account string) ([]string, error) { - users, err := r.getUsersByAccount(account) - if err != nil { - return nil, err - } - - resp := make([]string, 0) - for _, user := range users { - if user == nil { - continue - } - if user.Email != "" { - id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, user.Email) - if err != nil { - return nil, err - } - if found { - resp = append(resp, id) - continue - } - } - if user.Phone != "" { - id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, user.Phone) - if err != nil { - return nil, err - } - if found { - resp = append(resp, id) - } - } - } - return UniqStrings(resp), nil -} - func (r *Resolver) lookupLarkUserID(client *larktool.Client, appID, queryType, value string) (string, bool, error) { cacheKey := strings.Join([]string{appID, queryType, value}, ":") if cached, ok := r.larkUserIDCache[cacheKey]; ok { @@ -463,18 +344,6 @@ func (r *Resolver) lookupLarkUserID(client *larktool.Client, appID, queryType, v return userID, true, nil } -func (r *Resolver) getUsersByAccount(account string) ([]*userclient.User, error) { - if users, ok := r.accountUsersCache[account]; ok { - return users, nil - } - users, err := r.lookupUsersByAccount(account) - if err != nil { - return nil, err - } - r.accountUsersCache[account] = users - return users, nil -} - func (r *Resolver) getUsersByEmail(email string) ([]*userclient.User, error) { if users, ok := r.emailUsersCache[email]; ok { return users, nil @@ -614,14 +483,8 @@ func parseDynamicRecipient(input string) (*dynamicRecipientSpec, error) { kind = dynamicRecipientKindEmail case "mobile", "phone": kind = dynamicRecipientKindMobile - case "account": - kind = dynamicRecipientKindAccount - case "user_id", "userid": - kind = dynamicRecipientKindUserID - case "open_id": - kind = dynamicRecipientKindOpenID default: - return nil, fmt.Errorf("dynamic recipient %s is not supported, only email/mobile(phone)/account/user_id(userid)/open_id are allowed", input) + return nil, fmt.Errorf("dynamic recipient %s is not supported, only email/mobile(phone) are allowed", input) } return &dynamicRecipientSpec{ @@ -631,20 +494,6 @@ func parseDynamicRecipient(input string) (*dynamicRecipientSpec, error) { }, nil } -func searchUsersByAccount(account string) ([]*userclient.User, error) { - resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ - Account: account, - IdentityType: searchAllIdentityType, - }) - if err != nil { - return nil, err - } - if resp == nil { - return nil, nil - } - return resp.Users, nil -} - func searchUsersByEmail(email string) ([]*userclient.User, error) { resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ Email: email, @@ -684,3 +533,9 @@ func isLarkUserNotFoundErr(err error) bool { } return strings.Contains(strings.ToLower(err.Error()), "user not found") } + +func isLarkNotifyType(notifyType setting.NotifyWebHookType) bool { + return notifyType == setting.NotifyWebHookTypeFeishu || + notifyType == setting.NotifyWebhookTypeFeishuApp || + notifyType == setting.NotifyWebHookTypeFeishuPerson +} diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go index 087a996541..8c6016dd59 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go @@ -502,7 +502,7 @@ func resolveWorkflowNotifyDynamicRecipients(task *models.WorkflowTask, notify *m resolver := dynamicrecipient.NewResolver(keyMap) if cfg := notify.LarkHookNotificationConfig; cfg != nil { - users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID) if err != nil { return err } @@ -515,14 +515,14 @@ func resolveWorkflowNotifyDynamicRecipients(task *models.WorkflowTask, notify *m cfg.AtUsers = dynamicrecipient.UniqStrings(cfg.AtUsers) } if cfg := notify.LarkGroupNotificationConfig; cfg != nil { - users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID) if err != nil { return err } cfg.AtUsers = dynamicrecipient.UniqLarkUsers(append(cfg.AtUsers, users...)) } if cfg := notify.LarkPersonNotificationConfig; cfg != nil { - users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID) if err != nil { return err } diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index b3d68815ce..bf74e7a4c9 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -282,7 +282,7 @@ func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) resolver := dynamicrecipient.NewResolver(keyMap) if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { - users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID) if err != nil { return err } @@ -295,14 +295,14 @@ func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) cfg.AtUsers = lo.Uniq(cfg.AtUsers) } if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil { - users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID) if err != nil { return err } cfg.AtUsers = dynamicrecipient.UniqLarkUsers(append(cfg.AtUsers, users...)) } if cfg := c.jobTaskSpec.LarkPersonNotificationConfig; cfg != nil { - users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID) if err != nil { return err } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go index c83bb484d6..3273382256 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go @@ -67,19 +67,63 @@ func (j NotificationJobController) Validate(isExecution bool) error { if err := util.CheckZadigProfessionalLicense(); err != nil { return e.ErrLicenseInvalid.AddDesc("") } - if j.jobSpec.WebHookType == setting.NotifyWebHookTypeFeishu && j.jobSpec.LarkHookNotificationConfig != nil { - if err := dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig( - j.jobSpec.WebHookType, - j.jobSpec.LarkHookNotificationConfig.AppID, - []string(j.jobSpec.LarkHookNotificationConfig.DynamicRecipients), - ); err != nil { - return e.ErrLintWorkflow.AddDesc(err.Error()) - } + if err := validateNotificationJobDynamicRecipients(j.jobSpec); err != nil { + return e.ErrLintWorkflow.AddDesc(err.Error()) } return nil } +func validateNotificationJobDynamicRecipients(spec *commonmodels.NotificationJobSpec) error { + if spec == nil { + return nil + } + + validate := func(appID string, recipients commonmodels.DynamicRecipients) error { + return dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig(spec.WebHookType, appID, []string(recipients)) + } + + switch spec.WebHookType { + case setting.NotifyWebHookTypeFeishu: + if spec.LarkHookNotificationConfig == nil { + return nil + } + return validate(spec.LarkHookNotificationConfig.AppID, spec.LarkHookNotificationConfig.DynamicRecipients) + case setting.NotifyWebhookTypeFeishuApp: + if spec.LarkGroupNotificationConfig == nil { + return nil + } + return validate(spec.LarkGroupNotificationConfig.AppID, spec.LarkGroupNotificationConfig.DynamicRecipients) + case setting.NotifyWebHookTypeFeishuPerson: + if spec.LarkPersonNotificationConfig == nil { + return nil + } + return validate(spec.LarkPersonNotificationConfig.AppID, spec.LarkPersonNotificationConfig.DynamicRecipients) + case setting.NotifyWebHookTypeWechatWork: + if spec.WechatNotificationConfig == nil { + return nil + } + return validate("", spec.WechatNotificationConfig.DynamicRecipients) + case setting.NotifyWebHookTypeDingDing: + if spec.DingDingNotificationConfig == nil { + return nil + } + return validate("", spec.DingDingNotificationConfig.DynamicRecipients) + case setting.NotifyWebHookTypeMSTeam: + if spec.MSTeamsNotificationConfig == nil { + return nil + } + return validate("", spec.MSTeamsNotificationConfig.DynamicRecipients) + case setting.NotifyWebHookTypeMail: + if spec.MailNotificationConfig == nil { + return nil + } + return validate("", spec.MailNotificationConfig.DynamicRecipients) + default: + return nil + } +} + func (j NotificationJobController) Update(useUserInput bool, ticket *commonmodels.ApprovalTicket) error { currJob, err := j.workflow.FindJob(j.name, j.jobType) if err != nil { From e51cea0720dd5393dd57adabffa449b3a6cb2e0b Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Fri, 26 Jun 2026 09:38:16 +0800 Subject: [PATCH 21/22] chore: remove notification runtime tests from pr Signed-off-by: huanghongbo-hhb --- .../workflow_task_dynamic_recipient_test.go | 57 ----------- .../jobcontroller/job_notification_test.go | 94 ------------------- 2 files changed, 151 deletions(-) delete mode 100644 pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go delete mode 100644 pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go deleted file mode 100644 index faa52c0336..0000000000 --- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task_dynamic_recipient_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package instantmessage - -import ( - "testing" - "time" - - "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" - commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" - "github.com/koderover/zadig/v2/pkg/setting" -) - -func TestResolveWorkflowNotifyDynamicRecipientsSupportsPayloadEmail(t *testing.T) { - task := &commonmodels.WorkflowTask{ - ProjectName: "yaml", - ProjectDisplayName: "yaml", - TaskID: 350, - StartTime: time.Now().Unix(), - WorkflowArgs: &commonmodels.WorkflowV4{ - HookPayload: &commonmodels.HookPayload{ - Branch: "feature-1", - TargetBranch: "feature-1", - CommitID: "14d4e3a44d3a02a2c3e48dfb4aec4d5fe91df31a", - CommitSHA: "14d4e3a44d3a02a2c3e48dfb4aec4d5fe91df31a", - EventType: "push", - RawPayload: `{ - "head_commit": { - "author": { - "email": "huanghongbo@koderover.com" - } - } - }`, - }, - }, - } - - notify := &commonmodels.NotifyCtl{ - Enabled: true, - WebHookType: setting.NotifyWebHookTypeMail, - NotifyTypes: []string{string(config.StatusCreated)}, - MailNotificationConfig: &commonmodels.MailNotificationConfig{ - DynamicRecipients: commonmodels.DynamicRecipients{"{{.payload.head_commit.author.email}}"}, - }, - } - - if err := resolveWorkflowNotifyDynamicRecipients(task, notify); err != nil { - t.Fatalf("resolveWorkflowNotifyDynamicRecipients returned error: %v", err) - } - - if got := len(notify.MailNotificationConfig.TargetUsers); got != 1 { - t.Fatalf("expected 1 resolved mail target user, got %d", got) - } - - target := notify.MailNotificationConfig.TargetUsers[0] - if target == nil || target.Type != "email" || target.UserName != "huanghongbo@koderover.com" { - t.Fatalf("unexpected resolved target: %#v", target) - } -} diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go deleted file mode 100644 index daa6428319..0000000000 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package jobcontroller - -import ( - "testing" - - commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" - "github.com/koderover/zadig/v2/pkg/setting" -) - -func TestPrepareRuntimeNotificationFieldsSupportsPayloadDynamicRecipients(t *testing.T) { - ctl := &NotificationJobCtl{ - workflowCtx: &commonmodels.WorkflowTaskCtx{ - WorkflowKeyVals: []*commonmodels.KeyVal{ - {Key: "payload.user.email", Value: "dev@example.com"}, - }, - }, - jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ - WebHookType: setting.NotifyWebHookTypeMail, - MailNotificationConfig: &commonmodels.MailNotificationConfig{ - DynamicRecipients: commonmodels.DynamicRecipients{ - "{{.payload.user.email}}", - }, - }, - }, - } - - if err := ctl.prepareRuntimeNotificationFields(); err != nil { - t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) - } - - if len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers) != 1 { - t.Fatalf("expected 1 target user, got %d", len(ctl.jobTaskSpec.MailNotificationConfig.TargetUsers)) - } - - got := ctl.jobTaskSpec.MailNotificationConfig.TargetUsers[0] - if got == nil || got.Type != "email" || got.UserName != "dev@example.com" { - t.Fatalf("expected resolved payload email target user, got %#v", got) - } -} - -func TestPrepareRuntimeNotificationFieldsSupportsPayloadStaticRecipients(t *testing.T) { - ctl := &NotificationJobCtl{ - workflowCtx: &commonmodels.WorkflowTaskCtx{ - WorkflowKeyVals: []*commonmodels.KeyVal{ - {Key: "payload.reviewer.email", Value: "reviewer@example.com"}, - }, - }, - jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ - WebHookType: setting.NotifyWebHookTypeMSTeam, - MSTeamsNotificationConfig: &commonmodels.MSTeamsNotificationConfig{ - AtEmails: []string{"{{.payload.reviewer.email}}"}, - }, - }, - } - - if err := ctl.prepareRuntimeNotificationFields(); err != nil { - t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) - } - - if len(ctl.jobTaskSpec.MSTeamsNotificationConfig.AtEmails) != 1 { - t.Fatalf("expected 1 rendered email, got %d", len(ctl.jobTaskSpec.MSTeamsNotificationConfig.AtEmails)) - } - - if got := ctl.jobTaskSpec.MSTeamsNotificationConfig.AtEmails[0]; got != "reviewer@example.com" { - t.Fatalf("expected rendered payload email, got %q", got) - } -} - -func TestPrepareRuntimeNotificationFieldsDoesNotRenderPayloadInTitleOrContent(t *testing.T) { - ctl := &NotificationJobCtl{ - workflowCtx: &commonmodels.WorkflowTaskCtx{ - WorkflowKeyVals: []*commonmodels.KeyVal{ - {Key: "payload.user.email", Value: "dev@example.com"}, - {Key: "workflow.trigger.branch", Value: "feature/demo"}, - }, - }, - jobTaskSpec: &commonmodels.JobTaskNotificationSpec{ - Title: "branch={{.workflow.trigger.branch}} payload={{.payload.user.email}}", - Content: "branch={{.workflow.trigger.branch}} payload={{.payload.user.email}}", - }, - } - - if err := ctl.prepareRuntimeNotificationFields(); err != nil { - t.Fatalf("prepareRuntimeNotificationFields returned error: %v", err) - } - - want := "branch=feature/demo payload={{.payload.user.email}}" - if ctl.jobTaskSpec.Title != want { - t.Fatalf("expected title %q, got %q", want, ctl.jobTaskSpec.Title) - } - if ctl.jobTaskSpec.Content != want { - t.Fatalf("expected content %q, got %q", want, ctl.jobTaskSpec.Content) - } -} From 6d6f28b36cf8df4365d53216b01271cf7a9ef090 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Fri, 26 Jun 2026 09:52:23 +0800 Subject: [PATCH 22/22] fix: clarify notification runtime variables Signed-off-by: huanghongbo-hhb --- .../service/workflowcontroller/workflow.go | 23 ++++++++++--------- .../service/webhook/gitee_workflowv4_task.go | 2 -- .../service/webhook/github_workflowv4_task.go | 2 -- .../service/webhook/gitlab_workflowv4_task.go | 4 ++-- .../service/workflow/controller/workflow.go | 3 +++ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go index 5737a84780..90841358ab 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go @@ -261,17 +261,18 @@ func (c *workflowCtl) Run(ctx context.Context, concurrency int) { WorkflowTaskCreatorUserID: c.workflowTask.TaskCreatorID, WorkflowTaskCreatorMobile: c.workflowTask.TaskCreatorPhone, WorkflowTaskCreatorEmail: c.workflowTask.TaskCreatorEmail, - WorkflowKeyVals: commonutil.BuildWorkflowRuntimeVariableKVs(c.workflowTask.WorkflowArgs, c.workflowTask.ProjectName, c.workflowTask.ProjectDisplayName, c.workflowTask.TaskID, c.workflowTask.TaskCreator, c.workflowTask.TaskCreatorAccount, c.workflowTask.TaskCreatorID, time.Unix(c.workflowTask.StartTime, 0)), - Workspace: "/workspace", - DistDir: fmt.Sprintf("%s/%s/dist/%d", config.S3StoragePath(), c.workflowTask.WorkflowName, c.workflowTask.TaskID), - DockerMountDir: fmt.Sprintf("/tmp/%s/docker/%d", uuid.NewString(), time.Now().Unix()), - ConfigMapMountDir: fmt.Sprintf("/tmp/%s/cm/%d", uuid.NewString(), time.Now().Unix()), - GlobalContextGetAll: c.getGlobalContextAll, - GlobalContextGet: c.getGlobalContext, - GlobalContextSet: c.setGlobalContext, - GlobalContextEach: c.globalContextEach, - ClusterIDAdd: c.addClusterID, - StartTime: time.Now(), + // NotificationJobCtl uses these variables to render notification titles, content and recipient templates at send time. + WorkflowKeyVals: commonutil.BuildWorkflowRuntimeVariableKVs(c.workflowTask.WorkflowArgs, c.workflowTask.ProjectName, c.workflowTask.ProjectDisplayName, c.workflowTask.TaskID, c.workflowTask.TaskCreator, c.workflowTask.TaskCreatorAccount, c.workflowTask.TaskCreatorID, time.Unix(c.workflowTask.StartTime, 0)), + Workspace: "/workspace", + DistDir: fmt.Sprintf("%s/%s/dist/%d", config.S3StoragePath(), c.workflowTask.WorkflowName, c.workflowTask.TaskID), + DockerMountDir: fmt.Sprintf("/tmp/%s/docker/%d", uuid.NewString(), time.Now().Unix()), + ConfigMapMountDir: fmt.Sprintf("/tmp/%s/cm/%d", uuid.NewString(), time.Now().Unix()), + GlobalContextGetAll: c.getGlobalContextAll, + GlobalContextGet: c.getGlobalContext, + GlobalContextSet: c.setGlobalContext, + GlobalContextEach: c.globalContextEach, + ClusterIDAdd: c.addClusterID, + StartTime: time.Now(), } defer jobcontroller.CleanWorkflowJobs(ctx, c.workflowTask, workflowCtx, c.logger, c.ack) if err := scmnotify.NewService().UpdateWebhookCommentForWorkflowV4(c.workflowTask, c.logger); err != nil { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go index 285ef503b5..14a6af529a 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go @@ -136,7 +136,6 @@ func (gmem *giteeMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod TargetBranch: gmem.event.PullRequest.Base.Ref, PR: gmem.event.PullRequest.Number, CommitID: gmem.event.PullRequest.Head.Sha, - CommitMessage: gmem.event.PullRequest.Title, Committer: hookRepo.Committer, Source: hookRepo.Source, } @@ -286,7 +285,6 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque MergeRequestID: mergeRequestID, CommitID: commitID, CommitSHA: commitID, - CommitMessage: ev.PullRequest.Title, Committer: ev.PullRequest.User.Login, EventType: eventType, RawPayload: rawPayload, diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go index 4895a8c39b..05eb6a571d 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go @@ -136,7 +136,6 @@ func (gmem *githubMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo TargetBranch: *gmem.event.PullRequest.Base.Ref, PR: *gmem.event.PullRequest.Number, CommitID: *gmem.event.PullRequest.Head.SHA, - CommitMessage: *gmem.event.PullRequest.Title, Committer: hookRepo.Committer, Source: hookRepo.Source, } @@ -284,7 +283,6 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deli MergeRequestID: mergeRequestID, CommitID: commitID, CommitSHA: commitID, - CommitMessage: *ev.PullRequest.Title, Committer: *ev.PullRequest.User.Login, EventType: eventType, RawPayload: rawPayload, diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go index 5f69afcb63..8c70a6d9d7 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go @@ -105,7 +105,7 @@ func (gmem *gitlabMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo TargetBranch: gmem.event.ObjectAttributes.TargetBranch, PR: gmem.event.ObjectAttributes.IID, CommitID: gmem.event.ObjectAttributes.LastCommit.ID, - CommitMessage: gmem.event.ObjectAttributes.Title, + CommitMessage: gmem.event.ObjectAttributes.LastCommit.Message, Committer: hookRepo.Committer, Source: hookRepo.Source, } @@ -377,7 +377,7 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requ MergeRequestID: mergeRequestID, CommitID: commitID, CommitSHA: commitID, - CommitMessage: ev.ObjectAttributes.Title, + CommitMessage: ev.ObjectAttributes.LastCommit.Message, Committer: ev.User.Username, CodehostID: eventRepo.CodehostID, EventType: eventType, diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index eca5b95cea..b22184747c 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -260,6 +260,7 @@ func restoreNotificationDynamicRecipientsToTaskSpec(spec *commonmodels.JobTaskNo ) } +// RenderJobTaskWithGlobalVariables replays a task spec with persisted GlobalContext during retry/manual execution. func RenderJobTaskWithGlobalVariables(task *commonmodels.JobTask, globalKeyMap map[string]string) error { if task == nil { return nil @@ -267,6 +268,8 @@ func RenderJobTaskWithGlobalVariables(task *commonmodels.JobTask, globalKeyMap m var notificationRecipients *notificationDynamicRecipients if task.JobType == string(config.JobNotification) { + // DynamicRecipients must stay as templates until NotificationJobCtl resolves them with payload/user mapping. + // Rendering them here would turn {{.payload.user.email}} into a raw value and lose the identity suffix. spec := &commonmodels.JobTaskNotificationSpec{} if err := commonmodels.IToi(task.Spec, spec); err != nil { return fmt.Errorf("failed to decode notification task spec for task %s, error: %w", task.Name, err)