Files
Jonathan Miller 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
fix(custom-fields): log errors instead of silently discarding them
- 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>
2026-06-04 22:14:10 -05:00

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
}