Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ef4aeb04a | |||
| 4ec61ec260 | |||
| b5defc2a4a | |||
| d77713dd77 | |||
| a45be34139 | |||
| d97955394f | |||
| 592a71968f | |||
| 64e1e37e20 | |||
| a847129f9c | |||
| 30e16cccc1 | |||
| b74cf800ef | |||
| 90f612f211 | |||
| 49fe3cf6eb | |||
| 13352e7213 | |||
| 07827bcc2e | |||
| 8509932b41 | |||
| 8ad1b8a110 | |||
| be5c2d35a5 |
@@ -140,6 +140,78 @@ jobs:
|
||||
exit 1
|
||||
"
|
||||
|
||||
- name: Update updates.xml
|
||||
if: success()
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
||||
DEPLOY_ENV: ${{ github.event.inputs.environment }}
|
||||
run: |
|
||||
# Only update updates.xml for production stable releases
|
||||
if [ "$DEPLOY_ENV" != "production" ]; then
|
||||
echo "Skipping updates.xml — dev deployments don't update stable channel"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract moko version from tag (e.g. v1.26.1-moko.05.01.01 -> 05.01.01)
|
||||
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
|
||||
if [ -z "$MOKO_VER" ]; then
|
||||
echo "Could not extract moko version from tag: $TAG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RELEASE_URL="https://${REGISTRY}/MokoConsulting/MokoGitea/releases/tag/${TAG}"
|
||||
DOCKER_IMG="${REGISTRY}/${IMAGE}:${TAG}"
|
||||
|
||||
python3 << PYEOF
|
||||
import json, os, re, base64, urllib.request
|
||||
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
registry = os.environ["REGISTRY"]
|
||||
tag = os.environ["TAG"]
|
||||
moko_ver = os.environ["MOKO_VER"]
|
||||
release_url = os.environ["RELEASE_URL"]
|
||||
docker_img = os.environ["DOCKER_IMG"]
|
||||
api = f"https://{registry}/api/v1/repos/MokoConsulting/MokoGitea"
|
||||
|
||||
# Fetch current updates.xml
|
||||
req = urllib.request.Request(f"{api}/contents/updates.xml?ref=main",
|
||||
headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
data = json.loads(resp.read())
|
||||
sha = data["sha"]
|
||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
||||
|
||||
# Update stable channel version, infourl, and docker tag
|
||||
content = re.sub(
|
||||
r"(<tags><tag>stable</tag></tags>[\s\S]*?<version>)[^<]*(</version>)",
|
||||
rf"\g<1>{moko_ver}\2", content)
|
||||
content = re.sub(
|
||||
r"(<tags><tag>stable</tag></tags>[\s\S]*?<infourl[^>]*>)[^<]*(</infourl>)",
|
||||
rf"\g<1>{release_url}\2", content)
|
||||
content = re.sub(
|
||||
r"(<tags><tag>stable</tag></tags>[\s\S]*?<downloadurl[^>]*>)[^<]*(</downloadurl>)",
|
||||
rf"\g<1>{docker_img}\2", content)
|
||||
|
||||
# Also update VERSION comment at top
|
||||
content = re.sub(r"VERSION: [^\n]*", f"VERSION: {moko_ver}", content)
|
||||
|
||||
# Push updated file
|
||||
encoded = base64.b64encode(content.encode()).decode()
|
||||
payload = json.dumps({
|
||||
"message": f"chore(ci): update updates.xml to {moko_ver}",
|
||||
"content": encoded,
|
||||
"sha": sha,
|
||||
"branch": "main",
|
||||
}).encode()
|
||||
req = urllib.request.Request(f"{api}/contents/updates.xml",
|
||||
data=payload, method="PUT",
|
||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
print(f"updates.xml updated to {moko_ver}")
|
||||
PYEOF
|
||||
|
||||
- name: Disable maintenance mode
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -18,6 +18,7 @@ import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context/upload"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/convert"
|
||||
release_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/release"
|
||||
)
|
||||
|
||||
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
|
||||
@@ -263,6 +264,14 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate checksums after new attachment
|
||||
rel, relErr := repo_model.GetReleaseByID(ctx, releaseID)
|
||||
if relErr == nil {
|
||||
if checksumErr := release_service.GenerateReleaseChecksums(ctx, rel); checksumErr != nil {
|
||||
log.Error("GenerateReleaseChecksums after upload: %v", checksumErr)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplBranding templates.TplName = "admin/branding"
|
||||
|
||||
// brandingImageDir returns the path to the custom branding images directory.
|
||||
func brandingImageDir() string {
|
||||
return filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
}
|
||||
|
||||
// Branding shows the admin branding page.
|
||||
func Branding(ctx *context.Context) {
|
||||
ctx.Data["Title"] = "Branding"
|
||||
ctx.Data["PageIsAdminBranding"] = true
|
||||
|
||||
imgDir := brandingImageDir()
|
||||
ctx.Data["HasNavIcon"] = fileExists(filepath.Join(imgDir, "logo-small.png"))
|
||||
ctx.Data["HasLogo"] = fileExists(filepath.Join(imgDir, "logo.png"))
|
||||
ctx.Data["HasFavicon"] = fileExists(filepath.Join(imgDir, "favicon.png"))
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBranding)
|
||||
}
|
||||
|
||||
// BrandingUpload handles branding image uploads.
|
||||
func BrandingUpload(ctx *context.Context) {
|
||||
imageType := ctx.FormString("type")
|
||||
if imageType == "" {
|
||||
ctx.Flash.Error("No image type specified")
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
var filename string
|
||||
switch imageType {
|
||||
case "nav-icon":
|
||||
filename = "logo-small.png"
|
||||
case "logo":
|
||||
filename = "logo.png"
|
||||
case "favicon":
|
||||
filename = "favicon.png"
|
||||
default:
|
||||
ctx.Flash.Error("Invalid image type: " + imageType)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := ctx.Req.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.Flash.Error("Upload failed: " + err.Error())
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Validate file size (max 2MB)
|
||||
if header.Size > 2*1024*1024 {
|
||||
ctx.Flash.Error("File too large (max 2MB)")
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the custom image directory exists
|
||||
imgDir := brandingImageDir()
|
||||
if err := os.MkdirAll(imgDir, 0o755); err != nil {
|
||||
ctx.Flash.Error("Failed to create image directory")
|
||||
log.Error("MkdirAll %s: %v", imgDir, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Write the file
|
||||
destPath := filepath.Join(imgDir, filename)
|
||||
dest, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
ctx.Flash.Error("Failed to save image")
|
||||
log.Error("Create %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
if _, err := io.Copy(dest, file); err != nil {
|
||||
ctx.Flash.Error("Failed to write image")
|
||||
log.Error("Copy to %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Also remove SVG override if present (PNG should take priority)
|
||||
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
|
||||
if fileExists(svgPath) {
|
||||
os.Remove(svgPath)
|
||||
log.Info("Removed SVG override: %s", svgPath)
|
||||
}
|
||||
|
||||
ctx.Flash.Success("Branding image updated: " + imageType)
|
||||
log.Info("Branding image uploaded: %s (%d bytes)", filename, header.Size)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
|
||||
}
|
||||
|
||||
members, _, err := organization.FindOrgMembers(ctx, opts)
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgMembers", err)
|
||||
return
|
||||
@@ -102,6 +102,14 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
const orgOverviewTeamsLimit = 5
|
||||
ctx.Data["OrgOverviewMembers"] = members
|
||||
ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)]
|
||||
ctx.Data["Members"] = members
|
||||
ctx.Data["NumMembers"] = len(members)
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
|
||||
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
|
||||
ctx.Data["IsPublicMember"] = func(uid int64) bool {
|
||||
return membersIsPublic[uid]
|
||||
}
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
|
||||
|
||||
@@ -765,6 +765,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/settings", admin.ConfigSettings)
|
||||
})
|
||||
|
||||
m.Group("/branding", func() {
|
||||
m.Get("", admin.Branding)
|
||||
m.Post("/upload", admin.BrandingUpload)
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
m.Get("/stats", admin.MonitorStats)
|
||||
m.Get("/cron", admin.CronTasks)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package release
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
|
||||
attachment_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/attachment"
|
||||
)
|
||||
|
||||
// GenerateReleaseChecksums computes SHA256 checksums for all attachments
|
||||
// on a release and adds a checksums.sha256 manifest file as an attachment.
|
||||
func GenerateReleaseChecksums(ctx context.Context, rel *repo_model.Release) error {
|
||||
// Load attachments into rel.Attachments
|
||||
if err := repo_model.GetReleaseAttachments(ctx, rel); err != nil {
|
||||
return fmt.Errorf("GetReleaseAttachments: %w", err)
|
||||
}
|
||||
|
||||
if len(rel.Attachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove existing checksums file if present
|
||||
for _, a := range rel.Attachments {
|
||||
if a.Name == "checksums.sha256" {
|
||||
if err := repo_model.DeleteAttachment(ctx, a, true); err != nil {
|
||||
log.Warn("Failed to delete old checksums.sha256: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Compute SHA256 for each attachment
|
||||
var manifest bytes.Buffer
|
||||
for _, a := range rel.Attachments {
|
||||
if a.Name == "checksums.sha256" {
|
||||
continue
|
||||
}
|
||||
|
||||
fr, err := storage.Attachments.Open(a.RelativePath())
|
||||
if err != nil {
|
||||
log.Warn("Cannot open attachment %s for checksumming: %v", a.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, fr); err != nil {
|
||||
fr.Close()
|
||||
log.Warn("Cannot read attachment %s for checksumming: %v", a.Name, err)
|
||||
continue
|
||||
}
|
||||
fr.Close()
|
||||
|
||||
fmt.Fprintf(&manifest, "%x %s\n", h.Sum(nil), a.Name)
|
||||
}
|
||||
|
||||
if manifest.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the checksums.sha256 attachment
|
||||
checksumAttach := &repo_model.Attachment{
|
||||
RepoID: rel.RepoID,
|
||||
ReleaseID: rel.ID,
|
||||
Name: "checksums.sha256",
|
||||
}
|
||||
|
||||
if _, err := attachment_service.NewAttachment(ctx, checksumAttach, &manifest, int64(manifest.Len())); err != nil {
|
||||
return fmt.Errorf("create checksums.sha256 attachment: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Generated checksums.sha256 for release %s (repo %d)", rel.TagName, rel.RepoID)
|
||||
return nil
|
||||
}
|
||||
@@ -190,6 +190,13 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate SHA256 checksums for all release attachments
|
||||
if len(attachmentUUIDs) > 0 {
|
||||
if err := GenerateReleaseChecksums(gitRepo.Ctx, rel); err != nil {
|
||||
log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !rel.IsDraft {
|
||||
notify_service.NewRelease(gitRepo.Ctx, rel)
|
||||
}
|
||||
@@ -344,6 +351,13 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate checksums when attachments change
|
||||
if len(addAttachmentUUIDs) > 0 || len(delAttachmentUUIDs) > 0 {
|
||||
if err := GenerateReleaseChecksums(ctx, rel); err != nil {
|
||||
log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !rel.IsDraft {
|
||||
if !isTagCreated && !isConvertedFromTag {
|
||||
notify_service.UpdateRelease(gitRepo.Ctx, doer, rel)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
{{template "admin/layout_head" (dict "pageClass" "admin branding")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-paintbrush" 16}} Branding
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>Upload custom branding images. Changes take effect immediately — no restart required.</p>
|
||||
|
||||
<div class="ui three stackable cards">
|
||||
<!-- Nav Icon -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Nav Icon</div>
|
||||
<div class="meta">Top-left corner (30x30px recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo-small.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="nav-icon">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasNavIcon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Logo -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Login Logo</div>
|
||||
<div class="meta">Login page and homepage (wide format recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="logo">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasLogo}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favicon -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Favicon</div>
|
||||
<div class="meta">Browser tab icon (256x256px recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/favicon.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="favicon">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml,image/x-icon" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasFavicon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_tail" .}}
|
||||
@@ -8,6 +8,20 @@
|
||||
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="ui segment">
|
||||
<h4 class="ui header">{{svg "octicon-broadcast" 16}} Update Channel</h4>
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin" class="tw-flex tw-items-end tw-gap-4">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field tw-flex-1">
|
||||
<select name="update_channel" class="ui dropdown">
|
||||
{{range .UpdateChannels}}
|
||||
<option value="{{.value}}" {{if eq $.CurrentUpdateChannel .value}}selected{{end}}>{{.label}} — {{.desc}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-sync" 14}} Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
|
||||
</h4>
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
||||
{{svg "octicon-paintbrush" 16}} Branding
|
||||
</a>
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
|
||||
<div class="menu">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="navbar-left">
|
||||
<!-- the logo -->
|
||||
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
||||
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
</a>
|
||||
|
||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
|
||||
<div class="tw-mb-8 tw-px-8">
|
||||
<div class="center">
|
||||
<img class="logo" width="220" height="220" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
<div class="hero">
|
||||
<h1 class="ui icon header title tw-text-balance">
|
||||
{{AppName}}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="ui container tw-flex">
|
||||
<div class="item tw-flex-1">
|
||||
<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home_title"}}">
|
||||
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
+58
-41
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 04.00.00
|
||||
VERSION: 05.00.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
@@ -10,12 +10,12 @@
|
||||
<description>MokoGitea update</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>04.01.00</version>
|
||||
<version>05.00.00</version>
|
||||
<client>server</client>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.04.00.00</infourl>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.00.00</downloadurl>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
@@ -27,50 +27,67 @@
|
||||
<description>MokoGitea update</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>04.01.00</version>
|
||||
<version>05.00.00</version>
|
||||
<client>server</client>
|
||||
<tags><tag>dev</tag></tags>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<infourl title="MokoGitea RC">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea update</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>05.00.00</version>
|
||||
<client>server</client>
|
||||
<tags><tag>beta</tag></tags>
|
||||
<infourl title="MokoGitea Beta">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea update</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>05.00.00</version>
|
||||
<client>server</client>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
<infourl title="MokoGitea Alpha">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea update</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>06.00.00-dev</version>
|
||||
<client>server</client>
|
||||
<tags><tag>development</tag></tags>
|
||||
<infourl title="MokoGitea Dev">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/dev</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:latest-dev</downloadurl>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.06.00.00-dev</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea update</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>04.01.00</version>
|
||||
<client>server</client>
|
||||
<tags><tag>security</tag></tags>
|
||||
<infourl title="MokoGitea Security">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.04.00.00</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.00.00</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea RC from PR #170</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<version>04.01.00-rc.170</version>
|
||||
<client>server</client>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<infourl title="MokoGitea RC">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/pulls/170</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.01.00-rc.170</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
</updates>
|
||||
|
||||
Reference in New Issue
Block a user