diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 48de439f6a..e4cf454cb7 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -576,6 +576,12 @@ team_invite.text_1 = %[1]s has invited you to join team %[2]s in organization %[
team_invite.text_2 = Please click the following link to join the team:
team_invite.text_3 = Note: This invitation was intended for %[1]s. If you were not expecting this invitation, you can ignore this email.
+actions.successful_run_after_failure_subject = Workflow %[1]s recovered in repository %[2]s
+actions.not_successful_run_subject = Workflow %[1]s failed in repository %[2]s
+actions.successful_run_after_failure = Workflow %[1]s recovered in repository %[2]s
+actions.not_successful_run = Workflow %[1]s failed in repository %[2]s
+actions.run_info = This Run's Status: %[1]s (just updated from %[2]s)
Previous Run's Status: %[3]s
Branch: %[4]s
Commit: %[5]s
Triggered because: %[6]s
Triggered by: %[7]s
+
[modal]
yes = Yes
no = No
diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go
new file mode 100644
index 0000000000..7c63603a98
--- /dev/null
+++ b/services/mailer/mail_actions.go
@@ -0,0 +1,84 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+package mailer
+
+import (
+ "bytes"
+
+ actions_model "forgejo.org/models/actions"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
+)
+
+const (
+ tplActionNowDone base.TplName = "actions/now_done"
+)
+
+// requires !run.Status.IsSuccess() or !lastRun.Status.IsSuccess()
+func MailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
+ if setting.MailService == nil {
+ // No mail service configured
+ return nil
+ }
+
+ if run.TriggerUser.Email != "" && run.TriggerUser.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
+ if err := sendMailActionRun(run.TriggerUser, run, priorStatus, lastRun); err != nil {
+ return err
+ }
+ }
+
+ if run.Repo.Owner.Email != "" && run.Repo.Owner.Email != run.TriggerUser.Email && run.Repo.Owner.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
+ if err := sendMailActionRun(run.Repo.Owner, run, priorStatus, lastRun); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
+ var (
+ locale = translation.NewLocale(to.Language)
+ content bytes.Buffer
+ )
+
+ var subject string
+ if run.Status.IsSuccess() {
+ subject = locale.TrString("mail.actions.successful_run_after_failure_subject", run.Title, run.Repo.FullName())
+ } else {
+ subject = locale.TrString("mail.actions.not_successful_run", run.Title, run.Repo.FullName())
+ }
+
+ commitSHA := run.CommitSHA
+ if len(commitSHA) > 7 {
+ commitSHA = commitSHA[:7]
+ }
+ branch := run.PrettyRef()
+
+ data := map[string]any{
+ "locale": locale,
+ "Link": run.HTMLURL(),
+ "Subject": subject,
+ "Language": locale.Language(),
+ "RepoFullName": run.Repo.FullName(),
+ "Run": run,
+ "TriggerUserLink": run.TriggerUser.HTMLURL(),
+ "LastRun": lastRun,
+ "PriorStatus": priorStatus,
+ "CommitSHA": commitSHA,
+ "Branch": branch,
+ "IsSuccess": run.Status.IsSuccess(),
+ }
+
+ if err := bodyTemplates.ExecuteTemplate(&content, string(tplActionNowDone), data); err != nil {
+ return err
+ }
+
+ msg := NewMessage(to.EmailTo(), subject, content.String())
+ msg.Info = subject
+ SendAsync(msg)
+
+ return nil
+}
diff --git a/services/mailer/notify.go b/services/mailer/notify.go
index 8acfa86fb6..ae7226fac3 100644
--- a/services/mailer/notify.go
+++ b/services/mailer/notify.go
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
+ actions_model "forgejo.org/models/actions"
activities_model "forgejo.org/models/activities"
issues_model "forgejo.org/models/issues"
repo_model "forgejo.org/models/repo"
@@ -208,3 +209,12 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *
func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) {
MailNewUser(ctx, newUser)
}
+
+func (m *mailNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
+ if run.Status.IsSuccess() && lastRun.Status.IsSuccess() {
+ return
+ }
+ if err := MailActionRun(run, priorStatus, lastRun); err != nil {
+ log.Error("MailActionRunNowDone: %v", err)
+ }
+}
diff --git a/templates/mail/actions/now_done.tmpl b/templates/mail/actions/now_done.tmpl
new file mode 100644
index 0000000000..f3065705df
--- /dev/null
+++ b/templates/mail/actions/now_done.tmpl
@@ -0,0 +1,32 @@
+
+
+
+ {{if .IsSuccess}}
+ {{.locale.Tr "mail.actions.successful_run_after_failure" $action_run_link $repo_link}}
+ {{else}}
+ {{.locale.Tr "mail.actions.not_successful_run" $action_run_link $repo_link}}
+ {{end}}
+
+
+ {{.locale.Tr "mail.actions.run_info" .Run.Status .PriorStatus .LastRun.Status .Branch .CommitSHA .Run.TriggerEvent $trigger_user_link}}
+