Compare commits

..

13 Commits

Author SHA1 Message Date
jmiller a45be34139 Merge pull request 'feat(ci): auto-update updates.xml on production deploy' (#179) from feat/auto-update-xml into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 25s
2026-05-26 00:56:19 +00:00
Jonathan Miller d97955394f feat(ci): auto-update updates.xml on production deploy
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
After a successful production deployment, the deploy workflow now
automatically updates updates.xml on main with the new version,
release URL, and docker image tag for the stable channel.

Dev deployments skip this step — only production releases update
the stable channel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:49:04 -05:00
jmiller 592a71968f Merge pull request 'feat(ci): enable maintenance mode during deployments' (#177) from feat/deploy-maintenance-mode into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 23s
2026-05-26 00:35:09 +00:00
Jonathan Miller d55b79a9ff feat(ci): enable maintenance mode during deployments
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
The deploy workflow now:
1. Enables maintenance mode before building (users see maintenance page)
2. Builds, pushes, and restarts the container
3. Disables maintenance mode after health check passes (if: always)

Uses Gitea's built-in maintenance mode via admin config API.
If the instance is already down, the enable step gracefully warns
instead of failing. The disable step runs even if deploy fails
to avoid leaving the instance in maintenance mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:33:59 -05:00
jmiller 64e1e37e20 Merge pull request 'fix: generate checksums on API asset upload' (#175) from feat/release-sha-checksums into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 18s
2026-05-26 00:15:52 +00:00
jmiller 30e16cccc1 Merge pull request 'feat: auto-generate SHA256 checksums for release attachments' (#174) from feat/release-sha-checksums into dev 2026-05-26 00:08:48 +00:00
jmiller b74cf800ef Merge pull request 'feat: update checker channels, email + ntfy notifications' (#173) from feat/update-checker-channels into dev 2026-05-26 00:08:09 +00:00
Jonathan Miller 49fe3cf6eb feat: add ntfy push notification for update checker
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Update notifications now go through three channels:
- Admin dashboard banner (existing)
- Email to admin (added in previous commit)
- ntfy push notification (new)

Configure in app.ini:
  [ntfy]
  ENABLED = true
  SERVER_URL = https://ntfy.mokoconsulting.tech
  DEFAULT_TOPIC = mokogitea
  TOKEN = (optional bearer token)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 18:34:14 -05:00
Jonathan Miller 13352e7213 feat: email admin when MokoGitea update is detected
The update checker now emails the first admin user when a new version
is found on the configured channel. Notifications are deduplicated —
only sent once per new version, not on every cron tick.

- Added NotifyFunc callback in updatechecker module
- Wired to mailer in cron task registration
- Created mail_update.go with plain-text email including version,
  channel, release URL, and docker pull command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 18:24:03 -05:00
Jonathan Miller 07827bcc2e test: bump dev channel to 06.00.00-dev to test update checker
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 18:19:32 -05:00
Jonathan Miller 8509932b41 feat: update channel selector dropdown on admin dashboard
Add a dropdown on the admin dashboard to switch between update streams
(stable, rc, beta, alpha, development) matching the Joomla pattern.

Changes:
- Admin dashboard shows channel selector with descriptions
- POST handler validates and applies channel change in-memory
- Triggers immediate re-check against updates.xml after switch
- updates.xml has all 5 standard channels with descriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 17:58:39 -05:00
Jonathan Miller 8ad1b8a110 chore: align update streams to standard channels (dev/alpha/beta/rc/stable)
Matches the Joomla update server pattern used across all Moko repos.
Removed the non-standard 'security' channel. All five standard
channels now present in updates.xml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 17:55:28 -05:00
Jonathan Miller be5c2d35a5 chore: bump updates.xml to v05.00.00
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 17:53:13 -05:00
8 changed files with 322 additions and 48 deletions
+104 -2
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
required: true
default: 'latest'
environment:
@@ -30,6 +30,7 @@ env:
DEPLOY_HOST: git.mokoconsulting.tech
DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
deploy:
@@ -47,15 +48,30 @@ jobs:
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
echo "instance_url=https://git.mokoconsulting.tech" >> $GITHUB_OUTPUT
else
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
echo "branch=dev" >> $GITHUB_OUTPUT
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
fi
- name: Build, push, and deploy via SSH
- name: Enable maintenance mode
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: |
echo "Enabling maintenance mode on ${INSTANCE_URL}..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":true}' \
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
- name: Build and deploy via SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TAG: ${{ steps.config.outputs.tag }}
@@ -124,6 +140,92 @@ 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:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: |
echo "Disabling maintenance mode on ${INSTANCE_URL}..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":false}' \
|| echo "WARNING: Could not disable maintenance mode"
- name: Verify
run: |
sleep 5
+1 -1
View File
@@ -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",
+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)
}
+31
View File
@@ -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 {
+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)
}
+14
View File
@@ -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>
+58 -41
View File
@@ -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>