diff --git a/modules/setting/setting.go b/modules/setting/setting.go index f152a865a0..2fb301156d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -32,7 +32,7 @@ var ( UpdateChecker = struct { Enabled bool Endpoint string - Channel string // stable, dev, security + Channel string // stable, rc, beta, alpha, development }{ Enabled: true, Endpoint: "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/updates.xml", 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/routers/web/admin/admin.go b/routers/web/admin/admin.go index 901947a10a..37f945db53 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -144,6 +144,14 @@ func Dashboard(ctx *context.Context) { ctx.Data["ReleaseURL"] = info.ReleaseURL ctx.Data["UpdateChannel"] = info.Channel ctx.Data["DockerImage"] = info.DockerImage + ctx.Data["CurrentUpdateChannel"] = setting.UpdateChecker.Channel + ctx.Data["UpdateChannels"] = []map[string]string{ + {"value": "stable", "label": "Stable", "desc": "Production-ready releases"}, + {"value": "rc", "label": "Release Candidate", "desc": "Pre-release builds from merged PRs"}, + {"value": "beta", "label": "Beta", "desc": "Feature-complete, under testing"}, + {"value": "alpha", "label": "Alpha", "desc": "Early access, may have rough edges"}, + {"value": "development", "label": "Development", "desc": "Latest dev branch, bleeding edge"}, + } updateSystemStatus() ctx.Data["SysStatus"] = sysStatus @@ -166,6 +174,29 @@ func DashboardPost(ctx *context.Context) { updateSystemStatus() ctx.Data["SysStatus"] = sysStatus + // Handle update channel change + if channel := ctx.FormString("update_channel"); channel != "" { + validChannels := []string{"stable", "rc", "beta", "alpha", "development"} + isValid := false + for _, v := range validChannels { + if channel == v { + isValid = true + break + } + } + if isValid { + setting.UpdateChecker.Channel = channel + go func() { + if err := updatechecker.CheckForUpdate(); err != nil { + log.Error("CheckForUpdate after channel change: %v", err) + } + }() + ctx.Flash.Success("Update channel changed to: " + channel) + } + ctx.Redirect(setting.AppSubURL + "/-/admin") + return + } + // Run operation. if form.Op != "" { switch form.Op { 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..eb41a10b23 --- /dev/null +++ b/services/mailer/mail_update.go @@ -0,0 +1,93 @@ +// Copyright 2026 Moko Consulting +// 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) +} diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index bbe9ee62ea..ed2a866df5 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -8,6 +8,20 @@ {{if .DockerImage}}

docker pull {{.DockerImage}}

{{end}} {{end}} +
+

{{svg "octicon-broadcast" 16}} Update Channel

+
+ {{.CsrfTokenHtml}} +
+ +
+ +
+

{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}

diff --git a/updates.xml b/updates.xml index 28bdb0ba1d..8f21ddd206 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,12 +10,12 @@ MokoGitea update mokogitea application - 04.01.00 + 05.00.00 server stable - https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.04.00.00 + https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00 - git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.00.00 + git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00 @@ -27,50 +27,67 @@ MokoGitea update mokogitea application - 04.01.00 + 05.00.00 server - dev + rc + https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00 + + git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00 + + + + Moko Consulting + https://mokoconsulting.tech + + + MokoGitea + MokoGitea update + mokogitea + application + 05.00.00 + server + beta + https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00 + + git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00 + + + + Moko Consulting + https://mokoconsulting.tech + + + MokoGitea + MokoGitea update + mokogitea + application + 05.00.00 + server + alpha + https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00 + + git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00 + + + + Moko Consulting + https://mokoconsulting.tech + + + MokoGitea + MokoGitea update + mokogitea + application + 06.00.00-dev + server + development https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/dev - git.mokoconsulting.tech/mokoconsulting/mokogitea:latest-dev + git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.06.00.00-dev Moko Consulting https://mokoconsulting.tech - - MokoGitea - MokoGitea update - mokogitea - application - 04.01.00 - server - security - https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.04.00.00 - - git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.00.00 - - - - Moko Consulting - https://mokoconsulting.tech - - - MokoGitea - MokoGitea RC from PR #170 - mokogitea - application - 04.01.00-rc.170 - server - rc - https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/pulls/170 - - git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.01.00-rc.170 - - - - Moko Consulting - https://mokoconsulting.tech -