1935889f6b
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
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
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>
158 lines
4.6 KiB
Go
158 lines
4.6 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"
|
|
|
|
"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)
|
|
meta := resolveExtensionMetadata(ctx, repo, cfg)
|
|
shortName := meta.Element
|
|
title := meta.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
|
|
}
|