// Copyright 2026 Moko Consulting // SPDX-License-Identifier: GPL-3.0-or-later package updateserver import ( "context" "fmt" "strings" "time" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" 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 := updateserver_model.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 := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) versions := make(map[string]ComposerVersion) for _, rel := range releases { if rel.IsDraft || rel.IsTag { continue } ch := updateserver_model.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 }