@@ -249,3 +249,95 @@ func ServeComposerJSON(ctx *context.Context) {
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(jsonData)
|
||||
}
|
||||
|
||||
// ServePrestaShopXML generates and serves a PrestaShop module update XML.
|
||||
func ServePrestaShopXML(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "prestashop" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
allowedChannels, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><modules></modules>`))
|
||||
return
|
||||
}
|
||||
|
||||
xmlData, err := updateserver.GeneratePrestaShopXML(ctx, ctx.Repo.Repository, allowedChannels...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GeneratePrestaShopXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
|
||||
// ServeDrupalXML generates and serves a Drupal update status XML.
|
||||
func ServeDrupalXML(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "drupal" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
allowedChannels, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><project></project>`))
|
||||
return
|
||||
}
|
||||
|
||||
xmlData, err := updateserver.GenerateDrupalXML(ctx, ctx.Repo.Repository, allowedChannels...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateDrupalXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
|
||||
// ServeWHMCSJSON generates and serves a WHMCS module update JSON.
|
||||
func ServeWHMCSJSON(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "whmcs" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`{"name":"","version":"0.0.0"}`))
|
||||
return
|
||||
}
|
||||
|
||||
licenseKey := ctx.FormString("key")
|
||||
if licenseKey == "" {
|
||||
licenseKey = ctx.FormString("dlid")
|
||||
}
|
||||
|
||||
data, err := updateserver.GenerateWHMCSJSON(ctx, ctx.Repo.Repository, licenseKey)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateWHMCSJSON", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
ctx.ServerError("json.Marshal", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(jsonData)
|
||||
}
|
||||
|
||||
@@ -1520,6 +1520,9 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/updates/dolibarr.json", repo.ServeDolibarrJSON)
|
||||
m.Get("/updates/wordpress.json", repo.ServeWordPressJSON)
|
||||
m.Get("/updates/packages.json", repo.ServeComposerJSON)
|
||||
m.Get("/updates/prestashop.xml", repo.ServePrestaShopXML)
|
||||
m.Get("/updates/drupal.xml", repo.ServeDrupalXML)
|
||||
m.Get("/updates/whmcs.json", repo.ServeWHMCSJSON)
|
||||
m.Get("/changelog.xml", repo.ServeChangelogXML)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// Drupal update status XML structures.
|
||||
// See: https://www.drupal.org/docs/drupal-apis/update-status-module/update-status-xml-format
|
||||
|
||||
type drupalProject struct {
|
||||
XMLName xml.Name `xml:"project"`
|
||||
Title string `xml:"title"`
|
||||
ShortName string `xml:"short_name"`
|
||||
APIVersion string `xml:"api_version"`
|
||||
RecommendedMaj string `xml:"recommended_major"`
|
||||
DefaultMajor string `xml:"default_major"`
|
||||
ProjectStatus string `xml:"project_status"`
|
||||
Link string `xml:"link"`
|
||||
Releases drupalReleases `xml:"releases"`
|
||||
}
|
||||
|
||||
type drupalReleases struct {
|
||||
Releases []drupalRelease `xml:"release"`
|
||||
}
|
||||
|
||||
type drupalRelease struct {
|
||||
Name string `xml:"name"`
|
||||
Version string `xml:"version"`
|
||||
Tag string `xml:"tag"`
|
||||
Status string `xml:"status"`
|
||||
ReleaseLink string `xml:"release_link"`
|
||||
DownloadURL string `xml:"download_link"`
|
||||
Date string `xml:"date"`
|
||||
FileHash string `xml:"mdhash,omitempty"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateDrupalXML builds a Drupal update status-compatible XML feed.
|
||||
func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowedChannels ...string) ([]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("FindReleases: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := strings.TrimSuffix(setting.AppURL, "/")
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
shortName := strings.ToLower(repo.Name)
|
||||
title := repo.Name
|
||||
if cfg != nil {
|
||||
if cfg.ExtensionName != "" {
|
||||
shortName = cfg.ExtensionName
|
||||
}
|
||||
if cfg.DisplayName != "" {
|
||||
title = cfg.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
channelAllowed := make(map[string]bool)
|
||||
if len(allowedChannels) > 0 {
|
||||
for _, c := range allowedChannels {
|
||||
channelAllowed[NormalizeChannel(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
project := drupalProject{
|
||||
Title: title,
|
||||
ShortName: shortName,
|
||||
APIVersion: "7.x", // default API version
|
||||
RecommendedMaj: "1",
|
||||
DefaultMajor: "1",
|
||||
ProjectStatus: "published",
|
||||
Link: repoLink,
|
||||
}
|
||||
|
||||
if cfg != nil && cfg.TargetVersion != "" {
|
||||
project.APIVersion = cfg.TargetVersion
|
||||
}
|
||||
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var downloadURL, sha256Hash string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
||||
}
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
if isStreamName(version, streams) || version == "" {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = rel.TagName
|
||||
}
|
||||
|
||||
status := "published"
|
||||
if rel.IsPrerelease {
|
||||
status = "insecure" // Drupal uses this for non-stable
|
||||
}
|
||||
|
||||
project.Releases.Releases = append(project.Releases.Releases, drupalRelease{
|
||||
Name: fmt.Sprintf("%s %s", shortName, version),
|
||||
Version: version,
|
||||
Tag: rel.TagName,
|
||||
Status: status,
|
||||
ReleaseLink: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
||||
DownloadURL: downloadURL,
|
||||
Date: fmt.Sprintf("%d", rel.CreatedUnix),
|
||||
SHA256: sha256Hash,
|
||||
})
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(project, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// PrestaShop module update XML structures.
|
||||
|
||||
type psUpdates struct {
|
||||
XMLName xml.Name `xml:"modules"`
|
||||
Modules []psModule `xml:"module"`
|
||||
}
|
||||
|
||||
type psModule struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
DisplayName string `xml:"displayName"`
|
||||
Description string `xml:"description,omitempty"`
|
||||
Author string `xml:"author"`
|
||||
Tab string `xml:"tab,omitempty"`
|
||||
Download string `xml:"download"`
|
||||
Date string `xml:"date"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// GeneratePrestaShopXML builds a PrestaShop-compatible module update XML.
|
||||
func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, allowedChannels ...string) ([]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("FindReleases: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := strings.TrimSuffix(setting.AppURL, "/")
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
moduleName := strings.ToLower(repo.Name)
|
||||
displayName := repo.Name
|
||||
maintainer := repo.Owner.Name
|
||||
description := ""
|
||||
if cfg != nil {
|
||||
if cfg.ExtensionName != "" {
|
||||
moduleName = cfg.ExtensionName
|
||||
}
|
||||
if cfg.DisplayName != "" {
|
||||
displayName = cfg.DisplayName
|
||||
}
|
||||
if cfg.Maintainer != "" {
|
||||
maintainer = cfg.Maintainer
|
||||
}
|
||||
if cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Channel filtering.
|
||||
channelAllowed := make(map[string]bool)
|
||||
if len(allowedChannels) > 0 {
|
||||
for _, c := range allowedChannels {
|
||||
channelAllowed[NormalizeChannel(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Track best release per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
var result psUpdates
|
||||
for _, stream := range streams {
|
||||
ch := stream.Name
|
||||
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
||||
continue
|
||||
}
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok || ch != "stable" {
|
||||
continue // PrestaShop typically only serves stable
|
||||
}
|
||||
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var downloadURL, sha256Hash string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
||||
}
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
if isStreamName(version, streams) || version == "" {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = rel.TagName
|
||||
}
|
||||
|
||||
result.Modules = append(result.Modules, psModule{
|
||||
Name: moduleName,
|
||||
Version: version,
|
||||
DisplayName: displayName,
|
||||
Description: description,
|
||||
Author: maintainer,
|
||||
Download: downloadURL,
|
||||
Date: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
||||
SHA256: sha256Hash,
|
||||
})
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// WHMCS module update JSON structures.
|
||||
// WHMCS marketplace modules check for updates via a simple JSON endpoint.
|
||||
|
||||
type WHMCSUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
Changelog string `json:"changelog,omitempty"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Author string `json:"author,omitempty"`
|
||||
AuthorURL string `json:"author_url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateWHMCSJSON builds a WHMCS-compatible module update response.
|
||||
func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*WHMCSUpdate, 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("FindReleases: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := strings.TrimSuffix(setting.AppURL, "/")
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
displayName := repo.Name
|
||||
maintainer := repo.Owner.Name
|
||||
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
||||
description := ""
|
||||
if cfg != nil {
|
||||
if cfg.DisplayName != "" {
|
||||
displayName = cfg.DisplayName
|
||||
}
|
||||
if cfg.Maintainer != "" {
|
||||
maintainer = cfg.Maintainer
|
||||
}
|
||||
if cfg.MaintainerURL != "" {
|
||||
maintainerURL = cfg.MaintainerURL
|
||||
}
|
||||
if cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Find latest stable release.
|
||||
var latestStable *repo_model.Release
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if ch == "stable" {
|
||||
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
|
||||
latestStable = rel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestStable == nil {
|
||||
return &WHMCSUpdate{
|
||||
Name: displayName,
|
||||
Version: "0.0.0",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := latestStable.LoadAttributes(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadAttributes: %w", err)
|
||||
}
|
||||
|
||||
var downloadURL, sha256Hash string
|
||||
for _, att := range latestStable.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, latestStable.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, latestStable.TagName)
|
||||
}
|
||||
if licenseKey != "" {
|
||||
downloadURL += "?dlid=" + licenseKey
|
||||
}
|
||||
for _, att := range latestStable.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version := extractVersion(latestStable.TagName)
|
||||
if isStreamName(version, streams) || version == "" {
|
||||
version = extractVersion(latestStable.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = latestStable.TagName
|
||||
}
|
||||
|
||||
changelog := ""
|
||||
if latestStable.Note != "" {
|
||||
changelog = latestStable.Note
|
||||
}
|
||||
|
||||
return &WHMCSUpdate{
|
||||
Name: displayName,
|
||||
Version: version,
|
||||
Description: description,
|
||||
DownloadURL: downloadURL,
|
||||
Changelog: changelog,
|
||||
ReleaseDate: time.Unix(int64(latestStable.CreatedUnix), 0).Format("2006-01-02"),
|
||||
Author: maintainer,
|
||||
AuthorURL: maintainerURL,
|
||||
SHA256: sha256Hash,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user