From 13352e721346bac7dedc2da613e2a8422ea60cda Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 25 May 2026 18:24:03 -0500 Subject: [PATCH] feat: email admin when MokoGitea update is detected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update checker now emails the first admin user when a new version is found on the configured channel. Notifications are deduplicated — only sent once per new version, not on every cron tick. - Added NotifyFunc callback in updatechecker module - Wired to mailer in cron task registration - Created mail_update.go with plain-text email including version, channel, release URL, and docker pull command Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/updatechecker/updatechecker.go | 19 +++++++-- services/cron/tasks_basic.go | 6 +++ services/mailer/mail_update.go | 53 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 services/mailer/mail_update.go diff --git a/modules/updatechecker/updatechecker.go b/modules/updatechecker/updatechecker.go index 61093db2f4..83cfbd37eb 100644 --- a/modules/updatechecker/updatechecker.go +++ b/modules/updatechecker/updatechecker.go @@ -26,9 +26,14 @@ type UpdateInfo struct { CheckedAt time.Time } +// NotifyFunc is called when a new update is detected for the first time. +// Set this from the cron/mailer layer to send admin email notifications. +var NotifyFunc func(info *UpdateInfo) + var ( - cachedInfo *UpdateInfo - mu sync.RWMutex + cachedInfo *UpdateInfo + lastNotifiedVer string + mu sync.RWMutex ) // xmlUpdates mirrors the updates.xml structure (Joomla-style). @@ -134,16 +139,22 @@ func CheckForUpdate() error { } // Update is available if the latest version string is not a prefix of the current version. - // e.g., current "1.26.1+305-gabcdef" does not start with "04.00.00" - // This handles both moko semver and git-describe suffixed versions. info.UpdateAvailable = latestVersion != "" && !strings.Contains(currentVersion, latestVersion) mu.Lock() cachedInfo = info + // Notify only once per new version (avoid spamming on every cron tick) + shouldNotify := info.UpdateAvailable && latestVersion != lastNotifiedVer + if shouldNotify { + lastNotifiedVer = latestVersion + } mu.Unlock() if info.UpdateAvailable { log.Info("MokoGitea update available: %s [%s] (current: %s)", latestVersion, channel, currentVersion) + if shouldNotify && NotifyFunc != nil { + NotifyFunc(info) + } } else { log.Debug("MokoGitea is up to date: %s [%s]", currentVersion, channel) } diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index 9b1788dfef..c91249e06d 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -15,6 +15,7 @@ import ( "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/updatechecker" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mailer" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/migrations" mirror_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mirror" packages_cleanup_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/packages/cleanup" @@ -190,6 +191,11 @@ func initBasicTasks() { } func registerUpdateChecker() { + // Wire up email notification for admin when updates are detected + updatechecker.NotifyFunc = func(info *updatechecker.UpdateInfo) { + mailer.SendUpdateNotification(info.LatestVersion, info.Channel, info.ReleaseURL, info.DockerImage) + } + RegisterTaskFatal("update_checker", &BaseConfig{ Enabled: true, RunAtStart: true, diff --git a/services/mailer/mail_update.go b/services/mailer/mail_update.go new file mode 100644 index 0000000000..1b35106589 --- /dev/null +++ b/services/mailer/mail_update.go @@ -0,0 +1,53 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package mailer + +import ( + "context" + "fmt" + + user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" + sender_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mailer/sender" +) + +// SendUpdateNotification emails all admin users when a new MokoGitea version is available. +func SendUpdateNotification(version, channel, releaseURL, dockerImage string) { + if setting.MailService == nil { + log.Debug("Update notification skipped: mail service not configured") + return + } + + admin, err := user_model.GetAdminUser(context.Background()) + if err != nil { + log.Error("SendUpdateNotification: GetAdminUser: %v", err) + return + } + + subject := fmt.Sprintf("[MokoGitea] Update available: %s (%s)", version, channel) + + body := fmt.Sprintf(`MokoGitea Update Available + +A new version is available on the %s channel. + +Version: %s +Channel: %s +Current: %s`, channel, version, channel, setting.AppVer) + + if releaseURL != "" { + body += fmt.Sprintf("\nRelease: %s", releaseURL) + } + if dockerImage != "" { + body += fmt.Sprintf("\nDocker: docker pull %s", dockerImage) + } + + body += fmt.Sprintf("\n\nUpdate the channel in Site Administration > Dashboard.\n\n— %s", setting.AppName) + + msg := sender_service.NewMessage(admin.EmailTo(), subject, body) + msg.Info = "Update notification" + + SendAsync(msg) + log.Info("Update notification sent to %s for version %s [%s]", admin.Email, version, channel) +}