From 05f1ac1a1275e8d57c63777f6bd4f2b44835c761 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 19 May 2026 21:41:10 -0500 Subject: [PATCH] feat(admin): add MokoGitea update checker (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace removed upstream Gitea update checker with MokoGitea-native version that checks our own releases API. - New module: modules/updatechecker/ — fetches latest release from git.mokoconsulting.tech, compares semver, caches result - Cron task: runs every 24h (and at startup) - Admin dashboard: shows green banner when update available - Configurable via [update_checker] in app.ini Closes #74 Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/setting/setting.go | 16 ++++ modules/updatechecker/updatechecker.go | 106 +++++++++++++++++++++++++ routers/web/admin/admin.go | 10 ++- services/cron/tasks_basic.go | 14 ++++ templates/admin/dashboard.tmpl | 7 ++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 modules/updatechecker/updatechecker.go diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5bfe68697a..659fae4ed7 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -28,6 +28,15 @@ var ( CfgProvider ConfigProvider IsWindows bool + // UpdateChecker configuration for MokoGitea version checking + UpdateChecker = struct { + Enabled bool + Endpoint string + }{ + Enabled: true, + Endpoint: "https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoGitea/releases/latest", + } + // IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for: // * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable) // * Panic in dev or testing mode to make the problem more obvious and easier to debug @@ -158,9 +167,16 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { loadMarkupFrom(cfg) loadGlobalLockFrom(cfg) loadOtherFrom(cfg) + loadUpdateCheckerFrom(cfg) return nil } +func loadUpdateCheckerFrom(cfg ConfigProvider) { + sec := cfg.Section("update_checker") + UpdateChecker.Enabled = sec.Key("ENABLED").MustBool(true) + UpdateChecker.Endpoint = sec.Key("ENDPOINT").MustString(UpdateChecker.Endpoint) +} + func loadRunModeFrom(rootCfg ConfigProvider) { rootSec := rootCfg.Section("") RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername()) diff --git a/modules/updatechecker/updatechecker.go b/modules/updatechecker/updatechecker.go new file mode 100644 index 0000000000..83753d6ff9 --- /dev/null +++ b/modules/updatechecker/updatechecker.go @@ -0,0 +1,106 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package updatechecker + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// UpdateInfo holds the result of the latest update check. +type UpdateInfo struct { + UpdateAvailable bool + LatestVersion string + ReleaseURL string + CheckedAt time.Time +} + +var ( + cachedInfo *UpdateInfo + mu sync.RWMutex +) + +// giteaRelease is the subset of Gitea's release API response we need. +type giteaRelease struct { + TagName string `json:"tag_name"` + HTMLURL string `json:"html_url"` + Draft bool `json:"draft"` +} + +// CheckForUpdate fetches the latest release from the configured endpoint +// and compares it to the running version. +func CheckForUpdate() error { + if !setting.UpdateChecker.Enabled || setting.UpdateChecker.Endpoint == "" { + return nil + } + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Get(setting.UpdateChecker.Endpoint) + if err != nil { + return fmt.Errorf("update check failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("update check returned status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading update response: %w", err) + } + + var release giteaRelease + if err := json.Unmarshal(body, &release); err != nil { + return fmt.Errorf("parsing update response: %w", err) + } + + if release.Draft || release.TagName == "" { + return nil + } + + latestVersion := strings.TrimPrefix(release.TagName, "v") + currentVersion := setting.AppVer + + info := &UpdateInfo{ + LatestVersion: latestVersion, + ReleaseURL: release.HTMLURL, + CheckedAt: time.Now(), + } + + // Simple comparison: if latest != current, update is available. + // This handles both upgrades and the case where versions differ + // in any way (patch, upstream bump, etc.) + info.UpdateAvailable = latestVersion != "" && !strings.HasPrefix(currentVersion, latestVersion) + + mu.Lock() + cachedInfo = info + mu.Unlock() + + if info.UpdateAvailable { + log.Info("MokoGitea update available: %s (current: %s)", latestVersion, currentVersion) + } else { + log.Debug("MokoGitea is up to date: %s", currentVersion) + } + + return nil +} + +// GetUpdateInfo returns the cached update check result. +func GetUpdateInfo() *UpdateInfo { + mu.RLock() + defer mu.RUnlock() + if cachedInfo == nil { + return &UpdateInfo{} + } + return cachedInfo +} diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 3c03900c5d..0931ada33b 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/updatechecker" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -135,8 +136,13 @@ func prepareStartupProblemsAlert(ctx *context.Context) { func Dashboard(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.dashboard") ctx.Data["PageIsAdminDashboard"] = true - // MokoGitea: upstream update checker removed — this is an independent fork - ctx.Data["NeedUpdate"] = false + + // MokoGitea update checker + info := updatechecker.GetUpdateInfo() + ctx.Data["NeedUpdate"] = info.UpdateAvailable + ctx.Data["LatestVersion"] = info.LatestVersion + ctx.Data["ReleaseURL"] = info.ReleaseURL + updateSystemStatus() ctx.Data["SysStatus"] = sysStatus ctx.Data["SSH"] = setting.SSH diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index c620959cc1..206377de85 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/updatechecker" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" @@ -183,4 +184,17 @@ func initBasicTasks() { registerCleanupPackages() } registerSyncRepoLicenses() + if setting.UpdateChecker.Enabled { + registerUpdateChecker() + } +} + +func registerUpdateChecker() { + RegisterTaskFatal("update_checker", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 24h", + }, func(ctx context.Context, _ *user_model.User, _ Config) error { + return updatechecker.CheckForUpdate() + }) } diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 288be8b7ae..8d3a9762ca 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -1,5 +1,12 @@ {{template "admin/layout_head" (dict "pageClass" "admin dashboard")}}
+ {{if .NeedUpdate}} +
+
{{svg "octicon-info"}} MokoGitea Update Available
+

A new version {{.LatestVersion}} is available. + {{if .ReleaseURL}}View release notes{{end}}

+
+ {{end}}

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

-- 2.52.0