Merge remote-tracking branch 'origin/main' into dev

This commit is contained in:
Jonathan Miller
2026-05-25 18:43:15 -05:00
3 changed files with 114 additions and 4 deletions
+15 -4
View File
@@ -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)
}
+6
View File
@@ -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,
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package mailer
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
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 the admin and sends ntfy push when a new MokoGitea version is available.
func SendUpdateNotification(version, channel, releaseURL, dockerImage string) {
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)
// Send email to admin
if setting.MailService != nil {
admin, err := user_model.GetAdminUser(context.Background())
if err != nil {
log.Error("SendUpdateNotification: GetAdminUser: %v", err)
} else {
msg := sender_service.NewMessage(admin.EmailTo(), subject, body)
msg.Info = "Update notification"
SendAsync(msg)
log.Info("Update email sent to %s for version %s [%s]", admin.Email, version, channel)
}
}
// Send ntfy push notification
if setting.Ntfy.Enabled && setting.Ntfy.ServerURL != "" {
sendNtfyNotification(subject, body, releaseURL)
}
}
func sendNtfyNotification(title, body, clickURL string) {
url := fmt.Sprintf("%s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
req, err := http.NewRequest("POST", url, bytes.NewBufferString(body))
if err != nil {
log.Error("ntfy: create request: %v", err)
return
}
req.Header.Set("Title", title)
req.Header.Set("Priority", "high")
req.Header.Set("Tags", "arrow_up,mokogitea")
if clickURL != "" {
req.Header.Set("Click", clickURL)
}
if setting.Ntfy.Token != "" {
req.Header.Set("Authorization", "Bearer "+setting.Ntfy.Token)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error("ntfy: send notification: %v", err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= 300 {
log.Error("ntfy: unexpected status %d", resp.StatusCode)
return
}
log.Info("ntfy: update notification sent to %s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
}