1935889f6b
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Add resolveExtensionMetadata() with cascading priority: org-level repo-scoped custom fields → update_stream_config table → repo-derived defaults. All six feed generators (Joomla, WordPress, Composer, Drupal, PrestaShop, WHMCS) now use this unified resolver. Repos can be migrated to custom fields gradually since the config table remains as fallback. Ref #492 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
149 lines
4.1 KiB
Go
149 lines
4.1 KiB
Go
// 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)
|
|
meta := resolveExtensionMetadata(ctx, repo, cfg)
|
|
moduleName := meta.Element
|
|
displayName := meta.DisplayName
|
|
description := meta.Description
|
|
maintainer := repo.Owner.Name
|
|
if cfg != nil && cfg.Maintainer != "" {
|
|
maintainer = cfg.Maintainer
|
|
}
|
|
|
|
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
|
|
}
|