From 790fcdf0814b456d4077e2460be8515b04325f01 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 7 May 2026 14:37:15 +0800 Subject: [PATCH 01/18] feat(workflow): pass through webhook payload to runtime notifications Signed-off-by: huanghongbo-hhb (cherry picked from commit 4d0c078ff63ec1703416aa1c47eae8ebf8b01095) --- .../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 | 15 +- .../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, 908 insertions(+), 135 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 a1544c0a53..cb2a696d4d 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 @@ -783,6 +783,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 d9756ba13b..eb02584075 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -486,6 +486,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 dd7457eed5..622f2e1069 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1163,12 +1163,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"` @@ -1252,6 +1252,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) @@ -1260,44 +1264,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 9ef0ec1b5d..45cf6e42e4 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 ( @@ -239,6 +239,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 ff7117c8c0..a649f964bb 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 @@ -173,6 +173,8 @@ func (gruem *gerritChangeMergedEventMatcherForWorkflowV4) GetHookRepo(hookRepo * RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -223,7 +225,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, } } @@ -361,6 +365,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 c5bf677f35..0aed7a1df0 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 @@ -87,6 +87,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, } } @@ -139,7 +141,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, } } @@ -173,7 +177,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, } } @@ -206,7 +212,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) @@ -280,6 +286,7 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, MergeRequestID: mergeRequestID, CommitID: commitID, EventType: eventType, + RawPayload: rawPayload, } case *gitee.PushEvent: eventType = EventTypePush @@ -297,11 +304,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 699358b3a6..b33f29439c 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 @@ -25,7 +25,6 @@ import ( "github.com/hashicorp/go-multierror" "go.uber.org/zap" - internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "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" @@ -33,6 +32,7 @@ import ( workflowservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow/controller" "github.com/koderover/zadig/v2/pkg/setting" + internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" "github.com/koderover/zadig/v2/pkg/types" ) @@ -86,8 +86,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, } } @@ -143,9 +145,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, } } @@ -182,7 +186,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, } } @@ -215,7 +221,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) @@ -284,6 +290,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() != "" { @@ -302,12 +309,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 1c896aa053..520d728773 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 @@ -108,7 +108,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, } } @@ -231,6 +233,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, } } @@ -268,12 +272,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) @@ -370,6 +376,7 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string CommitID: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, + RawPayload: rawPayload, } case *gitlab.PushEvent: eventType = EventTypePush @@ -387,11 +394,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 03f6f8bad7..57c7f70dca 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 @@ -508,64 +508,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 cc48b3b19e..35bd00fa0b 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -391,6 +391,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 ddc0b45bea..dfb8206e5a 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go @@ -158,6 +158,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 @@ -169,36 +174,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 3bff0a6920..f6d344af16 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 @@ -668,6 +668,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) @@ -697,6 +698,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 { @@ -712,9 +727,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 @@ -725,7 +741,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) @@ -747,7 +764,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, }, @@ -770,9 +788,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 @@ -783,9 +802,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 @@ -796,8 +816,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 @@ -808,7 +829,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 { @@ -842,6 +864,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 { @@ -919,7 +969,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 { @@ -971,6 +1030,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 { @@ -1052,7 +1112,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 { @@ -1125,6 +1194,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 9b6fa0da66e3d7e65b671f210ea0d523068c5b3f Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Mon, 11 May 2026 09:55:52 +0800 Subject: [PATCH 02/18] fix(workflow): preserve runtime context job outputs Signed-off-by: huanghongbo-hhb (cherry picked from commit c29c05cf90dbdc0d4cf0b389c7e69b6da70da243) --- .../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 f6d344af16..423eed074d 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 @@ -865,10 +865,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, @@ -880,7 +889,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 55e2117da3c2a3ed16c9878d23848a98822a0bd8 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 12 May 2026 11:40:30 +0800 Subject: [PATCH 03/18] fix(workflow): preserve webhook repo fields across scm providers Signed-off-by: huanghongbo-hhb (cherry picked from commit 29630317b695820d2d6eeb99d217e0e9cb200ef8) --- .../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 a649f964bb..bc6eaedab9 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 @@ -174,6 +174,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, } @@ -227,6 +229,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 0aed7a1df0..5bac23d81f 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 @@ -81,6 +81,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, @@ -88,6 +92,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, } @@ -143,6 +149,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 520d728773..be3779715f 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 @@ -110,6 +110,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, } @@ -227,6 +229,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, @@ -234,6 +240,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 848cea549b..d0e64aeba8 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 @@ -1128,6 +1128,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 11dd783799a57bee8ca7a62ac2b2238f3e75a700 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Fri, 5 Jun 2026 10:48:14 +0800 Subject: [PATCH 04/18] fix(workflow): keep only payload runtime variables Signed-off-by: huanghongbo-hhb (cherry picked from commit 5fc72344f38fbb9b52c12cc76b5d5f640533ce55) --- .../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 57c7f70dca..03f6f8bad7 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 @@ -508,9 +508,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 412e26db62a8fa510c59c8c9f731e2289526ee8c Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 16 Jun 2026 16:28:25 +0800 Subject: [PATCH 05/18] support workflow trigger runtime variables Signed-off-by: huanghongbo-hhb (cherry picked from commit aead8d2955b69c04af5c2af5b52f0517393c94e1) --- .../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 + pr4667-docs/api.md | 493 ++++++++++++++++++ pr4667-docs/design.md | 330 ++++++++++++ pr4667-docs/requirement.md | 139 +++++ 11 files changed, 1139 insertions(+), 92 deletions(-) create mode 100644 pr4667-docs/api.md create mode 100644 pr4667-docs/design.md create mode 100644 pr4667-docs/requirement.md diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow.go b/pkg/microservice/aslan/core/common/repository/models/workflow.go index eb02584075..19541049d8 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -478,11 +478,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 bc6eaedab9..9a34f9c9bd 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 @@ -174,6 +174,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, @@ -275,7 +276,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) @@ -298,6 +298,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba continue } for _, item := range gitHooks { + var hookPayload *commonmodels.HookPayload if !item.Enabled { continue } @@ -329,7 +330,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. @@ -360,17 +362,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 5bac23d81f..a64fdb7da7 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 @@ -290,9 +290,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, } @@ -304,21 +307,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 b33f29439c..85aeadaac4 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,12 +283,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, } @@ -303,11 +306,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, } @@ -315,8 +322,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 be3779715f..db33c136e1 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 @@ -379,9 +379,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, @@ -394,21 +397,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 35bd00fa0b..31314eadf0 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -392,6 +392,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, @@ -404,6 +412,66 @@ func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid return resp, nil } +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") @@ -643,61 +711,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.Name, - 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 4a3e14b654..7ae6eb5560 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go @@ -2617,6 +2617,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 diff --git a/pr4667-docs/api.md b/pr4667-docs/api.md new file mode 100644 index 0000000000..bdd7075a42 --- /dev/null +++ b/pr4667-docs/api.md @@ -0,0 +1,493 @@ +# PR 4667 API 文档:Payload 透传与动态通知人 + +## 1. 动态通知人配置 + +### 接口范围 + +动态通知人能力用于工作流任务运行时通知配置,主要影响以下接口: + +| 场景 | 路由 | +| --- | --- | +| Zadig OpenAPI 创建自定义工作流任务 | `POST /api/aslan/workflow/openapi/custom/task` | +| Zadig 控制台创建工作流任务 | `POST /api/aslan/workflow/v4/workflowtask` | +| 工作流任务手动执行 / 重试 | 复用任务内已保存的通知配置和 runtime context | + +说明:实际网关前缀以部署环境为准,表中展示的是 Aslan workflow router 下的路径语义。 + +### 请求字段 + +`notify_inputs` 中各通知配置新增或变更 `dynamic_recipients` 字段。 + +字段统一为字符串数组: + +```json +{ + "notify_inputs": [ + { + "id": 0, + "type": "mail", + "mail_notification_config": { + "dynamic_recipients": [ + "{{.payload.user.email}}" + ] + } + } + ] +} +``` + +不再支持新写入以下结构: + +```json +{ + "dynamic_recipients": [ + { + "value": "{{.payload.user.email}}", + "identity_type": "email" + } + ] +} +``` + +旧结构仅用于服务端读取兼容,不作为新 API 契约。 + +### `notify_inputs` 基本结构 + +```json +{ + "workflow_key": "workflow-demo", + "project_key": "project-demo", + "parameters": [], + "inputs": [], + "notify_inputs": [ + { + "id": 0, + "type": "mail", + "enabled": true, + "mail_notification_config": { + "users": [], + "user_ids": [], + "dynamic_recipients": [ + "{{.payload.commits.0.author.email}}" + ] + } + } + ] +} +``` + +字段说明: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `id` | integer | 是 | 工作流通知配置下标,从 `0` 开始 | +| `type` | string | 是 | 通知类型,必须和工作流中第 `id` 个通知配置类型一致 | +| `enabled` | boolean | 否 | 运行时是否启用该通知;不传则保持原配置 | +| `*_notification_config.dynamic_recipients` | string[] | 否 | 动态通知人模板变量 | + +### 支持的通知类型 + +| `type` | 配置字段 | +| --- | --- | +| `feishu` | `lark_hook_notification_config` | +| `feishu_app` | `lark_group_notification_config` | +| `feishu_person` | `lark_person_notification_config` | +| `wechat_work` | `wechat_notification_config` | +| `dingding` | `dingding_notification_config` | +| `msteams` | `msteams_notification_config` | +| `mail` | `mail_notification_config` | + +## 2. 动态通知人变量规则 + +### 格式 + +每个动态通知人必须是单个模板变量: + +```text +{{.payload.user.email}} +``` + +不支持: + +```text +payload.user.email +user@example.com +{{.payload.user.email}} <{{.payload.user.name}}> +``` + +### 支持后缀 + +后端根据最后一级字段名识别身份类型。 + +| 后缀 | 识别类型 | +| --- | --- | +| `email` | 邮箱 | +| `mobile` / `phone` | 手机号 | +| `account` | Zadig 用户账号 | +| `user_id` / `userid` | 渠道 user_id | +| `open_id` | 飞书 open_id | + +示例: + +| 变量 | 识别结果 | +| --- | --- | +| `{{.payload.user.email}}` | `email` | +| `{{.payload.user.phone}}` | `mobile` | +| `{{.payload.user.account}}` | `account` | +| `{{.payload.user.user_id}}` | `user_id` | +| `{{.payload.user.open_id}}` | `open_id` | + +`{{.payload.user.email_address}}` 不支持,因为最后一级字段名不是 `email`。 + +### 渠道支持矩阵 + +| 通知类型 | 支持的变量后缀 | +| --- | --- | +| `feishu` | `user_id` | +| `feishu_app` | `email`、`mobile/phone`、`account`、`user_id` | +| `feishu_person` | `email`、`mobile/phone`、`account`、`user_id`、`open_id` | +| `wechat_work` | `user_id` | +| `dingding` | `email`、`mobile/phone`、`account` | +| `msteams` | `email`、`mobile/phone`、`account` | +| `mail` | `email`、`mobile/phone`、`account` | + +## 3. 不同渠道请求示例 + +### 邮件通知 + +```json +{ + "id": 0, + "type": "mail", + "mail_notification_config": { + "dynamic_recipients": [ + "{{.payload.author.email}}", + "{{.payload.reviewer.account}}" + ] + } +} +``` + +解析行为: + +| 输入类型 | 行为 | +| --- | --- | +| `email` | 直接作为邮件接收人 | +| `mobile/phone` | 查 Zadig 用户,取邮箱 | +| `account` | 查 Zadig 用户,取邮箱 | + +### 钉钉通知 + +```json +{ + "id": 1, + "type": "dingding", + "dingding_notification_config": { + "dynamic_recipients": [ + "{{.payload.author.email}}", + "{{.payload.owner.mobile}}" + ] + } +} +``` + +解析行为: + +| 输入类型 | 行为 | +| --- | --- | +| `mobile/phone` | 直接作为 `at_mobiles` | +| `email` | 查 Zadig 用户,取手机号 | +| `account` | 查 Zadig 用户,取手机号 | + +### 飞书群通知(自建应用) + +```json +{ + "id": 2, + "type": "feishu_app", + "lark_group_notification_config": { + "chat_id": "oc_xxx", + "dynamic_recipients": [ + "{{.payload.author.email}}", + "{{.payload.reviewer.user_id}}" + ] + } +} +``` + +解析行为: + +| 输入类型 | 行为 | +| --- | --- | +| `user_id` | 直接作为飞书 user_id | +| `email` | 优先用邮箱查飞书 user_id;查不到则查 Zadig 用户手机号再查飞书 user_id | +| `mobile/phone` | 优先用手机号查飞书 user_id;查不到则查 Zadig 用户邮箱再查飞书 user_id | +| `account` | 查 Zadig 用户,再用邮箱/手机号查飞书 user_id | + +`feishu_app` 不支持 `open_id`。 + +### 飞书个人通知 + +```json +{ + "id": 3, + "type": "feishu_person", + "lark_person_notification_config": { + "dynamic_recipients": [ + "{{.payload.author.open_id}}", + "{{.payload.reviewer.email}}" + ] + } +} +``` + +`feishu_person` 支持 `open_id` 和 `user_id`。 + +### 飞书机器人 / 企业微信机器人 + +```json +{ + "id": 4, + "type": "feishu", + "lark_hook_notification_config": { + "dynamic_recipients": [ + "{{.payload.author.user_id}}" + ] + } +} +``` + +`feishu` 和 `wechat_work` 只支持 `user_id`,不做邮箱/手机号/账号转换。 + +## 4. Webhook Payload 透传变量 + +### 数据来源 + +GitHub、GitLab、Gitee、Gerrit 触发 workflow v4 时,服务端会保存原始 webhook request body 到任务的 hook payload 中: + +```go +HookPayload.RawPayload +``` + +运行时变量渲染时,后端将 `RawPayload` 解析为 JSON,并 flatten 成 `payload.*` 变量。 + +同时,代码仓库上下文信息不会因为引入 payload 透传而丢失,运行时统一暴露以下 `workflow.trigger.*` 变量: + +| 变量 | 说明 | +| --- | --- | +| `workflow.trigger.branch` | 当前触发分支语义 | +| `workflow.trigger.target_branch` | PR/MR 目标分支,或 push/tag 匹配后的目标分支语义 | +| `workflow.trigger.pr` | PR / MR 编号 | +| `workflow.trigger.commit_id` | 触发本次任务的 commit ID | +| `workflow.trigger.commit_message` | 触发提交消息或 PR 标题 | +| `workflow.trigger.committer` | 触发人 / 提交人 | +| `workflow.trigger.event` | 事件类型 | + +这些信息用于已有 repo/runtime 变量链路,以及重试、手动执行等场景。 + +说明:当前通知 job 直接使用的渲染变量来自 `BuildWorkflowRuntimeVariableKVs`,明确包括 `payload.*`、`workflow.task.*`、`workflow.params.*`、`workflow.trigger.*` 以及 project/workflow 基础变量。 + +示例: + +```text +{{.workflow.trigger.branch}} +{{.workflow.trigger.target_branch}} +{{.workflow.trigger.pr}} +{{.workflow.trigger.commit_id}} +{{.workflow.trigger.commit_message}} +{{.workflow.trigger.committer}} +{{.workflow.trigger.event}} +``` + +### Flatten 规则 + +| JSON 类型 | 变量生成规则 | +| --- | --- | +| object | 用字段名继续展开 | +| array | 用数字下标展开 | +| string/number/bool | 生成叶子变量 | +| null | 不生成变量 | + +示例 payload: + +```json +{ + "user": { + "email": "dev@example.com", + "mobile": "13800000000" + }, + "commits": [ + { + "author": { + "email": "author@example.com" + } + } + ] +} +``` + +可用变量: + +```text +{{.payload.user.email}} +{{.payload.user.mobile}} +{{.payload.commits.0.author.email}} +``` + +### 存储说明 + +`payload.*` 变量不会持久化到 `GlobalContext`。原因是 raw payload 已经保存在 `HookPayload.RawPayload`,运行时按需解析即可,避免在 MongoDB 中重复存储大量 payload 展开字段。 + +代码仓库上下文信息则继续沿用已有 workflow/task 数据结构保存和重建,不依赖把所有 `payload.*` 展开后落库。 + +### 重试 / 手动执行行为 + +对于 webhook 触发的任务: + +1. 首次执行时,`payload.*` 来源于 `HookPayload.RawPayload`。 +2. 重试时,会重建 runtime context,继续使用同一份 hook payload 和已保存的代码仓库上下文。 +3. 手动执行阶段时,也会重建 runtime context,确保动态通知人仍能基于 payload 变量渲染,并保留代码仓库上下文供已有任务链路复用。 + +因此,以下两类变量都应继续可用: + +```text +{{.payload.user.email}} +{{.payload.commits.0.author.email}} +{{.workflow.trigger.target_branch}} +{{.workflow.trigger.commit_id}} +``` + +以及已有的代码仓库相关 runtime/repo 语义会继续保留在 workflow/task 链路中,供已有代码逻辑复用。 + +## 5. 用户搜索 API 扩展 + +### 接口 + +```text +POST /api/v1/users/search +``` + +### 请求体 + +```json +{ + "email": "dev@example.com" +} +``` + +或: + +```json +{ + "phone": "13800000000" +} +``` + +或跨身份源账号查询: + +```json +{ + "account": "dev01", + "identity_type": "*" +} +``` + +### 查询优先级 + +当请求体同时包含多个条件时,后端按以下顺序选择一种查询: + +1. `uids` +2. `email` +3. `phone` +4. `account` +5. 列表查询 + +### 响应体 + +```json +{ + "users": [ + { + "uid": "user-uid", + "name": "Dev User", + "email": "dev@example.com", + "phone": "13800000000", + "identity_type": "system", + "account": "dev01" + } + ], + "totalCount": 1 +} +``` + +说明:动态通知人解析内部会复用该用户查询能力,用于邮箱、手机号、账号之间的转换。 + +## 6. 错误行为 + +### 非单变量格式 + +请求: + +```json +{ + "dynamic_recipients": ["dev@example.com"] +} +``` + +结果:创建任务失败,错误信息类似: + +```text +dynamic recipient must be a single template variable, got dev@example.com +``` + +### 不支持的字段后缀 + +请求: + +```json +{ + "dynamic_recipients": ["{{.payload.user.email_address}}"] +} +``` + +结果:创建任务失败,错误信息会提示允许的后缀: + +```text +only email/mobile(phone)/account/user_id(userid)/open_id are allowed +``` + +### 渠道不支持该类型 + +请求: + +```json +{ + "type": "feishu_app", + "lark_group_notification_config": { + "dynamic_recipients": ["{{.payload.user.open_id}}"] + } +} +``` + +结果:创建任务失败,因为飞书群通知不支持 `open_id`。 + +## 7. 兼容性 + +服务端读取 workflow/task 中的旧中间态数据时,兼容以下结构: + +```json +{ + "dynamic_recipients": [ + { + "value": "{{.payload.user.email}}", + "identity_type": "email" + } + ] +} +``` + +兼容行为: + +1. 只读取 `value`。 +2. 丢弃 `identity_type`。 +3. 后续仍按新规则校验和解析。 diff --git a/pr4667-docs/design.md b/pr4667-docs/design.md new file mode 100644 index 0000000000..1fe4b9b4d0 --- /dev/null +++ b/pr4667-docs/design.md @@ -0,0 +1,330 @@ +# PR 4667 设计文档:工作流动态通知人解析与渠道身份转换 + +## 设计概述 + +本 PR 将通知配置中的动态通知人从结构体数组改为字符串数组,用户只需要配置 payload 模板变量: + +```json +{ + "dynamic_recipients": ["{{.payload.user.email}}"] +} +``` + +后端通过变量最后一级字段名推断身份类型,例如 `email`、`mobile`、`account`、`user_id`、`open_id`。解析过程不做盲猜,而是在创建任务提交 `notify_inputs` 时做格式和渠道能力校验,在任务运行时按确定类型完成渠道转换。 + +## 接口设计 + +### 输入结构 + +所有通知配置中的 `dynamic_recipients` 统一为 `[]string`。 + +示例: + +```json +{ + "mail_notification_config": { + "dynamic_recipients": [ + "{{.payload.commit.author.email}}" + ] + } +} +``` + +旧结构已废弃: + +```json +{ + "dynamic_recipients": [ + { + "value": "{{.payload.commit.author.email}}", + "identity_type": "email" + } + ] +} +``` + +### 校验规则 + +`dynamic_recipients` 中每一项必须满足: + +1. 是单个模板变量,格式为 `{{.xxx.xxx}}`。 +2. 最后一级字段名在允许列表中。 +3. 字段后缀对应的身份类型被当前通知渠道支持。 + +允许后缀: + +| 后缀 | 内部类型 | +| --- | --- | +| `email` | `email` | +| `mobile` / `phone` | `mobile` | +| `account` | `account` | +| `user_id` / `userid` | `user_id` | +| `open_id` | `open_id` | + +## 渠道能力矩阵 + +| 渠道 | 内部通知类型 | 支持类型 | 输出目标 | +| --- | --- | --- | --- | +| 飞书机器人 Webhook | `feishu` | `user_id` | `at_users` | +| 飞书群通知 | `feishu_app` | `email`、`mobile`、`account`、`user_id` | `AtUsers`,ID type 为 user_id | +| 飞书个人通知 | `feishu_person` | `email`、`mobile`、`account`、`user_id`、`open_id` | `TargetUsers`,支持 user_id/open_id | +| 企业微信 Webhook | `wechat_work` | `user_id` | `at_users` | +| 钉钉 | `dingding` | `email`、`mobile`、`account` | `at_mobiles` | +| Microsoft Teams | `msteams` | `email`、`mobile`、`account` | `at_emails` | +| 邮件 | `mail` | `email`、`mobile`、`account` | `target_users` | + +飞书群通知不允许 `open_id`,因为运行路径按 user_id 发送;飞书个人通知允许 `open_id`。 + +## 数据流 + +1. 前端或 API 提交通知配置。 +2. `updateNotifyCtls` 裁剪空字符串,并调用 `ValidateDynamicRecipientsForNotifyType` 做边界校验。 +3. 通知配置持久化到 workflow/task 的通知配置中。 +4. Webhook 触发 workflow v4 时,原始 request body 保存到 `HookPayload.RawPayload`,同时仓库上下文中的 `branch`、`target_branch`、`pr`、`commit_id`、`commit_message`、`committer`、`event` 等信息保留到 `HookPayload` 中。 +5. 工作流任务运行时构造 runtime context;通知渲染直接使用 `payload.*`、`workflow.task.*`、`workflow.params.*`、`workflow.trigger.*` 以及 project/workflow 基础变量。 +5. `NotificationJobCtl.resolveDynamicRecipients` 按通知渠道调用对应 resolver。 +6. resolver 渲染模板变量,得到实际值。 +7. resolver 按变量后缀和渠道能力转换目标人。 +8. 转换结果去重后合并到静态通知人列表。 +9. 后续发送逻辑复用原有通知发送链路。 + +## 代码信息透传设计 + +### 透传内容 + +本次 PR 中“代码信息透传”不是只透传原始 payload,而是分成两层: + +1. 原始 webhook body 以 `HookPayload.RawPayload` 保存。 +2. 代码仓库上下文信息继续保留在 workflow/task 运行链路中,包括: + - `branch` + - `tag` + - `pr` + - `commit_id` + - `commit_message` + - `committer` + +这些信息一部分来自 webhook matcher 对仓库对象的补全,一部分来自 `HookPayload` 字段持久化;运行时统一通过 `workflow.trigger.*` 暴露给通知模板和工作流变量引用。 + +### 统一运行时变量 + +当前 PR 统一暴露以下触发变量: + +| 变量 | 说明 | +| --- | --- | +| `workflow.trigger.branch` | 触发分支 | +| `workflow.trigger.target_branch` | 目标分支,PR/MR 场景为目标分支,push/tag 场景为匹配分支语义 | +| `workflow.trigger.pr` | PR / MR 编号 | +| `workflow.trigger.commit_id` | 触发本次任务的 commit 标识 | +| `workflow.trigger.commit_message` | 提交消息或 PR 标题 | +| `workflow.trigger.committer` | 提交人 / 触发人 | +| `workflow.trigger.event` | 统一事件标识;GitHub/GitLab/Gitee 为 `pr` / `push` / `tag`,Gerrit 为原始事件类型 | + +### payload 变量生成 + +运行时渲染阶段,后端将 `HookPayload.RawPayload` 解析为 JSON,并 flatten 为 `payload.*` 变量,例如: + +```text +{{.payload.user.email}} +{{.payload.commits.0.author.email}} +``` + +这部分变量是运行时按需生成的,不直接展开后持久化进 MongoDB。 + +### 代码仓库上下文保留 + +对于已有 repo 相关运行时语义,设计上继续保留原有行为,不让 payload 透传破坏已有代码任务链路。当前保留的典型信息包括: + +| 信息 | 用途 | +| --- | --- | +| `workflow.trigger.branch` | 构建、部署等已有代码任务中的分支语义 | +| `workflow.trigger.target_branch` | PR / MR 目标分支,或匹配后的触发分支语义 | +| `workflow.trigger.pr` | PR / MR 场景复用 | +| `workflow.trigger.commit_id` | 定位具体提交 | +| `workflow.trigger.commit_message` | 从提交消息中继续提取环境变量片段 | +| `workflow.trigger.committer` | 代码触发链路中的触发人标识 | +| `workflow.trigger.event` | 按渠道/代码源区分事件类型 | + +说明:这组变量通过 `BuildWorkflowRuntimeVariableKVs`、`getWorkflowDefaultParams` 和变量引用列表统一暴露,因此通知 job 渲染、默认参数渲染、变量选择面板保持一致。 + +### 重试和手动执行 + +本次设计要求代码信息透传不仅在首次 webhook 触发时可用,还要在以下场景继续可用: + +1. `RetryWorkflowTaskV4` +2. `ManualExecWorkflowTaskV4` + +实现方式是重建任务 runtime context,并复用 workflow args、hook payload 和已有 global/job output 信息,而不是只依赖首次执行现场内存数据。 + +## 转换策略 + +### 邮件 / Teams + +目标需要邮箱。 + +| 输入类型 | 转换 | +| --- | --- | +| `email` | 直接作为邮箱 | +| `mobile` | 按手机号查询 Zadig 用户,取用户邮箱 | +| `account` | 按账号查询 Zadig 用户,取用户邮箱 | + +### 钉钉 + +目标需要手机号。 + +| 输入类型 | 转换 | +| --- | --- | +| `mobile` | 直接作为手机号 | +| `email` | 按邮箱查询 Zadig 用户,取用户手机号 | +| `account` | 按账号查询 Zadig 用户,取用户手机号 | + +### 飞书群 / 飞书个人 + +目标主要使用飞书 user_id。 + +| 输入类型 | 转换 | +| --- | --- | +| `user_id` | 直接作为飞书 user_id | +| `open_id` | 仅飞书个人通知允许,直接作为 open_id | +| `email` | 先用邮箱查询飞书 user_id;查不到时按邮箱查 Zadig 用户,再用手机号查询飞书 user_id | +| `mobile` | 先用手机号查询飞书 user_id;查不到时按手机号查 Zadig 用户,再用邮箱查询飞书 user_id | +| `account` | 按账号查 Zadig 用户,再依次用邮箱、手机号查询飞书 user_id | + +### 飞书机器人 Webhook / 企业微信 Webhook + +只支持 `user_id`,不做用户信息转换。 + +## 用户查询扩展 + +`/users/search` 新增支持按 `email` 和 `phone` 查询用户。 + +查询优先级: + +1. `uids` +2. `email` +3. `phone` +4. `account` +5. 常规列表查询 + +账号查询支持 `identity_type="*"`,用于跨身份源查找同一账号。动态通知人解析使用该能力,避免调用方必须知道用户来自本地、LDAP、OAuth 或其它身份源。 + +## 存储与兼容 + +### 新结构 + +持久化字段为: + +```go +type DynamicRecipients []string +``` + +各通知配置仍使用字段名 `dynamic_recipients`。 + +Webhook 透传部分沿用已有 `HookPayload` 结构,其中 `RawPayload` 用于保存原始 Webhook JSON,请求运行时再按需展开为 `payload.*` 变量。 + +### 旧中间结构兼容 + +为了兼容 PR 中间态已经落库的数据,`DynamicRecipients` 支持反序列化: + +```json +[ + { + "value": "{{.payload.user.email}}", + "identity_type": "email" + } +] +``` + +兼容策略: + +1. JSON/BSON 读取时优先按 `[]string` 解码。 +2. 解码失败后按旧结构解码。 +3. 旧结构只保留 `value`。 +4. 不继续使用 `identity_type`,避免废弃 schema 变成新契约。 + +## 性能设计 + +### 避免长查询链 + +本设计不对同一个输入做 email、phone、account、user_id 的盲猜。变量后缀决定身份类型,因此查询链是确定的。 + +### 请求级缓存 + +单次通知解析内缓存: + +1. account -> Zadig users +2. email -> Zadig users +3. phone -> Zadig users +4. appID + queryType + value -> 飞书 user_id +5. 飞书 user_id miss 结果 +6. appID -> 飞书 client + +这样同一个通知配置中多处引用相同用户信息时,不会重复打用户服务或飞书接口。 + +### 数据库索引 + +新增索引: + +| 字段 | 索引名 | +| --- | --- | +| `user.email` | `idx_email` | +| `user.phone` | `idx_phone` | + +覆盖范围: + +1. MySQL 初始化 SQL。 +2. 达梦初始化 SQL。 +3. GORM model tag。 +4. 4.3.0 upgradeassistant 迁移,用于已有线上库补索引。 + +## 错误处理 + +1. 配置格式不合法时,在创建任务并提交 `notify_inputs` 阶段返回错误。 +2. 渠道不支持某类型时,在创建任务并提交 `notify_inputs` 阶段返回错误。 +3. 模板运行时渲染为空或未解析完成时跳过该动态收件人。 +4. 外部查询报错时返回错误,避免静默丢通知。 +5. 查询不到用户或用户缺少必要字段时跳过该用户,不阻断其它通知人。 + +## 取舍说明 + +### 为什么去掉 `identity_type` + +保留 `identity_type` 会要求用户同时配置“变量值”和“变量含义”,例如: + +```json +{ + "value": "{{.payload.user.email}}", + "identity_type": "email" +} +``` + +这增加了配置负担,也容易出现 `value` 是邮箱但 `identity_type` 写成手机号的冲突。当前设计改为通过字段后缀表达语义,用户只需要配置一个字符串变量。 + +### 为什么不支持任意变量名 + +例如 `{{.payload.contact.email_address}}` 无法通过后缀识别身份类型。为了避免后端猜测和长查询链,当前要求用户使用约定后缀,例如 `email`、`mobile`、`account`。 + +### 为什么旧结构只兼容读取 `value` + +`identity_type` 是 PR 中间态字段,不是最终产品契约。继续使用它会把临时 schema 固化为正式行为,因此只做读取兼容,不在运行时继续依赖它。 + +## 风险与缓解 + +| 风险 | 影响 | 缓解 | +| --- | --- | --- | +| 用户 payload 字段命名不符合后缀约定 | 配置校验失败或运行时报错 | 错误信息列出允许后缀,文档明确命名规范 | +| 代码信息只在首次 webhook 触发时可用,重试/手动执行丢失 | 代码相关任务链路行为不一致 | 重建 runtime context 并复用 hook payload / workflow args | +| Zadig 用户缺少邮箱或手机号 | 无法转换到部分渠道 | 跳过该用户,不影响其它可解析用户 | +| 飞书外部接口查询失败 | 当前通知任务返回错误 | 复用原有错误链路,保留可观测错误 | +| 已有库缺少 email/phone 索引 | 查询可能全表扫 | 初始化 SQL 和 upgradeassistant 迁移补索引 | +| PR 中间态旧数据 schema 不一致 | 老任务重试可能读取失败 | `DynamicRecipients` 自定义 JSON/BSON 解码兼容 | + +## 验证建议 + +1. 通过 Webhook payload 提供 `payload.user.email`,配置邮件动态通知人,确认邮件目标包含该邮箱。 +2. 通过 GitHub/GitLab/Gitee/Gerrit webhook 触发任务,确认 `payload.*` 和 `workflow.trigger.*` 可用于通知变量渲染,且这些代码信息在运行时仍可被已有代码逻辑消费。 +2. 通过同一邮箱查到 Zadig 用户手机号,配置钉钉动态通知人,确认 `at_mobiles` 被补充。 +3. 配置飞书个人通知 `{{.payload.user.open_id}}`,确认允许保存并可通知。 +4. 配置飞书群通知 `{{.payload.user.open_id}}`,确认创建任务时报错。 +5. 模拟旧结构 `{value, identity_type}` 存量数据,确认可反序列化并继续运行。 +6. 检查新库和升级库均存在 `idx_email`、`idx_phone`。 +7. 在重试和手动执行阶段,再次验证 `payload.*` 仍可用于通知变量渲染,代码仓库上下文仍可用于已有任务链路重建。 diff --git a/pr4667-docs/requirement.md b/pr4667-docs/requirement.md new file mode 100644 index 0000000000..989e55f138 --- /dev/null +++ b/pr4667-docs/requirement.md @@ -0,0 +1,139 @@ +# PR 4667 需求文档:工作流 Payload 信息透传与动态通知人 + +## 背景 + +当前工作流支持在 Webhook 触发后执行任务,但通知对象主要依赖静态配置,无法直接从 Webhook payload 中提取代码 author、触发人或业务字段作为通知人。对于代码提交、合并请求、外部系统事件等场景,用户希望工作流执行完成后,IM 或邮件通知能够自动发送给 payload 中指定的人。 + +不同通知渠道对“人”的标识要求不同。例如飞书可能需要 user_id/open_id,钉钉需要手机号,邮件需要邮箱。如果要求用户在配置时额外声明 `identity_type`,前端和用户理解成本较高,也容易配置错误。因此本需求改为:用户只配置 payload 模板变量,后端根据变量字段后缀和通知渠道能力做确定性解析和转换。 + +## 目标 + +1. 工作流通知配置支持从 payload 中读取动态通知人。 +2. 用户无需配置 `identity_type`,只填写字符串模板变量,例如 `{{.payload.user.email}}`。 +3. 后端根据变量后缀识别身份类型,并按通知渠道转换成该渠道可发送的目标人标识。 +4. 支持代码信息透传场景,任务执行阶段可以使用 Webhook payload 生成运行时变量,并用于通知人解析。 +5. Webhook 触发的代码仓库上下文信息需要在任务生命周期内保留,并统一暴露为 `workflow.trigger.*` 运行时变量,包括分支、目标分支、PR、commit ID、commit message、committer、event 等,确保通知渲染、已有代码相关任务逻辑、重试和手动执行都可复用这些信息。 +6. 对历史 PR 中间态数据保持读取兼容,避免已有任务重试或手动执行时因 schema 变化失败。 + +## 非目标 + +1. 不支持任意字符串作为动态通知人,例如 `someaccount`。 +2. 不支持要求后端盲猜输入是邮箱、手机号、账号还是渠道 ID。 +3. 不引入新的 `identity_type` 配置项。 +4. 不保证每个渠道都支持所有身份类型,按渠道能力分别限制。 +5. 不处理通知渠道外部账号体系本身不完整的问题,例如 Zadig 用户未填写手机号时无法发送钉钉通知。 + +## 用户配置方式 + +动态通知人字段统一为字符串数组: + +```json +{ + "dynamic_recipients": [ + "{{.payload.user.email}}", + "{{.payload.owner.mobile}}", + "{{.payload.author.account}}" + ] +} +``` + +模板变量必须是单个变量表达式,且最后一级字段名必须符合约定。 + +支持的字段后缀: + +| 后缀 | 含义 | +| --- | --- | +| `email` | 邮箱 | +| `mobile` / `phone` | 手机号 | +| `account` | Zadig 用户账号 | +| `user_id` / `userid` | 渠道 user_id | +| `open_id` | 飞书 open_id | + +## 代码信息透传范围 + +Webhook 触发 workflow v4 时,后端除了保留原始 payload 外,还需要保留供已有代码相关任务逻辑和任务链路重建复用的仓库上下文信息,至少包括: + +| 信息 | 说明 | +| --- | --- | +| `branch` | 当前触发分支或匹配后的目标分支语义 | +| `tag` | tag 触发时的 tag 信息 | +| `pr` | PR / MR 编号 | +| `commit_id` | 触发本次任务的提交 ID | +| `commit_message` | 触发提交或 PR 标题对应的消息 | +| `committer` | 触发人/提交人标识 | +| `payload.*` | 从原始 Webhook JSON flatten 出来的叶子变量 | + +当前 PR 对通知动态收件人、通知标题和通知内容渲染,明确保证可直接使用的是: + +| 变量组 | 说明 | +| --- | --- | +| `payload.*` | 从原始 Webhook JSON flatten 出来的叶子变量 | +| `workflow.task.*` | 任务创建人、时间、URL 等运行时变量 | +| `workflow.params.*` | 工作流参数变量 | +| `workflow.trigger.*` | Webhook 统一触发变量,包括 branch、target_branch、pr、commit_id、commit_message、committer、event | +| `project` / `project.*` / `workflow.*` | 项目与工作流基础变量 | + +其中 `workflow.trigger.*` 既服务于通知模板渲染,也保留代码仓库上下文在重试、手动执行、已有代码逻辑中的复用能力。 + +这些信息需要满足: + +1. 首次 Webhook 触发任务时可用。 +2. 任务重试时不丢失。 +3. 手动执行阶段时不丢失。 +4. 可同时服务于通知人解析(`payload.*`)和已有代码仓库相关任务逻辑。 + +## 渠道支持范围 + +| 通知渠道 | 支持的动态通知人类型 | +| --- | --- | +| 飞书群通知(应用群) | `email`、`mobile/phone`、`account`、`user_id` | +| 飞书个人通知 | `email`、`mobile/phone`、`account`、`user_id`、`open_id` | +| 飞书机器人 Webhook | `user_id` | +| 企业微信 Webhook | `user_id` | +| 钉钉 | `email`、`mobile/phone`、`account` | +| Microsoft Teams | `email`、`mobile/phone`、`account` | +| 邮件 | `email`、`mobile/phone`、`account` | + +说明:飞书群通知不支持 `open_id`,飞书个人通知支持 `open_id`。 + +## 解析和转换规则 + +1. 用户创建任务时提交 `notify_inputs`,后端校验 `dynamic_recipients` 是否符合当前通知渠道支持范围。 +2. 任务运行时,后端使用 runtime context 渲染模板变量。 +3. 渲染结果为空或仍包含未解析模板时,该动态通知人跳过,不阻断通知。 +4. 渲染结果非空时,后端按变量后缀确定身份类型,并转换为渠道需要的目标标识。 +5. 转换结果去重后合并到原有静态通知人列表。 + +转换示例: + +| 用户配置 | 钉钉通知 | 邮件通知 | 飞书通知 | +| --- | --- | --- | --- | +| `{{.payload.user.email}}` | 通过邮箱查 Zadig 用户,再取手机号 | 直接使用邮箱 | 优先用邮箱查飞书 user_id,查不到再通过 Zadig 用户手机号查 | +| `{{.payload.user.mobile}}` | 直接使用手机号 | 通过手机号查 Zadig 用户,再取邮箱 | 优先用手机号查飞书 user_id,查不到再通过 Zadig 用户邮箱查 | +| `{{.payload.user.account}}` | 通过账号查 Zadig 用户,再取手机号 | 通过账号查 Zadig 用户,再取邮箱 | 通过账号查 Zadig 用户,再用邮箱/手机号查飞书 user_id | + +## 兼容性要求 + +1. 新接口和新持久化结构使用 `dynamic_recipients: []string`。 +2. 兼容 PR 中间态旧结构:`[{ "value": "...", "identity_type": "..." }]`。 +3. 旧结构读取时只保留 `value`,不继续使用 `identity_type`。 +4. 如果旧结构中的 `value` 不是合法模板变量,运行时 fail fast,返回清晰错误。 +5. Webhook 透传产生的 payload 和代码仓库上下文信息,在任务重试、手动执行阶段仍需可复用。 + +## 性能要求 + +1. 不做全类型盲猜,不对一个输入依次尝试 email、phone、account、user_id。 +2. 只按字段后缀触发必要查询。 +3. 单次通知解析内复用请求级缓存,避免重复查询同一账号、邮箱、手机号或飞书 user_id。 +4. 为 `user.email` 和 `user.phone` 增加数据库索引,避免转换首次落地后出现全表扫描风险。 + +## 验收标准 + +1. 用户配置 `{{.payload.xxx.email}}` 后,邮件和 Teams 能收到邮箱通知。 +2. 用户配置 `{{.payload.xxx.email}}` 后,钉钉可通过 Zadig 用户信息转换成手机号发送。 +3. 用户配置 `{{.payload.xxx.email}}` 或 `{{.payload.xxx.mobile}}` 后,飞书可转换成 user_id 通知。 +4. 用户配置不支持的后缀或渠道不支持的身份类型时,创建任务阶段返回明确错误。 +5. 飞书群通知配置 `open_id` 时校验失败,飞书个人通知配置 `open_id` 时允许。 +6. 已存在的 PR 中间态动态通知人数据可被读取为字符串数组。 +7. MySQL 新库和升级场景都具备 `idx_email`、`idx_phone` 索引。 +8. Webhook 触发任务后,`payload.*` 和 `workflow.trigger.*` 可在首次执行、重试、手动执行阶段继续用于通知变量渲染;代码仓库上下文信息可在这些阶段继续用于已有任务链路重建和代码相关逻辑。 From 87a0649bccc6e6b7a2e952502478162255443851 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 16 Jun 2026 18:18:30 +0800 Subject: [PATCH 06/18] feat(workflow): add commit sha runtime variable Signed-off-by: huanghongbo-hhb (cherry picked from commit 1281ef7ddeaa34653fdcb12f9adc659af4d01ee0) --- .../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 19541049d8..d35d5a2272 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -484,6 +484,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 9a34f9c9bd..b5b2921d56 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 @@ -330,6 +330,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 { @@ -339,6 +340,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, @@ -365,6 +367,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, @@ -375,6 +378,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 a64fdb7da7..a723bc4633 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 @@ -294,6 +294,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, @@ -315,6 +316,7 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, reque Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CommitMessage: eventRepo.CommitMessage, Committer: eventRepo.Committer, EventType: eventType, @@ -323,11 +325,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 85aeadaac4..4a03d425b0 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 @@ -290,6 +290,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, @@ -304,29 +305,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 db33c136e1..f8bd3cc91b 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 @@ -383,6 +383,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, @@ -404,6 +405,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, @@ -413,11 +415,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 31314eadf0..565e6bf621 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -466,6 +466,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 7ae6eb5560..a167795253 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go @@ -2621,6 +2621,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 7d49aac22f6a8802b76d57d7f2962f8579ed26d6 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 16 Jun 2026 11:38:28 +0800 Subject: [PATCH 07/18] fix dynamic notification recipients Signed-off-by: huanghongbo-hhb (cherry picked from commit 03cfa77213e705d12db861ef5dc5308ac281f598) --- 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 7437c2de92..de3277a933 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 622f2e1069..0dca5e421e 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1264,56 +1264,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 5c42464f8a..b4da09ee7b 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 @@ -279,206 +279,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 dfb8206e5a..5a5c7858b3 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go @@ -158,11 +158,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 @@ -174,43 +169,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 423eed074d..8f76ffdd5f 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 @@ -558,7 +558,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 { @@ -692,24 +695,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 { @@ -725,11 +729,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, } @@ -739,10 +747,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) @@ -762,10 +774,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, }, @@ -786,11 +802,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, } @@ -800,11 +820,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, } @@ -814,11 +838,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 @@ -827,10 +855,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 { @@ -861,7 +893,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 7b6a2c47c1148d09078ed704e45ceb5c2004d800 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 17 Jun 2026 11:23:08 +0800 Subject: [PATCH 08/18] fix trigger vars and lark hook recipients Signed-off-by: huanghongbo-hhb (cherry picked from commit 59573e9c797e8daf4204e7eb49eb307a28d1c74a) --- .../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 0dca5e421e..21cf3004ae 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1280,6 +1280,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 b4da09ee7b..e27671e74f 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,11 +282,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 6bd2c876ee..132f41e4ac 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 @@ -299,6 +299,7 @@ func TriggerTestByGiteeEvent(event interface{}, baseURI, requestID string, log * IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *gitee.PushEvent: @@ -317,6 +318,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 1699b3b67b..d87f6be173 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 @@ -96,6 +96,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: @@ -113,6 +114,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 f9f46ffd8e..d667e73e13 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 @@ -87,6 +87,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: @@ -104,6 +105,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 77c102a423..467c76f5d4 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 @@ -93,6 +93,7 @@ func TriggerScanningByGitlabEvent(event interface{}, baseURI, requestID string, IsPr: true, MergeRequestID: strconv.Itoa(mergeRequestID), CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } @@ -111,6 +112,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 711eefa487..ddaebac794 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 @@ -224,6 +224,7 @@ func TriggerTestByGitlabEvent(event interface{}, baseURI, requestID string, log IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } @@ -242,6 +243,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 565e6bf621..43c6b785d6 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -400,14 +400,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 8f76ffdd5f..23982963a6 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 @@ -701,7 +701,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) @@ -710,7 +710,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 @@ -729,12 +729,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), @@ -747,7 +748,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 } @@ -774,7 +775,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 } @@ -802,7 +803,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 } @@ -820,7 +821,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 } @@ -838,7 +839,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 } @@ -855,7 +856,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 } @@ -922,11 +923,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 73f951e1b76973cd494e3bd6c99fba3f32ca5eba Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 17 Jun 2026 11:41:12 +0800 Subject: [PATCH 09/18] fix payload scope for workflow runtime Signed-off-by: huanghongbo-hhb (cherry picked from commit 9e844df3d9eb6705e3f58852cbd384f6e557d61f) --- .../jobcontroller/job_notification.go | 14 ++----- .../core/common/util/workflow_variables.go | 40 +++++++++++++++++++ .../service/workflow/controller/workflow.go | 8 ++++ .../service/workflow/workflow_task_v4.go | 5 +++ 4 files changed, 57 insertions(+), 10 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 e27671e74f..d870005519 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,18 +246,12 @@ 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() + keyMap := util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) + for key := range keyMap { + if strings.HasPrefix(key, "payload.") { + delete(keyMap, key) } } - - insertKVs(c.workflowCtx.WorkflowKeyVals) return keyMap } 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 43c6b785d6..565e6bf621 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 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 23982963a6..7b0a57470f 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 @@ -923,6 +923,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 6c2d37dfb0344eb886a88108b87964b38d62d99d Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 17 Jun 2026 16:14:37 +0800 Subject: [PATCH 10/18] fix: allow payload variables in notification recipients Signed-off-by: huanghongbo-hhb (cherry picked from commit cb0c9a03e0927c363d5c4736a8e918bebae98a0d) --- .../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 d870005519..7a09d0e407 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 bf1fd96a71f3b9bb8af8a787267b480cbf16f750 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 18 Jun 2026 10:41:15 +0800 Subject: [PATCH 11/18] fix: preserve notification dynamic recipients runtime rendering Signed-off-by: huanghongbo-hhb (cherry picked from commit ab92a79d30d25967733b848c4cc917a07a399baa) --- .../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 565e6bf621..e753911584 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 7b0a57470f..7c728e7a78 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 @@ -1091,15 +1091,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 { @@ -1293,15 +1286,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 ddae2975291ee9eabb4acfca79a8bad6e619a155 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 18 Jun 2026 11:36:51 +0800 Subject: [PATCH 12/18] fix: preserve notification payload templates at runtime Signed-off-by: huanghongbo-hhb (cherry picked from commit 6bd3b6232292650bdfa13599f0d16b0ddc14101f) --- .../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 ce6c3199cb..b44a1258e3 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 { @@ -173,6 +307,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) @@ -183,6 +325,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 de788d3df116b3bbbfcd1cb83856be2ba501849b Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 23 Jun 2026 11:02:12 +0800 Subject: [PATCH 13/18] fix: send mail to dynamic email recipients Signed-off-by: huanghongbo-hhb (cherry picked from commit 21db44fc8ed3afae1b50d692069acdf38c94dacc) --- .../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 9ed076117f922a0e260a3308fb45e326a6223713 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Tue, 23 Jun 2026 11:41:32 +0800 Subject: [PATCH 14/18] fix: resolve workflow notification dynamic recipients Signed-off-by: huanghongbo-hhb (cherry picked from commit 93a978251d20ab31721b77d37f435ca09fb8a86a) --- .../dynamic_recipient.go} | 122 ++++++++++++++---- .../service/instantmessage/workflow_task.go | 95 ++++++++++++++ .../workflow_task_dynamic_recipient_test.go | 57 ++++++++ .../jobcontroller/job_notification.go | 84 +++--------- .../controller/job/job_notification.go | 4 +- .../service/workflow/workflow_task_v4.go | 3 +- 6 files changed, 274 insertions(+), 91 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 b1d5b7b1c9..8e32c4502e 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 { @@ -371,6 +377,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 { @@ -440,6 +451,90 @@ func (w *Service) SendWorkflowTaskNotifications(task *models.WorkflowTask) error return nil } +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 7a09d0e407..93b42b4835 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,60 +340,17 @@ 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, - }) +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)) } - 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) + atMessage := strings.Join(atUserList, " ") + if isAtAll { + atMessage += "" } - return resp + return atMessage } func renderNotificationString(input string, keyMap map[string]string) string { 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 7c728e7a78..c82da003d6 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" @@ -710,7 +711,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 5ed0f13b01c3576f1a0df45836b2705035a9b08c Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Wed, 24 Jun 2026 14:11:57 +0800 Subject: [PATCH 15/18] fix: make dingding notifications mention recipients Signed-off-by: huanghongbo-hhb (cherry picked from commit e08278892bc8e5129d765ac24c1e21c6b25cbed9) --- .../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 8e32c4502e..0f092c4a55 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go @@ -1004,7 +1004,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) @@ -1261,7 +1261,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) @@ -1584,6 +1584,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 93b42b4835..6d7ec3704d 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 @@ -488,31 +488,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 1a2bba7ad65cef12958da93b2dca5d9b56a1f435 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 25 Jun 2026 15:56:05 +0800 Subject: [PATCH 16/18] chore: remove ua migration from pr4667 4.3 backport Signed-off-by: huanghongbo-hhb --- pkg/cli/upgradeassistant/cmd/migrate/430.go | 35 +- .../internal/repository/models/migration.go | 1 - pr4667-docs/api.md | 493 ------------------ pr4667-docs/design.md | 330 ------------ pr4667-docs/requirement.md | 139 ----- 5 files changed, 3 insertions(+), 995 deletions(-) delete mode 100644 pr4667-docs/api.md delete mode 100644 pr4667-docs/design.md delete mode 100644 pr4667-docs/requirement.md diff --git a/pkg/cli/upgradeassistant/cmd/migrate/430.go b/pkg/cli/upgradeassistant/cmd/migrate/430.go index 142298fb3f..99cbb550be 100644 --- a/pkg/cli/upgradeassistant/cmd/migrate/430.go +++ b/pkg/cli/upgradeassistant/cmd/migrate/430.go @@ -117,21 +117,15 @@ func V421ToV430() error { updateMigrationError(migrationInfo.ID, err) }() - // 这次迁移分四段: + // 这次迁移分三段: // 1. MySQL: user 表新增 api_token_enabled - // 2. MySQL: user 表新增 email/phone 索引 - // 3. MySQL: permission action + role/template 绑定 - // 4. Mongo: collaboration mode / instance verbs + // 2. MySQL: permission action + role/template 绑定 + // 3. 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 @@ -150,29 +144,6 @@ 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 de3277a933..7437c2de92 100644 --- a/pkg/cli/upgradeassistant/internal/repository/models/migration.go +++ b/pkg/cli/upgradeassistant/internal/repository/models/migration.go @@ -41,7 +41,6 @@ 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/pr4667-docs/api.md b/pr4667-docs/api.md deleted file mode 100644 index bdd7075a42..0000000000 --- a/pr4667-docs/api.md +++ /dev/null @@ -1,493 +0,0 @@ -# PR 4667 API 文档:Payload 透传与动态通知人 - -## 1. 动态通知人配置 - -### 接口范围 - -动态通知人能力用于工作流任务运行时通知配置,主要影响以下接口: - -| 场景 | 路由 | -| --- | --- | -| Zadig OpenAPI 创建自定义工作流任务 | `POST /api/aslan/workflow/openapi/custom/task` | -| Zadig 控制台创建工作流任务 | `POST /api/aslan/workflow/v4/workflowtask` | -| 工作流任务手动执行 / 重试 | 复用任务内已保存的通知配置和 runtime context | - -说明:实际网关前缀以部署环境为准,表中展示的是 Aslan workflow router 下的路径语义。 - -### 请求字段 - -`notify_inputs` 中各通知配置新增或变更 `dynamic_recipients` 字段。 - -字段统一为字符串数组: - -```json -{ - "notify_inputs": [ - { - "id": 0, - "type": "mail", - "mail_notification_config": { - "dynamic_recipients": [ - "{{.payload.user.email}}" - ] - } - } - ] -} -``` - -不再支持新写入以下结构: - -```json -{ - "dynamic_recipients": [ - { - "value": "{{.payload.user.email}}", - "identity_type": "email" - } - ] -} -``` - -旧结构仅用于服务端读取兼容,不作为新 API 契约。 - -### `notify_inputs` 基本结构 - -```json -{ - "workflow_key": "workflow-demo", - "project_key": "project-demo", - "parameters": [], - "inputs": [], - "notify_inputs": [ - { - "id": 0, - "type": "mail", - "enabled": true, - "mail_notification_config": { - "users": [], - "user_ids": [], - "dynamic_recipients": [ - "{{.payload.commits.0.author.email}}" - ] - } - } - ] -} -``` - -字段说明: - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | integer | 是 | 工作流通知配置下标,从 `0` 开始 | -| `type` | string | 是 | 通知类型,必须和工作流中第 `id` 个通知配置类型一致 | -| `enabled` | boolean | 否 | 运行时是否启用该通知;不传则保持原配置 | -| `*_notification_config.dynamic_recipients` | string[] | 否 | 动态通知人模板变量 | - -### 支持的通知类型 - -| `type` | 配置字段 | -| --- | --- | -| `feishu` | `lark_hook_notification_config` | -| `feishu_app` | `lark_group_notification_config` | -| `feishu_person` | `lark_person_notification_config` | -| `wechat_work` | `wechat_notification_config` | -| `dingding` | `dingding_notification_config` | -| `msteams` | `msteams_notification_config` | -| `mail` | `mail_notification_config` | - -## 2. 动态通知人变量规则 - -### 格式 - -每个动态通知人必须是单个模板变量: - -```text -{{.payload.user.email}} -``` - -不支持: - -```text -payload.user.email -user@example.com -{{.payload.user.email}} <{{.payload.user.name}}> -``` - -### 支持后缀 - -后端根据最后一级字段名识别身份类型。 - -| 后缀 | 识别类型 | -| --- | --- | -| `email` | 邮箱 | -| `mobile` / `phone` | 手机号 | -| `account` | Zadig 用户账号 | -| `user_id` / `userid` | 渠道 user_id | -| `open_id` | 飞书 open_id | - -示例: - -| 变量 | 识别结果 | -| --- | --- | -| `{{.payload.user.email}}` | `email` | -| `{{.payload.user.phone}}` | `mobile` | -| `{{.payload.user.account}}` | `account` | -| `{{.payload.user.user_id}}` | `user_id` | -| `{{.payload.user.open_id}}` | `open_id` | - -`{{.payload.user.email_address}}` 不支持,因为最后一级字段名不是 `email`。 - -### 渠道支持矩阵 - -| 通知类型 | 支持的变量后缀 | -| --- | --- | -| `feishu` | `user_id` | -| `feishu_app` | `email`、`mobile/phone`、`account`、`user_id` | -| `feishu_person` | `email`、`mobile/phone`、`account`、`user_id`、`open_id` | -| `wechat_work` | `user_id` | -| `dingding` | `email`、`mobile/phone`、`account` | -| `msteams` | `email`、`mobile/phone`、`account` | -| `mail` | `email`、`mobile/phone`、`account` | - -## 3. 不同渠道请求示例 - -### 邮件通知 - -```json -{ - "id": 0, - "type": "mail", - "mail_notification_config": { - "dynamic_recipients": [ - "{{.payload.author.email}}", - "{{.payload.reviewer.account}}" - ] - } -} -``` - -解析行为: - -| 输入类型 | 行为 | -| --- | --- | -| `email` | 直接作为邮件接收人 | -| `mobile/phone` | 查 Zadig 用户,取邮箱 | -| `account` | 查 Zadig 用户,取邮箱 | - -### 钉钉通知 - -```json -{ - "id": 1, - "type": "dingding", - "dingding_notification_config": { - "dynamic_recipients": [ - "{{.payload.author.email}}", - "{{.payload.owner.mobile}}" - ] - } -} -``` - -解析行为: - -| 输入类型 | 行为 | -| --- | --- | -| `mobile/phone` | 直接作为 `at_mobiles` | -| `email` | 查 Zadig 用户,取手机号 | -| `account` | 查 Zadig 用户,取手机号 | - -### 飞书群通知(自建应用) - -```json -{ - "id": 2, - "type": "feishu_app", - "lark_group_notification_config": { - "chat_id": "oc_xxx", - "dynamic_recipients": [ - "{{.payload.author.email}}", - "{{.payload.reviewer.user_id}}" - ] - } -} -``` - -解析行为: - -| 输入类型 | 行为 | -| --- | --- | -| `user_id` | 直接作为飞书 user_id | -| `email` | 优先用邮箱查飞书 user_id;查不到则查 Zadig 用户手机号再查飞书 user_id | -| `mobile/phone` | 优先用手机号查飞书 user_id;查不到则查 Zadig 用户邮箱再查飞书 user_id | -| `account` | 查 Zadig 用户,再用邮箱/手机号查飞书 user_id | - -`feishu_app` 不支持 `open_id`。 - -### 飞书个人通知 - -```json -{ - "id": 3, - "type": "feishu_person", - "lark_person_notification_config": { - "dynamic_recipients": [ - "{{.payload.author.open_id}}", - "{{.payload.reviewer.email}}" - ] - } -} -``` - -`feishu_person` 支持 `open_id` 和 `user_id`。 - -### 飞书机器人 / 企业微信机器人 - -```json -{ - "id": 4, - "type": "feishu", - "lark_hook_notification_config": { - "dynamic_recipients": [ - "{{.payload.author.user_id}}" - ] - } -} -``` - -`feishu` 和 `wechat_work` 只支持 `user_id`,不做邮箱/手机号/账号转换。 - -## 4. Webhook Payload 透传变量 - -### 数据来源 - -GitHub、GitLab、Gitee、Gerrit 触发 workflow v4 时,服务端会保存原始 webhook request body 到任务的 hook payload 中: - -```go -HookPayload.RawPayload -``` - -运行时变量渲染时,后端将 `RawPayload` 解析为 JSON,并 flatten 成 `payload.*` 变量。 - -同时,代码仓库上下文信息不会因为引入 payload 透传而丢失,运行时统一暴露以下 `workflow.trigger.*` 变量: - -| 变量 | 说明 | -| --- | --- | -| `workflow.trigger.branch` | 当前触发分支语义 | -| `workflow.trigger.target_branch` | PR/MR 目标分支,或 push/tag 匹配后的目标分支语义 | -| `workflow.trigger.pr` | PR / MR 编号 | -| `workflow.trigger.commit_id` | 触发本次任务的 commit ID | -| `workflow.trigger.commit_message` | 触发提交消息或 PR 标题 | -| `workflow.trigger.committer` | 触发人 / 提交人 | -| `workflow.trigger.event` | 事件类型 | - -这些信息用于已有 repo/runtime 变量链路,以及重试、手动执行等场景。 - -说明:当前通知 job 直接使用的渲染变量来自 `BuildWorkflowRuntimeVariableKVs`,明确包括 `payload.*`、`workflow.task.*`、`workflow.params.*`、`workflow.trigger.*` 以及 project/workflow 基础变量。 - -示例: - -```text -{{.workflow.trigger.branch}} -{{.workflow.trigger.target_branch}} -{{.workflow.trigger.pr}} -{{.workflow.trigger.commit_id}} -{{.workflow.trigger.commit_message}} -{{.workflow.trigger.committer}} -{{.workflow.trigger.event}} -``` - -### Flatten 规则 - -| JSON 类型 | 变量生成规则 | -| --- | --- | -| object | 用字段名继续展开 | -| array | 用数字下标展开 | -| string/number/bool | 生成叶子变量 | -| null | 不生成变量 | - -示例 payload: - -```json -{ - "user": { - "email": "dev@example.com", - "mobile": "13800000000" - }, - "commits": [ - { - "author": { - "email": "author@example.com" - } - } - ] -} -``` - -可用变量: - -```text -{{.payload.user.email}} -{{.payload.user.mobile}} -{{.payload.commits.0.author.email}} -``` - -### 存储说明 - -`payload.*` 变量不会持久化到 `GlobalContext`。原因是 raw payload 已经保存在 `HookPayload.RawPayload`,运行时按需解析即可,避免在 MongoDB 中重复存储大量 payload 展开字段。 - -代码仓库上下文信息则继续沿用已有 workflow/task 数据结构保存和重建,不依赖把所有 `payload.*` 展开后落库。 - -### 重试 / 手动执行行为 - -对于 webhook 触发的任务: - -1. 首次执行时,`payload.*` 来源于 `HookPayload.RawPayload`。 -2. 重试时,会重建 runtime context,继续使用同一份 hook payload 和已保存的代码仓库上下文。 -3. 手动执行阶段时,也会重建 runtime context,确保动态通知人仍能基于 payload 变量渲染,并保留代码仓库上下文供已有任务链路复用。 - -因此,以下两类变量都应继续可用: - -```text -{{.payload.user.email}} -{{.payload.commits.0.author.email}} -{{.workflow.trigger.target_branch}} -{{.workflow.trigger.commit_id}} -``` - -以及已有的代码仓库相关 runtime/repo 语义会继续保留在 workflow/task 链路中,供已有代码逻辑复用。 - -## 5. 用户搜索 API 扩展 - -### 接口 - -```text -POST /api/v1/users/search -``` - -### 请求体 - -```json -{ - "email": "dev@example.com" -} -``` - -或: - -```json -{ - "phone": "13800000000" -} -``` - -或跨身份源账号查询: - -```json -{ - "account": "dev01", - "identity_type": "*" -} -``` - -### 查询优先级 - -当请求体同时包含多个条件时,后端按以下顺序选择一种查询: - -1. `uids` -2. `email` -3. `phone` -4. `account` -5. 列表查询 - -### 响应体 - -```json -{ - "users": [ - { - "uid": "user-uid", - "name": "Dev User", - "email": "dev@example.com", - "phone": "13800000000", - "identity_type": "system", - "account": "dev01" - } - ], - "totalCount": 1 -} -``` - -说明:动态通知人解析内部会复用该用户查询能力,用于邮箱、手机号、账号之间的转换。 - -## 6. 错误行为 - -### 非单变量格式 - -请求: - -```json -{ - "dynamic_recipients": ["dev@example.com"] -} -``` - -结果:创建任务失败,错误信息类似: - -```text -dynamic recipient must be a single template variable, got dev@example.com -``` - -### 不支持的字段后缀 - -请求: - -```json -{ - "dynamic_recipients": ["{{.payload.user.email_address}}"] -} -``` - -结果:创建任务失败,错误信息会提示允许的后缀: - -```text -only email/mobile(phone)/account/user_id(userid)/open_id are allowed -``` - -### 渠道不支持该类型 - -请求: - -```json -{ - "type": "feishu_app", - "lark_group_notification_config": { - "dynamic_recipients": ["{{.payload.user.open_id}}"] - } -} -``` - -结果:创建任务失败,因为飞书群通知不支持 `open_id`。 - -## 7. 兼容性 - -服务端读取 workflow/task 中的旧中间态数据时,兼容以下结构: - -```json -{ - "dynamic_recipients": [ - { - "value": "{{.payload.user.email}}", - "identity_type": "email" - } - ] -} -``` - -兼容行为: - -1. 只读取 `value`。 -2. 丢弃 `identity_type`。 -3. 后续仍按新规则校验和解析。 diff --git a/pr4667-docs/design.md b/pr4667-docs/design.md deleted file mode 100644 index 1fe4b9b4d0..0000000000 --- a/pr4667-docs/design.md +++ /dev/null @@ -1,330 +0,0 @@ -# PR 4667 设计文档:工作流动态通知人解析与渠道身份转换 - -## 设计概述 - -本 PR 将通知配置中的动态通知人从结构体数组改为字符串数组,用户只需要配置 payload 模板变量: - -```json -{ - "dynamic_recipients": ["{{.payload.user.email}}"] -} -``` - -后端通过变量最后一级字段名推断身份类型,例如 `email`、`mobile`、`account`、`user_id`、`open_id`。解析过程不做盲猜,而是在创建任务提交 `notify_inputs` 时做格式和渠道能力校验,在任务运行时按确定类型完成渠道转换。 - -## 接口设计 - -### 输入结构 - -所有通知配置中的 `dynamic_recipients` 统一为 `[]string`。 - -示例: - -```json -{ - "mail_notification_config": { - "dynamic_recipients": [ - "{{.payload.commit.author.email}}" - ] - } -} -``` - -旧结构已废弃: - -```json -{ - "dynamic_recipients": [ - { - "value": "{{.payload.commit.author.email}}", - "identity_type": "email" - } - ] -} -``` - -### 校验规则 - -`dynamic_recipients` 中每一项必须满足: - -1. 是单个模板变量,格式为 `{{.xxx.xxx}}`。 -2. 最后一级字段名在允许列表中。 -3. 字段后缀对应的身份类型被当前通知渠道支持。 - -允许后缀: - -| 后缀 | 内部类型 | -| --- | --- | -| `email` | `email` | -| `mobile` / `phone` | `mobile` | -| `account` | `account` | -| `user_id` / `userid` | `user_id` | -| `open_id` | `open_id` | - -## 渠道能力矩阵 - -| 渠道 | 内部通知类型 | 支持类型 | 输出目标 | -| --- | --- | --- | --- | -| 飞书机器人 Webhook | `feishu` | `user_id` | `at_users` | -| 飞书群通知 | `feishu_app` | `email`、`mobile`、`account`、`user_id` | `AtUsers`,ID type 为 user_id | -| 飞书个人通知 | `feishu_person` | `email`、`mobile`、`account`、`user_id`、`open_id` | `TargetUsers`,支持 user_id/open_id | -| 企业微信 Webhook | `wechat_work` | `user_id` | `at_users` | -| 钉钉 | `dingding` | `email`、`mobile`、`account` | `at_mobiles` | -| Microsoft Teams | `msteams` | `email`、`mobile`、`account` | `at_emails` | -| 邮件 | `mail` | `email`、`mobile`、`account` | `target_users` | - -飞书群通知不允许 `open_id`,因为运行路径按 user_id 发送;飞书个人通知允许 `open_id`。 - -## 数据流 - -1. 前端或 API 提交通知配置。 -2. `updateNotifyCtls` 裁剪空字符串,并调用 `ValidateDynamicRecipientsForNotifyType` 做边界校验。 -3. 通知配置持久化到 workflow/task 的通知配置中。 -4. Webhook 触发 workflow v4 时,原始 request body 保存到 `HookPayload.RawPayload`,同时仓库上下文中的 `branch`、`target_branch`、`pr`、`commit_id`、`commit_message`、`committer`、`event` 等信息保留到 `HookPayload` 中。 -5. 工作流任务运行时构造 runtime context;通知渲染直接使用 `payload.*`、`workflow.task.*`、`workflow.params.*`、`workflow.trigger.*` 以及 project/workflow 基础变量。 -5. `NotificationJobCtl.resolveDynamicRecipients` 按通知渠道调用对应 resolver。 -6. resolver 渲染模板变量,得到实际值。 -7. resolver 按变量后缀和渠道能力转换目标人。 -8. 转换结果去重后合并到静态通知人列表。 -9. 后续发送逻辑复用原有通知发送链路。 - -## 代码信息透传设计 - -### 透传内容 - -本次 PR 中“代码信息透传”不是只透传原始 payload,而是分成两层: - -1. 原始 webhook body 以 `HookPayload.RawPayload` 保存。 -2. 代码仓库上下文信息继续保留在 workflow/task 运行链路中,包括: - - `branch` - - `tag` - - `pr` - - `commit_id` - - `commit_message` - - `committer` - -这些信息一部分来自 webhook matcher 对仓库对象的补全,一部分来自 `HookPayload` 字段持久化;运行时统一通过 `workflow.trigger.*` 暴露给通知模板和工作流变量引用。 - -### 统一运行时变量 - -当前 PR 统一暴露以下触发变量: - -| 变量 | 说明 | -| --- | --- | -| `workflow.trigger.branch` | 触发分支 | -| `workflow.trigger.target_branch` | 目标分支,PR/MR 场景为目标分支,push/tag 场景为匹配分支语义 | -| `workflow.trigger.pr` | PR / MR 编号 | -| `workflow.trigger.commit_id` | 触发本次任务的 commit 标识 | -| `workflow.trigger.commit_message` | 提交消息或 PR 标题 | -| `workflow.trigger.committer` | 提交人 / 触发人 | -| `workflow.trigger.event` | 统一事件标识;GitHub/GitLab/Gitee 为 `pr` / `push` / `tag`,Gerrit 为原始事件类型 | - -### payload 变量生成 - -运行时渲染阶段,后端将 `HookPayload.RawPayload` 解析为 JSON,并 flatten 为 `payload.*` 变量,例如: - -```text -{{.payload.user.email}} -{{.payload.commits.0.author.email}} -``` - -这部分变量是运行时按需生成的,不直接展开后持久化进 MongoDB。 - -### 代码仓库上下文保留 - -对于已有 repo 相关运行时语义,设计上继续保留原有行为,不让 payload 透传破坏已有代码任务链路。当前保留的典型信息包括: - -| 信息 | 用途 | -| --- | --- | -| `workflow.trigger.branch` | 构建、部署等已有代码任务中的分支语义 | -| `workflow.trigger.target_branch` | PR / MR 目标分支,或匹配后的触发分支语义 | -| `workflow.trigger.pr` | PR / MR 场景复用 | -| `workflow.trigger.commit_id` | 定位具体提交 | -| `workflow.trigger.commit_message` | 从提交消息中继续提取环境变量片段 | -| `workflow.trigger.committer` | 代码触发链路中的触发人标识 | -| `workflow.trigger.event` | 按渠道/代码源区分事件类型 | - -说明:这组变量通过 `BuildWorkflowRuntimeVariableKVs`、`getWorkflowDefaultParams` 和变量引用列表统一暴露,因此通知 job 渲染、默认参数渲染、变量选择面板保持一致。 - -### 重试和手动执行 - -本次设计要求代码信息透传不仅在首次 webhook 触发时可用,还要在以下场景继续可用: - -1. `RetryWorkflowTaskV4` -2. `ManualExecWorkflowTaskV4` - -实现方式是重建任务 runtime context,并复用 workflow args、hook payload 和已有 global/job output 信息,而不是只依赖首次执行现场内存数据。 - -## 转换策略 - -### 邮件 / Teams - -目标需要邮箱。 - -| 输入类型 | 转换 | -| --- | --- | -| `email` | 直接作为邮箱 | -| `mobile` | 按手机号查询 Zadig 用户,取用户邮箱 | -| `account` | 按账号查询 Zadig 用户,取用户邮箱 | - -### 钉钉 - -目标需要手机号。 - -| 输入类型 | 转换 | -| --- | --- | -| `mobile` | 直接作为手机号 | -| `email` | 按邮箱查询 Zadig 用户,取用户手机号 | -| `account` | 按账号查询 Zadig 用户,取用户手机号 | - -### 飞书群 / 飞书个人 - -目标主要使用飞书 user_id。 - -| 输入类型 | 转换 | -| --- | --- | -| `user_id` | 直接作为飞书 user_id | -| `open_id` | 仅飞书个人通知允许,直接作为 open_id | -| `email` | 先用邮箱查询飞书 user_id;查不到时按邮箱查 Zadig 用户,再用手机号查询飞书 user_id | -| `mobile` | 先用手机号查询飞书 user_id;查不到时按手机号查 Zadig 用户,再用邮箱查询飞书 user_id | -| `account` | 按账号查 Zadig 用户,再依次用邮箱、手机号查询飞书 user_id | - -### 飞书机器人 Webhook / 企业微信 Webhook - -只支持 `user_id`,不做用户信息转换。 - -## 用户查询扩展 - -`/users/search` 新增支持按 `email` 和 `phone` 查询用户。 - -查询优先级: - -1. `uids` -2. `email` -3. `phone` -4. `account` -5. 常规列表查询 - -账号查询支持 `identity_type="*"`,用于跨身份源查找同一账号。动态通知人解析使用该能力,避免调用方必须知道用户来自本地、LDAP、OAuth 或其它身份源。 - -## 存储与兼容 - -### 新结构 - -持久化字段为: - -```go -type DynamicRecipients []string -``` - -各通知配置仍使用字段名 `dynamic_recipients`。 - -Webhook 透传部分沿用已有 `HookPayload` 结构,其中 `RawPayload` 用于保存原始 Webhook JSON,请求运行时再按需展开为 `payload.*` 变量。 - -### 旧中间结构兼容 - -为了兼容 PR 中间态已经落库的数据,`DynamicRecipients` 支持反序列化: - -```json -[ - { - "value": "{{.payload.user.email}}", - "identity_type": "email" - } -] -``` - -兼容策略: - -1. JSON/BSON 读取时优先按 `[]string` 解码。 -2. 解码失败后按旧结构解码。 -3. 旧结构只保留 `value`。 -4. 不继续使用 `identity_type`,避免废弃 schema 变成新契约。 - -## 性能设计 - -### 避免长查询链 - -本设计不对同一个输入做 email、phone、account、user_id 的盲猜。变量后缀决定身份类型,因此查询链是确定的。 - -### 请求级缓存 - -单次通知解析内缓存: - -1. account -> Zadig users -2. email -> Zadig users -3. phone -> Zadig users -4. appID + queryType + value -> 飞书 user_id -5. 飞书 user_id miss 结果 -6. appID -> 飞书 client - -这样同一个通知配置中多处引用相同用户信息时,不会重复打用户服务或飞书接口。 - -### 数据库索引 - -新增索引: - -| 字段 | 索引名 | -| --- | --- | -| `user.email` | `idx_email` | -| `user.phone` | `idx_phone` | - -覆盖范围: - -1. MySQL 初始化 SQL。 -2. 达梦初始化 SQL。 -3. GORM model tag。 -4. 4.3.0 upgradeassistant 迁移,用于已有线上库补索引。 - -## 错误处理 - -1. 配置格式不合法时,在创建任务并提交 `notify_inputs` 阶段返回错误。 -2. 渠道不支持某类型时,在创建任务并提交 `notify_inputs` 阶段返回错误。 -3. 模板运行时渲染为空或未解析完成时跳过该动态收件人。 -4. 外部查询报错时返回错误,避免静默丢通知。 -5. 查询不到用户或用户缺少必要字段时跳过该用户,不阻断其它通知人。 - -## 取舍说明 - -### 为什么去掉 `identity_type` - -保留 `identity_type` 会要求用户同时配置“变量值”和“变量含义”,例如: - -```json -{ - "value": "{{.payload.user.email}}", - "identity_type": "email" -} -``` - -这增加了配置负担,也容易出现 `value` 是邮箱但 `identity_type` 写成手机号的冲突。当前设计改为通过字段后缀表达语义,用户只需要配置一个字符串变量。 - -### 为什么不支持任意变量名 - -例如 `{{.payload.contact.email_address}}` 无法通过后缀识别身份类型。为了避免后端猜测和长查询链,当前要求用户使用约定后缀,例如 `email`、`mobile`、`account`。 - -### 为什么旧结构只兼容读取 `value` - -`identity_type` 是 PR 中间态字段,不是最终产品契约。继续使用它会把临时 schema 固化为正式行为,因此只做读取兼容,不在运行时继续依赖它。 - -## 风险与缓解 - -| 风险 | 影响 | 缓解 | -| --- | --- | --- | -| 用户 payload 字段命名不符合后缀约定 | 配置校验失败或运行时报错 | 错误信息列出允许后缀,文档明确命名规范 | -| 代码信息只在首次 webhook 触发时可用,重试/手动执行丢失 | 代码相关任务链路行为不一致 | 重建 runtime context 并复用 hook payload / workflow args | -| Zadig 用户缺少邮箱或手机号 | 无法转换到部分渠道 | 跳过该用户,不影响其它可解析用户 | -| 飞书外部接口查询失败 | 当前通知任务返回错误 | 复用原有错误链路,保留可观测错误 | -| 已有库缺少 email/phone 索引 | 查询可能全表扫 | 初始化 SQL 和 upgradeassistant 迁移补索引 | -| PR 中间态旧数据 schema 不一致 | 老任务重试可能读取失败 | `DynamicRecipients` 自定义 JSON/BSON 解码兼容 | - -## 验证建议 - -1. 通过 Webhook payload 提供 `payload.user.email`,配置邮件动态通知人,确认邮件目标包含该邮箱。 -2. 通过 GitHub/GitLab/Gitee/Gerrit webhook 触发任务,确认 `payload.*` 和 `workflow.trigger.*` 可用于通知变量渲染,且这些代码信息在运行时仍可被已有代码逻辑消费。 -2. 通过同一邮箱查到 Zadig 用户手机号,配置钉钉动态通知人,确认 `at_mobiles` 被补充。 -3. 配置飞书个人通知 `{{.payload.user.open_id}}`,确认允许保存并可通知。 -4. 配置飞书群通知 `{{.payload.user.open_id}}`,确认创建任务时报错。 -5. 模拟旧结构 `{value, identity_type}` 存量数据,确认可反序列化并继续运行。 -6. 检查新库和升级库均存在 `idx_email`、`idx_phone`。 -7. 在重试和手动执行阶段,再次验证 `payload.*` 仍可用于通知变量渲染,代码仓库上下文仍可用于已有任务链路重建。 diff --git a/pr4667-docs/requirement.md b/pr4667-docs/requirement.md deleted file mode 100644 index 989e55f138..0000000000 --- a/pr4667-docs/requirement.md +++ /dev/null @@ -1,139 +0,0 @@ -# PR 4667 需求文档:工作流 Payload 信息透传与动态通知人 - -## 背景 - -当前工作流支持在 Webhook 触发后执行任务,但通知对象主要依赖静态配置,无法直接从 Webhook payload 中提取代码 author、触发人或业务字段作为通知人。对于代码提交、合并请求、外部系统事件等场景,用户希望工作流执行完成后,IM 或邮件通知能够自动发送给 payload 中指定的人。 - -不同通知渠道对“人”的标识要求不同。例如飞书可能需要 user_id/open_id,钉钉需要手机号,邮件需要邮箱。如果要求用户在配置时额外声明 `identity_type`,前端和用户理解成本较高,也容易配置错误。因此本需求改为:用户只配置 payload 模板变量,后端根据变量字段后缀和通知渠道能力做确定性解析和转换。 - -## 目标 - -1. 工作流通知配置支持从 payload 中读取动态通知人。 -2. 用户无需配置 `identity_type`,只填写字符串模板变量,例如 `{{.payload.user.email}}`。 -3. 后端根据变量后缀识别身份类型,并按通知渠道转换成该渠道可发送的目标人标识。 -4. 支持代码信息透传场景,任务执行阶段可以使用 Webhook payload 生成运行时变量,并用于通知人解析。 -5. Webhook 触发的代码仓库上下文信息需要在任务生命周期内保留,并统一暴露为 `workflow.trigger.*` 运行时变量,包括分支、目标分支、PR、commit ID、commit message、committer、event 等,确保通知渲染、已有代码相关任务逻辑、重试和手动执行都可复用这些信息。 -6. 对历史 PR 中间态数据保持读取兼容,避免已有任务重试或手动执行时因 schema 变化失败。 - -## 非目标 - -1. 不支持任意字符串作为动态通知人,例如 `someaccount`。 -2. 不支持要求后端盲猜输入是邮箱、手机号、账号还是渠道 ID。 -3. 不引入新的 `identity_type` 配置项。 -4. 不保证每个渠道都支持所有身份类型,按渠道能力分别限制。 -5. 不处理通知渠道外部账号体系本身不完整的问题,例如 Zadig 用户未填写手机号时无法发送钉钉通知。 - -## 用户配置方式 - -动态通知人字段统一为字符串数组: - -```json -{ - "dynamic_recipients": [ - "{{.payload.user.email}}", - "{{.payload.owner.mobile}}", - "{{.payload.author.account}}" - ] -} -``` - -模板变量必须是单个变量表达式,且最后一级字段名必须符合约定。 - -支持的字段后缀: - -| 后缀 | 含义 | -| --- | --- | -| `email` | 邮箱 | -| `mobile` / `phone` | 手机号 | -| `account` | Zadig 用户账号 | -| `user_id` / `userid` | 渠道 user_id | -| `open_id` | 飞书 open_id | - -## 代码信息透传范围 - -Webhook 触发 workflow v4 时,后端除了保留原始 payload 外,还需要保留供已有代码相关任务逻辑和任务链路重建复用的仓库上下文信息,至少包括: - -| 信息 | 说明 | -| --- | --- | -| `branch` | 当前触发分支或匹配后的目标分支语义 | -| `tag` | tag 触发时的 tag 信息 | -| `pr` | PR / MR 编号 | -| `commit_id` | 触发本次任务的提交 ID | -| `commit_message` | 触发提交或 PR 标题对应的消息 | -| `committer` | 触发人/提交人标识 | -| `payload.*` | 从原始 Webhook JSON flatten 出来的叶子变量 | - -当前 PR 对通知动态收件人、通知标题和通知内容渲染,明确保证可直接使用的是: - -| 变量组 | 说明 | -| --- | --- | -| `payload.*` | 从原始 Webhook JSON flatten 出来的叶子变量 | -| `workflow.task.*` | 任务创建人、时间、URL 等运行时变量 | -| `workflow.params.*` | 工作流参数变量 | -| `workflow.trigger.*` | Webhook 统一触发变量,包括 branch、target_branch、pr、commit_id、commit_message、committer、event | -| `project` / `project.*` / `workflow.*` | 项目与工作流基础变量 | - -其中 `workflow.trigger.*` 既服务于通知模板渲染,也保留代码仓库上下文在重试、手动执行、已有代码逻辑中的复用能力。 - -这些信息需要满足: - -1. 首次 Webhook 触发任务时可用。 -2. 任务重试时不丢失。 -3. 手动执行阶段时不丢失。 -4. 可同时服务于通知人解析(`payload.*`)和已有代码仓库相关任务逻辑。 - -## 渠道支持范围 - -| 通知渠道 | 支持的动态通知人类型 | -| --- | --- | -| 飞书群通知(应用群) | `email`、`mobile/phone`、`account`、`user_id` | -| 飞书个人通知 | `email`、`mobile/phone`、`account`、`user_id`、`open_id` | -| 飞书机器人 Webhook | `user_id` | -| 企业微信 Webhook | `user_id` | -| 钉钉 | `email`、`mobile/phone`、`account` | -| Microsoft Teams | `email`、`mobile/phone`、`account` | -| 邮件 | `email`、`mobile/phone`、`account` | - -说明:飞书群通知不支持 `open_id`,飞书个人通知支持 `open_id`。 - -## 解析和转换规则 - -1. 用户创建任务时提交 `notify_inputs`,后端校验 `dynamic_recipients` 是否符合当前通知渠道支持范围。 -2. 任务运行时,后端使用 runtime context 渲染模板变量。 -3. 渲染结果为空或仍包含未解析模板时,该动态通知人跳过,不阻断通知。 -4. 渲染结果非空时,后端按变量后缀确定身份类型,并转换为渠道需要的目标标识。 -5. 转换结果去重后合并到原有静态通知人列表。 - -转换示例: - -| 用户配置 | 钉钉通知 | 邮件通知 | 飞书通知 | -| --- | --- | --- | --- | -| `{{.payload.user.email}}` | 通过邮箱查 Zadig 用户,再取手机号 | 直接使用邮箱 | 优先用邮箱查飞书 user_id,查不到再通过 Zadig 用户手机号查 | -| `{{.payload.user.mobile}}` | 直接使用手机号 | 通过手机号查 Zadig 用户,再取邮箱 | 优先用手机号查飞书 user_id,查不到再通过 Zadig 用户邮箱查 | -| `{{.payload.user.account}}` | 通过账号查 Zadig 用户,再取手机号 | 通过账号查 Zadig 用户,再取邮箱 | 通过账号查 Zadig 用户,再用邮箱/手机号查飞书 user_id | - -## 兼容性要求 - -1. 新接口和新持久化结构使用 `dynamic_recipients: []string`。 -2. 兼容 PR 中间态旧结构:`[{ "value": "...", "identity_type": "..." }]`。 -3. 旧结构读取时只保留 `value`,不继续使用 `identity_type`。 -4. 如果旧结构中的 `value` 不是合法模板变量,运行时 fail fast,返回清晰错误。 -5. Webhook 透传产生的 payload 和代码仓库上下文信息,在任务重试、手动执行阶段仍需可复用。 - -## 性能要求 - -1. 不做全类型盲猜,不对一个输入依次尝试 email、phone、account、user_id。 -2. 只按字段后缀触发必要查询。 -3. 单次通知解析内复用请求级缓存,避免重复查询同一账号、邮箱、手机号或飞书 user_id。 -4. 为 `user.email` 和 `user.phone` 增加数据库索引,避免转换首次落地后出现全表扫描风险。 - -## 验收标准 - -1. 用户配置 `{{.payload.xxx.email}}` 后,邮件和 Teams 能收到邮箱通知。 -2. 用户配置 `{{.payload.xxx.email}}` 后,钉钉可通过 Zadig 用户信息转换成手机号发送。 -3. 用户配置 `{{.payload.xxx.email}}` 或 `{{.payload.xxx.mobile}}` 后,飞书可转换成 user_id 通知。 -4. 用户配置不支持的后缀或渠道不支持的身份类型时,创建任务阶段返回明确错误。 -5. 飞书群通知配置 `open_id` 时校验失败,飞书个人通知配置 `open_id` 时允许。 -6. 已存在的 PR 中间态动态通知人数据可被读取为字符串数组。 -7. MySQL 新库和升级场景都具备 `idx_email`、`idx_phone` 索引。 -8. Webhook 触发任务后,`payload.*` 和 `workflow.trigger.*` 可在首次执行、重试、手动执行阶段继续用于通知变量渲染;代码仓库上下文信息可在这些阶段继续用于已有任务链路重建和代码相关逻辑。 From 042ae72fb1d5f52eb3d0fca7eeed7ee31cbd3454 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 25 Jun 2026 16:00:19 +0800 Subject: [PATCH 17/18] fix: preserve webhook repo context fields in 4.3 backport 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 03f6f8bad7..da50168b79 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 @@ -246,6 +246,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 031e0e3e147e570f506447c50cc9a8fa0bb42754 Mon Sep 17 00:00:00 2001 From: huanghongbo-hhb Date: Thu, 25 Jun 2026 16:07:59 +0800 Subject: [PATCH 18/18] chore: remove backport test files Signed-off-by: huanghongbo-hhb --- .../workflow_task_dynamic_recipient_test.go | 57 ------ .../jobcontroller/job_notification_test.go | 129 ------------- .../workflow/controller/workflow_test.go | 176 ------------------ 3 files changed, 362 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 delete mode 100644 pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow_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 042e7cdae8..0000000000 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification_test.go +++ /dev/null @@ -1,129 +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) - } -} - -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) - } -}