Files
Jonathan Miller 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
fix(build): remove unused time import in drupal.go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:10:11 -05:00

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
}