bd81616432
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 20s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
4.7 KiB
Go
165 lines
4.7 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)
|
|
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
|
|
}
|