Compare commits

..

11 Commits

Author SHA1 Message Date
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
Jonathan Miller a847129f9c fix: generate checksums on API asset upload, not just CreateRelease
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
The API endpoint POST /releases/{id}/assets bypasses CreateRelease
and UpdateRelease, so checksums were not generated for API uploads.
Added GenerateReleaseChecksums call after successful asset upload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:15:12 -05: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 90f612f211 feat: auto-generate SHA256 checksums for release attachments
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
When a release is created or updated with attachments, automatically
compute SHA256 checksums for every file and attach a checksums.sha256
manifest file. The manifest follows the standard sha256sum format:
  <hash>  <filename>

Existing checksums.sha256 files are replaced when attachments change.
Checksums are generated for both CreateRelease and UpdateRelease flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:05:13 -05: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
11 changed files with 325 additions and 78 deletions
+2 -32
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
required: true
default: 'latest'
environment:
@@ -30,7 +30,6 @@ env:
DEPLOY_HOST: git.mokoconsulting.tech
DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
deploy:
@@ -48,30 +47,15 @@ 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: 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
- name: Build, push, and deploy via SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TAG: ${{ steps.config.outputs.tag }}
@@ -140,20 +124,6 @@ jobs:
exit 1
"
- 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)
}
@@ -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))
}
+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)
}
+82
View File
@@ -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
}
+14
View File
@@ -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)
+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>