05f1ac1a12
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
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) <noreply@anthropic.com>
107 lines
2.5 KiB
Go
107 lines
2.5 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// 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
|
|
}
|