Skip to content

Commit 0d00ec7

Browse files
NorthRealmwxiaoguangChristopherHX
authored
Send email on Workflow Run Success/Failure (#34982)
Closes #23725 ![1](https://github.com/user-attachments/assets/9bfa76ea-8c45-4155-a5d4-dc2f0667faa8) ![2](https://github.com/user-attachments/assets/49be7402-e5d5-486e-a1c2-8d3222540b13) /claim #23725 --------- Signed-off-by: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: ChristopherHX <christopher.homberger@web.de>
1 parent cd3fb95 commit 0d00ec7

File tree

14 files changed

+364
-41
lines changed

14 files changed

+364
-41
lines changed

models/user/setting_keys.go renamed to models/user/setting_options.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ const (
2121
SignupUserAgent = "signup.user_agent"
2222

2323
SettingsKeyCodeViewShowFileTree = "code_view.show_file_tree"
24+
25+
SettingsKeyEmailNotificationGiteaActions = "email_notification.gitea_actions"
26+
SettingEmailNotificationGiteaActionsAll = "all"
27+
SettingEmailNotificationGiteaActionsFailureOnly = "failure-only" // Default for actions email preference
28+
SettingEmailNotificationGiteaActionsDisabled = "disabled"
2429
)

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,8 @@ email_notifications.onmention = Only Email on Mention
10211021
email_notifications.disable = Disable Email Notifications
10221022
email_notifications.submit = Set Email Preference
10231023
email_notifications.andyourown = And Your Own Notifications
1024+
email_notifications.actions.desc = Notifications for workflow runs on repositories set up with <a target="_blank" href="%s">Gitea Actions</a>.
1025+
email_notifications.actions.failure_only = Only notify for failed workflow runs
10241026

10251027
visibility = User visibility
10261028
visibility.public = Public

routers/web/devtest/mail_preview.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
func MailPreviewRender(ctx *context.Context) {
1818
tmplName := ctx.PathParam("*")
19-
mockDataContent, err := templates.AssetFS().ReadFile("mail/" + tmplName + ".mock.yml")
19+
mockDataContent, err := templates.AssetFS().ReadFile("mail/" + tmplName + ".devtest.yml")
2020
mockData := map[string]any{}
2121
if err == nil {
2222
err = yaml.Unmarshal(mockDataContent, &mockData)

routers/web/user/setting/notifications.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
package setting
55

66
import (
7-
"errors"
87
"net/http"
98

9+
"code.gitea.io/gitea/models/unit"
1010
user_model "code.gitea.io/gitea/models/user"
11-
"code.gitea.io/gitea/modules/log"
1211
"code.gitea.io/gitea/modules/optional"
1312
"code.gitea.io/gitea/modules/setting"
1413
"code.gitea.io/gitea/modules/templates"
@@ -29,6 +28,13 @@ func Notifications(ctx *context.Context) {
2928
ctx.Data["PageIsSettingsNotifications"] = true
3029
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
3130

31+
actionsEmailPref, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly)
32+
if err != nil {
33+
ctx.ServerError("GetUserSetting", err)
34+
return
35+
}
36+
ctx.Data["ActionsEmailNotificationsPreference"] = actionsEmailPref
37+
3238
ctx.HTML(http.StatusOK, tplSettingsNotifications)
3339
}
3440

@@ -44,19 +50,40 @@ func NotificationsEmailPost(ctx *context.Context) {
4450
preference == user_model.EmailNotificationsOnMention ||
4551
preference == user_model.EmailNotificationsDisabled ||
4652
preference == user_model.EmailNotificationsAndYourOwn) {
47-
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name)
48-
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
53+
ctx.Flash.Error(ctx.Tr("invalid_data", preference))
54+
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
4955
return
5056
}
5157
opts := &user.UpdateOptions{
5258
EmailNotificationsPreference: optional.Some(preference),
5359
}
5460
if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil {
55-
log.Error("Set Email Notifications failed: %v", err)
5661
ctx.ServerError("UpdateUser", err)
5762
return
5863
}
59-
log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name)
64+
ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
65+
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
66+
}
67+
68+
// NotificationsActionsEmailPost set user's email notification preference on Gitea Actions
69+
func NotificationsActionsEmailPost(ctx *context.Context) {
70+
if !setting.Actions.Enabled || unit.TypeActions.UnitGlobalDisabled() {
71+
ctx.NotFound(nil)
72+
return
73+
}
74+
75+
preference := ctx.FormString("preference")
76+
if !(preference == user_model.SettingEmailNotificationGiteaActionsAll ||
77+
preference == user_model.SettingEmailNotificationGiteaActionsDisabled ||
78+
preference == user_model.SettingEmailNotificationGiteaActionsFailureOnly) {
79+
ctx.Flash.Error(ctx.Tr("invalid_data", preference))
80+
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
81+
return
82+
}
83+
if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyEmailNotificationGiteaActions, preference); err != nil {
84+
ctx.ServerError("SetUserSetting", err)
85+
return
86+
}
6087
ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
6188
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
6289
}

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ func registerWebRoutes(m *web.Router) {
598598
m.Group("/notifications", func() {
599599
m.Get("", user_setting.Notifications)
600600
m.Post("/email", user_setting.NotificationsEmailPost)
601+
m.Post("/actions", user_setting.NotificationsActionsEmailPost)
601602
})
602603
m.Group("/security", func() {
603604
m.Get("", security.Security)

services/mailer/mail.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,41 @@ func fromDisplayName(u *user_model.User) string {
174174
}
175175
return u.GetCompleteName()
176176
}
177+
178+
func generateMetadataHeaders(repo *repo_model.Repository) map[string]string {
179+
return map[string]string{
180+
// https://datatracker.ietf.org/doc/html/rfc2919
181+
"List-ID": fmt.Sprintf("%s <%s.%s.%s>", repo.FullName(), repo.Name, repo.OwnerName, setting.Domain),
182+
183+
// https://datatracker.ietf.org/doc/html/rfc2369
184+
"List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()),
185+
186+
"X-Mailer": "Gitea",
187+
188+
"X-Gitea-Repository": repo.Name,
189+
"X-Gitea-Repository-Path": repo.FullName(),
190+
"X-Gitea-Repository-Link": repo.HTMLURL(),
191+
192+
"X-GitLab-Project": repo.Name,
193+
"X-GitLab-Project-Path": repo.FullName(),
194+
}
195+
}
196+
197+
func generateSenderRecipientHeaders(doer, recipient *user_model.User) map[string]string {
198+
return map[string]string{
199+
"X-Gitea-Sender": doer.Name,
200+
"X-Gitea-Recipient": recipient.Name,
201+
"X-Gitea-Recipient-Address": recipient.Email,
202+
"X-GitHub-Sender": doer.Name,
203+
"X-GitHub-Recipient": recipient.Name,
204+
"X-GitHub-Recipient-Address": recipient.Email,
205+
}
206+
}
207+
208+
func generateReasonHeaders(reason string) map[string]string {
209+
return map[string]string{
210+
"X-Gitea-Reason": reason,
211+
"X-GitHub-Reason": reason,
212+
"X-GitLab-NotificationReason": reason,
213+
}
214+
}

services/mailer/mail_issue_common.go

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"context"
99
"fmt"
10+
"maps"
1011
"strconv"
1112
"strings"
1213
"time"
@@ -29,7 +30,7 @@ import (
2930
// Many e-mail service providers have limitations on the size of the email body, it's usually from 10MB to 25MB
3031
const maxEmailBodySize = 9_000_000
3132

32-
func fallbackMailSubject(issue *issues_model.Issue) string {
33+
func fallbackIssueMailSubject(issue *issues_model.Issue) string {
3334
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
3435
}
3536

@@ -86,7 +87,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
8687
if actName != "new" {
8788
prefix = "Re: "
8889
}
89-
fallback = prefix + fallbackMailSubject(comment.Issue)
90+
fallback = prefix + fallbackIssueMailSubject(comment.Issue)
9091

9192
if comment.Comment != nil && comment.Comment.Review != nil {
9293
reviewComments = make([]*issues_model.Comment, 0, 10)
@@ -202,7 +203,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
202203
msg.SetHeader("References", references...)
203204
msg.SetHeader("List-Unsubscribe", listUnsubscribe...)
204205

205-
for key, value := range generateAdditionalHeaders(comment, actType, recipient) {
206+
for key, value := range generateAdditionalHeadersForIssue(comment, actType, recipient) {
206207
msg.SetHeader(key, value)
207208
}
208209

@@ -302,35 +303,18 @@ func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.
302303
return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain)
303304
}
304305

305-
func generateAdditionalHeaders(ctx *mailComment, reason string, recipient *user_model.User) map[string]string {
306+
func generateAdditionalHeadersForIssue(ctx *mailComment, reason string, recipient *user_model.User) map[string]string {
306307
repo := ctx.Issue.Repo
307308

308-
return map[string]string{
309-
// https://datatracker.ietf.org/doc/html/rfc2919
310-
"List-ID": fmt.Sprintf("%s <%s.%s.%s>", repo.FullName(), repo.Name, repo.OwnerName, setting.Domain),
311-
312-
// https://datatracker.ietf.org/doc/html/rfc2369
313-
"List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()),
314-
315-
"X-Mailer": "Gitea",
316-
"X-Gitea-Reason": reason,
317-
"X-Gitea-Sender": ctx.Doer.Name,
318-
"X-Gitea-Recipient": recipient.Name,
319-
"X-Gitea-Recipient-Address": recipient.Email,
320-
"X-Gitea-Repository": repo.Name,
321-
"X-Gitea-Repository-Path": repo.FullName(),
322-
"X-Gitea-Repository-Link": repo.HTMLURL(),
323-
"X-Gitea-Issue-ID": strconv.FormatInt(ctx.Issue.Index, 10),
324-
"X-Gitea-Issue-Link": ctx.Issue.HTMLURL(),
325-
326-
"X-GitHub-Reason": reason,
327-
"X-GitHub-Sender": ctx.Doer.Name,
328-
"X-GitHub-Recipient": recipient.Name,
329-
"X-GitHub-Recipient-Address": recipient.Email,
330-
331-
"X-GitLab-NotificationReason": reason,
332-
"X-GitLab-Project": repo.Name,
333-
"X-GitLab-Project-Path": repo.FullName(),
334-
"X-GitLab-Issue-IID": strconv.FormatInt(ctx.Issue.Index, 10),
335-
}
309+
issueID := strconv.FormatInt(ctx.Issue.Index, 10)
310+
headers := generateMetadataHeaders(repo)
311+
312+
maps.Copy(headers, generateSenderRecipientHeaders(ctx.Doer, recipient))
313+
maps.Copy(headers, generateReasonHeaders(reason))
314+
315+
headers["X-Gitea-Issue-ID"] = issueID
316+
headers["X-Gitea-Issue-Link"] = ctx.Issue.HTMLURL()
317+
headers["X-GitLab-Issue-IID"] = issueID
318+
319+
return headers
336320
}

services/mailer/mail_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"testing"
1717
texttmpl "text/template"
1818

19+
actions_model "code.gitea.io/gitea/models/actions"
1920
activities_model "code.gitea.io/gitea/models/activities"
2021
"code.gitea.io/gitea/models/db"
2122
issues_model "code.gitea.io/gitea/models/issues"
@@ -298,13 +299,13 @@ func testComposeIssueCommentMessage(t *testing.T, ctx *mailComment, recipients [
298299
return msgs[0]
299300
}
300301

301-
func TestGenerateAdditionalHeaders(t *testing.T) {
302+
func TestGenerateAdditionalHeadersForIssue(t *testing.T) {
302303
doer, _, issue, _ := prepareMailerTest(t)
303304

304305
comment := &mailComment{Issue: issue, Doer: doer}
305306
recipient := &user_model.User{Name: "test", Email: "test@gitea.com"}
306307

307-
headers := generateAdditionalHeaders(comment, "dummy-reason", recipient)
308+
headers := generateAdditionalHeadersForIssue(comment, "dummy-reason", recipient)
308309

309310
expected := map[string]string{
310311
"List-ID": "user2/repo1 <repo1.user2.localhost>",
@@ -441,6 +442,16 @@ func TestGenerateMessageIDForRelease(t *testing.T) {
441442
assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
442443
}
443444

445+
func TestGenerateMessageIDForActionsWorkflowRunStatusEmail(t *testing.T) {
446+
assert.NoError(t, unittest.PrepareTestDatabase())
447+
448+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
449+
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 795, RepoID: repo.ID})
450+
assert.NoError(t, run.LoadAttributes(db.DefaultContext))
451+
msgID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
452+
assert.Equal(t, "<user2/repo2/actions/runs/191@localhost>", msgID)
453+
}
454+
444455
func TestFromDisplayName(t *testing.T) {
445456
tmpl, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
446457
assert.NoError(t, err)

0 commit comments

Comments
 (0)