1b9b82d59a
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 24s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
231 lines
7.6 KiB
Go
231 lines
7.6 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package updateserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html"
|
|
"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"
|
|
)
|
|
|
|
// WordPressPluginInfo is the JSON response format compatible with the
|
|
// YahnisElsts plugin-update-checker (PUC) library — the standard for
|
|
// commercial WordPress plugin self-hosted updates.
|
|
type WordPressPluginInfo struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Version string `json:"version"`
|
|
DownloadURL string `json:"download_url,omitempty"`
|
|
Homepage string `json:"homepage,omitempty"`
|
|
Requires string `json:"requires,omitempty"`
|
|
Tested string `json:"tested,omitempty"`
|
|
RequiresPHP string `json:"requires_php,omitempty"`
|
|
Author string `json:"author,omitempty"`
|
|
AuthorHomepage string `json:"author_homepage,omitempty"`
|
|
LastUpdated string `json:"last_updated,omitempty"`
|
|
UpgradeNotice string `json:"upgrade_notice,omitempty"`
|
|
Sections map[string]string `json:"sections,omitempty"`
|
|
Icons map[string]string `json:"icons,omitempty"`
|
|
Banners map[string]string `json:"banners,omitempty"`
|
|
}
|
|
|
|
// GenerateWordPressJSON builds a PUC-compatible JSON response for the latest
|
|
// stable release. The license key (if provided) is embedded in the download URL.
|
|
func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*WordPressPluginInfo, 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)
|
|
|
|
// Load extension metadata.
|
|
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
|
|
|
slug := strings.ToLower(repo.Name)
|
|
displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name)
|
|
maintainer := repo.Owner.Name
|
|
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
|
homepage := repoLink
|
|
requiresPHP := ""
|
|
if cfg != nil {
|
|
if cfg.ExtensionName != "" {
|
|
slug = cfg.ExtensionName
|
|
}
|
|
if cfg.DisplayName != "" {
|
|
displayName = cfg.DisplayName
|
|
}
|
|
if cfg.Maintainer != "" {
|
|
maintainer = cfg.Maintainer
|
|
}
|
|
if cfg.MaintainerURL != "" {
|
|
maintainerURL = cfg.MaintainerURL
|
|
}
|
|
if cfg.SupportURL != "" {
|
|
homepage = cfg.SupportURL
|
|
} else if cfg.InfoURL != "" {
|
|
homepage = cfg.InfoURL
|
|
}
|
|
if cfg.PHPMinimum != "" {
|
|
requiresPHP = cfg.PHPMinimum
|
|
}
|
|
}
|
|
|
|
// Resolve streams and find the latest stable release.
|
|
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
|
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 &WordPressPluginInfo{
|
|
Name: displayName,
|
|
Slug: slug,
|
|
Version: "0.0.0",
|
|
}, nil
|
|
}
|
|
|
|
// Load attachments.
|
|
if err := latestStable.LoadAttributes(ctx); err != nil {
|
|
return nil, fmt.Errorf("LoadAttributes: %w", err)
|
|
}
|
|
|
|
// Find zip download URL.
|
|
var downloadURL 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)
|
|
}
|
|
|
|
// Append license key to download URL if provided.
|
|
if licenseKey != "" {
|
|
if strings.Contains(downloadURL, "?") {
|
|
downloadURL += "&dlid=" + licenseKey
|
|
} else {
|
|
downloadURL += "?dlid=" + licenseKey
|
|
}
|
|
}
|
|
|
|
version := extractVersion(latestStable.TagName)
|
|
if version == "" || isStreamName(latestStable.TagName, streams) {
|
|
version = extractVersion(latestStable.Title)
|
|
}
|
|
if version == "" {
|
|
version = latestStable.TagName
|
|
}
|
|
lastUpdated := time.Unix(int64(latestStable.CreatedUnix), 0).Format("2006-01-02 3:04pm MST")
|
|
|
|
// Build sections from release notes.
|
|
sections := map[string]string{}
|
|
if latestStable.Note != "" {
|
|
sections["changelog"] = buildWordPressChangelog(ctx, releases, streams)
|
|
}
|
|
if cfg != nil && cfg.Description != "" {
|
|
sections["description"] = "<p>" + html.EscapeString(cfg.Description) + "</p>"
|
|
}
|
|
|
|
// Build icon/banner URLs from repo assets.
|
|
icons := buildAssetURLs(repoLink, repo.DefaultBranch, "icon", []string{"128x128", "256x256"})
|
|
banners := buildBannerURLs(repoLink, repo.DefaultBranch)
|
|
|
|
return &WordPressPluginInfo{
|
|
Name: displayName,
|
|
Slug: slug,
|
|
Version: version,
|
|
DownloadURL: downloadURL,
|
|
Homepage: homepage,
|
|
RequiresPHP: requiresPHP,
|
|
Author: maintainer,
|
|
AuthorHomepage: maintainerURL,
|
|
LastUpdated: lastUpdated,
|
|
Sections: sections,
|
|
Icons: icons,
|
|
Banners: banners,
|
|
}, nil
|
|
}
|
|
|
|
// buildWordPressChangelog builds an HTML changelog from multiple releases.
|
|
func buildWordPressChangelog(ctx context.Context, releases []*repo_model.Release, streams []licenses.StreamDef) string {
|
|
var b strings.Builder
|
|
count := 0
|
|
for _, rel := range releases {
|
|
if rel.IsDraft || rel.IsTag || rel.Note == "" {
|
|
continue
|
|
}
|
|
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
|
if ch != "stable" {
|
|
continue
|
|
}
|
|
version := extractVersion(rel.TagName)
|
|
date := time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02")
|
|
b.WriteString(fmt.Sprintf("<h4>%s - %s</h4>\n", version, date))
|
|
|
|
// Convert markdown list items to HTML list.
|
|
lines := strings.Split(rel.Note, "\n")
|
|
b.WriteString("<ul>\n")
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") {
|
|
b.WriteString(fmt.Sprintf("<li>%s</li>\n", html.EscapeString(strings.TrimLeft(trimmed[2:], " "))))
|
|
}
|
|
}
|
|
b.WriteString("</ul>\n")
|
|
|
|
count++
|
|
if count >= 10 {
|
|
break // limit to last 10 releases
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// buildAssetURLs generates icon URLs from conventional paths in the repo.
|
|
func buildAssetURLs(repoLink, branch, prefix string, sizes []string) map[string]string {
|
|
icons := make(map[string]string)
|
|
for i, size := range sizes {
|
|
key := fmt.Sprintf("%dx", i+1)
|
|
icons[key] = fmt.Sprintf("%s/raw/branch/%s/assets/%s-%s.png", repoLink, branch, prefix, size)
|
|
}
|
|
return icons
|
|
}
|
|
|
|
// buildBannerURLs generates banner URLs from conventional paths.
|
|
func buildBannerURLs(repoLink, branch string) map[string]string {
|
|
return map[string]string{
|
|
"low": fmt.Sprintf("%s/raw/branch/%s/assets/banner-772x250.png", repoLink, branch),
|
|
"high": fmt.Sprintf("%s/raw/branch/%s/assets/banner-1544x500.png", repoLink, branch),
|
|
}
|
|
}
|