8e0388c9d8
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 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
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
- saveCustomFieldsFromForm: log GetCustomFieldsByOwner errors - resolveExtensionMetadata: log DB errors on custom field lookup - NewIssue/ViewIssue: log errors from GetCustomFieldsByOwner and GetCustomFieldValuesMap instead of blank-assigning - Composer: fix misleading comment about override source Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package updateserver
|
|
|
|
import (
|
|
"context"
|
|
"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"
|
|
)
|
|
|
|
// ComposerPackage represents the packages.json response for Composer/Packagist.
|
|
type ComposerPackage struct {
|
|
Packages map[string]map[string]ComposerVersion `json:"packages"`
|
|
}
|
|
|
|
// ComposerVersion represents a single version entry.
|
|
type ComposerVersion struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Type string `json:"type,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Homepage string `json:"homepage,omitempty"`
|
|
License []string `json:"license,omitempty"`
|
|
Authors []ComposerAuthor `json:"authors,omitempty"`
|
|
Dist *ComposerDist `json:"dist,omitempty"`
|
|
Require map[string]string `json:"require,omitempty"`
|
|
Time string `json:"time,omitempty"`
|
|
}
|
|
|
|
// ComposerAuthor represents a package author.
|
|
type ComposerAuthor struct {
|
|
Name string `json:"name"`
|
|
Homepage string `json:"homepage,omitempty"`
|
|
}
|
|
|
|
// ComposerDist represents a distribution archive.
|
|
type ComposerDist struct {
|
|
URL string `json:"url"`
|
|
Type string `json:"type"`
|
|
Shasum string `json:"shasum,omitempty"`
|
|
Reference string `json:"reference,omitempty"`
|
|
}
|
|
|
|
// GenerateComposerJSON builds a Composer packages.json from repo releases.
|
|
func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*ComposerPackage, 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)
|
|
|
|
// Composer package name: vendor/package (override with resolved extension name if set)
|
|
packageName := fmt.Sprintf("%s/%s", strings.ToLower(repo.Owner.Name), strings.ToLower(repo.Name))
|
|
if meta.Element != strings.ToLower(repo.Name) {
|
|
packageName = meta.Element
|
|
}
|
|
|
|
description := meta.Description
|
|
maintainer := repo.Owner.Name
|
|
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
|
if cfg != nil && cfg.Maintainer != "" {
|
|
maintainer = cfg.Maintainer
|
|
}
|
|
if cfg != nil && cfg.MaintainerURL != "" {
|
|
maintainerURL = cfg.MaintainerURL
|
|
}
|
|
|
|
phpMin := ""
|
|
if meta.PHPMinimum != "" {
|
|
phpMin = ">=" + meta.PHPMinimum
|
|
}
|
|
|
|
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
|
versions := make(map[string]ComposerVersion)
|
|
|
|
for _, rel := range releases {
|
|
if rel.IsDraft || rel.IsTag {
|
|
continue
|
|
}
|
|
|
|
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
|
if ch != "stable" {
|
|
continue // Composer only serves stable versions
|
|
}
|
|
|
|
version := extractVersion(rel.TagName)
|
|
if isStreamName(version, streams) {
|
|
version = extractVersion(rel.Title)
|
|
}
|
|
if version == "" {
|
|
continue
|
|
}
|
|
|
|
if err := rel.LoadAttributes(ctx); err != nil {
|
|
continue
|
|
}
|
|
|
|
var downloadURL string
|
|
var 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)
|
|
}
|
|
if licenseKey != "" {
|
|
downloadURL += "?dlid=" + licenseKey
|
|
}
|
|
|
|
// Look for SHA256 sidecar
|
|
for _, att := range rel.Attachments {
|
|
if strings.HasSuffix(att.Name, ".sha256") {
|
|
sha256Hash = readSHA256FromSidecar(ctx, att)
|
|
break
|
|
}
|
|
}
|
|
|
|
require := make(map[string]string)
|
|
if phpMin != "" {
|
|
require["php"] = phpMin
|
|
}
|
|
|
|
v := ComposerVersion{
|
|
Name: packageName,
|
|
Version: version,
|
|
Description: description,
|
|
Homepage: repoLink,
|
|
Authors: []ComposerAuthor{
|
|
{Name: maintainer, Homepage: maintainerURL},
|
|
},
|
|
Dist: &ComposerDist{
|
|
URL: downloadURL,
|
|
Type: "zip",
|
|
Shasum: sha256Hash,
|
|
Reference: rel.TagName,
|
|
},
|
|
Require: require,
|
|
Time: time.Unix(int64(rel.CreatedUnix), 0).Format(time.RFC3339),
|
|
}
|
|
|
|
versions[version] = v
|
|
}
|
|
|
|
return &ComposerPackage{
|
|
Packages: map[string]map[string]ComposerVersion{
|
|
packageName: versions,
|
|
},
|
|
}, nil
|
|
}
|