Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bf7313e40 | |||
| 6c06384966 |
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/updateserver"
|
||||
)
|
||||
|
||||
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
|
||||
// from the repository's releases.
|
||||
func ServeUpdatesXML(ctx *context.Context) {
|
||||
xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateJoomlaXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
@@ -1494,6 +1494,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
|
||||
// end "/{username}/{reponame}": repo releases
|
||||
|
||||
// "/{username}/{reponame}": update server (Joomla-compatible updates.xml)
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/updates.xml", repo.ServeUpdatesXML)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// Joomla-compatible updates.xml structures for XML marshaling.
|
||||
|
||||
type xmlUpdates struct {
|
||||
XMLName xml.Name `xml:"updates"`
|
||||
Updates []xmlUpdate `xml:"update"`
|
||||
}
|
||||
|
||||
type xmlUpdate struct {
|
||||
Name string `xml:"name"`
|
||||
Description string `xml:"description"`
|
||||
Element string `xml:"element"`
|
||||
Type string `xml:"type"`
|
||||
Client string `xml:"client"`
|
||||
Version string `xml:"version"`
|
||||
CreationDate string `xml:"creationDate"`
|
||||
InfoURL xmlInfoURL `xml:"infourl"`
|
||||
Downloads xmlDownloads `xml:"downloads"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
Tags xmlTags `xml:"tags"`
|
||||
ChangelogURL string `xml:"changelogurl,omitempty"`
|
||||
Maintainer string `xml:"maintainer,omitempty"`
|
||||
MaintainerURL string `xml:"maintainerurl,omitempty"`
|
||||
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
|
||||
}
|
||||
|
||||
type xmlInfoURL struct {
|
||||
Title string `xml:"title,attr"`
|
||||
URL string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type xmlDownloads struct {
|
||||
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
|
||||
}
|
||||
|
||||
type xmlDownloadURL struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Format string `xml:"format,attr"`
|
||||
URL string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type xmlTags struct {
|
||||
Tag string `xml:"tag"`
|
||||
}
|
||||
|
||||
type xmlTargetPlat struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
}
|
||||
|
||||
// channelFromTag maps a release tag name to a Joomla update channel.
|
||||
func channelFromTag(tagName string, isPrerelease bool) string {
|
||||
lower := strings.ToLower(tagName)
|
||||
switch {
|
||||
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
|
||||
return "dev"
|
||||
case strings.Contains(lower, "-alpha") || strings.Contains(lower, "alpha"):
|
||||
return "alpha"
|
||||
case strings.Contains(lower, "-beta") || strings.Contains(lower, "beta"):
|
||||
return "beta"
|
||||
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
|
||||
return "rc"
|
||||
case isPrerelease:
|
||||
return "rc"
|
||||
default:
|
||||
return "stable"
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
|
||||
// It returns the raw XML bytes. The element, maintainer, and target platform
|
||||
// are derived from the repo name and owner.
|
||||
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository) ([]byte, error) {
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IncludeDrafts: false,
|
||||
IncludeTags: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetReleasesByRepoID: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := setting.AppURL
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
element := strings.ToLower(repo.Name)
|
||||
|
||||
// Track best (latest) release per channel to emit one entry per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
var updates xmlUpdates
|
||||
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Load attachments for download URLs.
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the first .zip attachment as the download URL.
|
||||
var downloadURL string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fall back to the release tag archive if no zip attachment.
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
suffix := channelSuffix(ch)
|
||||
if suffix != "" {
|
||||
version = version + suffix
|
||||
}
|
||||
|
||||
u := xmlUpdate{
|
||||
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
||||
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
|
||||
Element: element,
|
||||
Type: "component",
|
||||
Client: "site",
|
||||
Version: version,
|
||||
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
||||
InfoURL: xmlInfoURL{
|
||||
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
||||
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
||||
},
|
||||
Downloads: xmlDownloads{
|
||||
DownloadURL: []xmlDownloadURL{
|
||||
{Type: "full", Format: "zip", URL: downloadURL},
|
||||
},
|
||||
},
|
||||
Tags: xmlTags{Tag: ch},
|
||||
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
|
||||
Maintainer: repo.Owner.Name,
|
||||
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
|
||||
TargetPlatform: xmlTargetPlat{
|
||||
Name: "joomla",
|
||||
Version: ".*",
|
||||
},
|
||||
}
|
||||
|
||||
updates.Updates = append(updates.Updates, u)
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(updates, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
|
||||
// extractVersion strips common tag prefixes (v, release-, etc.) to get the version.
|
||||
func extractVersion(tagName string) string {
|
||||
v := tagName
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
v = strings.TrimPrefix(v, "release-")
|
||||
v = strings.TrimPrefix(v, "release/")
|
||||
// Strip channel suffixes to get base version.
|
||||
for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
|
||||
if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
|
||||
v = v[:idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// channelSuffix returns the version suffix for a channel.
|
||||
func channelSuffix(channel string) string {
|
||||
switch channel {
|
||||
case "dev":
|
||||
return "-dev"
|
||||
case "alpha":
|
||||
return "-alpha"
|
||||
case "beta":
|
||||
return "-beta"
|
||||
case "rc":
|
||||
return "-rc"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
+31
-31
@@ -1,23 +1,23 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 05.02.00
|
||||
VERSION: 05.05.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
<update>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea dev build.</description>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea dev build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.02.00-dev</version>
|
||||
<version>05.05.00-dev</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.02.00-dev.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.05.00-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>a3ca6159a3ff878150906852dfa994eabe58f9e62fb5b224dea2a3593013bc8b</sha256>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<tags><tag>dev</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -25,18 +25,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea alpha build.</description>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea alpha build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.02.00-alpha</version>
|
||||
<version>05.05.00-alpha</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.02.00-alpha.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.05.00-alpha.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>a3ca6159a3ff878150906852dfa994eabe58f9e62fb5b224dea2a3593013bc8b</sha256>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -44,18 +44,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea beta build.</description>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea beta build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.02.00-beta</version>
|
||||
<version>05.05.00-beta</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.02.00-beta.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.05.00-beta.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>a3ca6159a3ff878150906852dfa994eabe58f9e62fb5b224dea2a3593013bc8b</sha256>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<tags><tag>beta</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -63,18 +63,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea rc build.</description>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea rc build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.02.00-rc</version>
|
||||
<version>05.05.00-rc</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.02.00-rc.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.05.00-rc.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>a3ca6159a3ff878150906852dfa994eabe58f9e62fb5b224dea2a3593013bc8b</sha256>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -82,18 +82,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea stable build.</description>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea stable build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.02.00</version>
|
||||
<version>05.05.00</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title='Application - MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.02.00.zip</downloadurl>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.05.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>a3ca6159a3ff878150906852dfa994eabe58f9e62fb5b224dea2a3593013bc8b</sha256>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user