gitea源码

mail_workflow_run.go 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // Copyright 2025 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package mailer
  4. import (
  5. "bytes"
  6. "context"
  7. "fmt"
  8. "sort"
  9. "time"
  10. actions_model "code.gitea.io/gitea/models/actions"
  11. repo_model "code.gitea.io/gitea/models/repo"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/log"
  15. "code.gitea.io/gitea/modules/setting"
  16. "code.gitea.io/gitea/modules/templates"
  17. "code.gitea.io/gitea/modules/translation"
  18. "code.gitea.io/gitea/services/convert"
  19. sender_service "code.gitea.io/gitea/services/mailer/sender"
  20. )
  21. const tplWorkflowRun templates.TplName = "repo/actions/workflow_run"
  22. type convertedWorkflowJob struct {
  23. HTMLURL string
  24. Name string
  25. Status actions_model.Status
  26. Attempt int64
  27. Duration time.Duration
  28. }
  29. func generateMessageIDForActionsWorkflowRunStatusEmail(repo *repo_model.Repository, run *actions_model.ActionRun) string {
  30. return fmt.Sprintf("<%s/actions/runs/%d@%s>", repo.FullName(), run.Index, setting.Domain)
  31. }
  32. func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, sender *user_model.User, recipients []*user_model.User) error {
  33. jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
  34. if err != nil {
  35. return err
  36. }
  37. for _, job := range jobs {
  38. if !job.Status.IsDone() {
  39. log.Debug("composeAndSendActionsWorkflowRunStatusEmail: A job is not done. Will not compose and send actions email.")
  40. return nil
  41. }
  42. }
  43. var subjectTrString string
  44. switch run.Status {
  45. case actions_model.StatusFailure:
  46. subjectTrString = "mail.repo.actions.run.failed"
  47. case actions_model.StatusCancelled:
  48. subjectTrString = "mail.repo.actions.run.cancelled"
  49. case actions_model.StatusSuccess:
  50. subjectTrString = "mail.repo.actions.run.succeeded"
  51. }
  52. displayName := fromDisplayName(sender)
  53. messageID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
  54. metadataHeaders := generateMetadataHeaders(repo)
  55. sort.SliceStable(jobs, func(i, j int) bool {
  56. si, sj := jobs[i].Status, jobs[j].Status
  57. /*
  58. If both i and j are/are not success, leave it to si < sj.
  59. If i is success and j is not, since the desired is j goes "smaller" and i goes "bigger", this func should return false.
  60. If j is success and i is not, since the desired is i goes "smaller" and j goes "bigger", this func should return true.
  61. */
  62. if si.IsSuccess() != sj.IsSuccess() {
  63. return !si.IsSuccess()
  64. }
  65. return si < sj
  66. })
  67. convertedJobs := make([]convertedWorkflowJob, 0, len(jobs))
  68. for _, job := range jobs {
  69. converted0, err := convert.ToActionWorkflowJob(ctx, repo, nil, job)
  70. if err != nil {
  71. log.Error("convert.ToActionWorkflowJob: %v", err)
  72. continue
  73. }
  74. convertedJobs = append(convertedJobs, convertedWorkflowJob{
  75. HTMLURL: converted0.HTMLURL,
  76. Name: converted0.Name,
  77. Status: job.Status,
  78. Attempt: converted0.RunAttempt,
  79. Duration: job.Duration(),
  80. })
  81. }
  82. langMap := make(map[string][]*user_model.User)
  83. for _, user := range recipients {
  84. langMap[user.Language] = append(langMap[user.Language], user)
  85. }
  86. for lang, tos := range langMap {
  87. locale := translation.NewLocale(lang)
  88. var runStatusTrString string
  89. switch run.Status {
  90. case actions_model.StatusSuccess:
  91. runStatusTrString = "mail.repo.actions.jobs.all_succeeded"
  92. case actions_model.StatusFailure:
  93. runStatusTrString = "mail.repo.actions.jobs.all_failed"
  94. for _, job := range jobs {
  95. if !job.Status.IsFailure() {
  96. runStatusTrString = "mail.repo.actions.jobs.some_not_successful"
  97. break
  98. }
  99. }
  100. case actions_model.StatusCancelled:
  101. runStatusTrString = "mail.repo.actions.jobs.all_cancelled"
  102. }
  103. subject := fmt.Sprintf("%s: %s (%s)", locale.TrString(subjectTrString), run.WorkflowID, base.ShortSha(run.CommitSHA))
  104. var mailBody bytes.Buffer
  105. if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, string(tplWorkflowRun), map[string]any{
  106. "Subject": subject,
  107. "Repo": repo,
  108. "Run": run,
  109. "RunStatusText": locale.TrString(runStatusTrString),
  110. "Jobs": convertedJobs,
  111. "locale": locale,
  112. }); err != nil {
  113. return err
  114. }
  115. msgs := make([]*sender_service.Message, 0, len(tos))
  116. for _, rec := range tos {
  117. log.Trace("Sending actions email to %s (UID: %d)", rec.Name, rec.ID)
  118. msg := sender_service.NewMessageFrom(
  119. rec.Email,
  120. displayName,
  121. setting.MailService.FromEmail,
  122. subject,
  123. mailBody.String(),
  124. )
  125. msg.Info = subject
  126. for k, v := range generateSenderRecipientHeaders(sender, rec) {
  127. msg.SetHeader(k, v)
  128. }
  129. for k, v := range metadataHeaders {
  130. msg.SetHeader(k, v)
  131. }
  132. msg.SetHeader("Message-ID", messageID)
  133. msgs = append(msgs, msg)
  134. }
  135. SendAsync(msgs...)
  136. }
  137. return nil
  138. }
  139. func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo_model.Repository, run *actions_model.ActionRun) error {
  140. if setting.MailService == nil {
  141. return nil
  142. }
  143. if !run.Status.IsDone() || run.Status.IsSkipped() {
  144. return nil
  145. }
  146. recipients := make([]*user_model.User, 0)
  147. if !sender.IsGiteaActions() && !sender.IsGhost() && sender.IsMailable() {
  148. notifyPref, err := user_model.GetUserSetting(ctx, sender.ID,
  149. user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly)
  150. if err != nil {
  151. return err
  152. }
  153. if notifyPref == user_model.SettingEmailNotificationGiteaActionsAll || !run.Status.IsSuccess() && notifyPref != user_model.SettingEmailNotificationGiteaActionsDisabled {
  154. recipients = append(recipients, sender)
  155. }
  156. }
  157. if len(recipients) > 0 {
  158. log.Debug("MailActionsTrigger: Initiate email composition")
  159. return composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients)
  160. }
  161. return nil
  162. }