feat(updates): Composer feed (#354), hide Actions/Licenses tabs for guests #417
@@ -211,3 +211,41 @@ func ServeWordPressJSON(ctx *context.Context) {
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(jsonData)
|
||||
}
|
||||
|
||||
// ServeComposerJSON generates and serves a Composer packages.json feed.
|
||||
func ServeComposerJSON(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "composer" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`{"packages":{}}`))
|
||||
return
|
||||
}
|
||||
|
||||
licenseKey := ctx.FormString("key")
|
||||
if licenseKey == "" {
|
||||
licenseKey = ctx.FormString("dlid")
|
||||
}
|
||||
|
||||
data, err := updateserver.GenerateComposerJSON(ctx, ctx.Repo.Repository, licenseKey)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateComposerJSON", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
ctx.ServerError("json.Marshal", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(jsonData)
|
||||
}
|
||||
|
||||
@@ -1519,6 +1519,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/updates.xml", repo.ServeUpdatesXML)
|
||||
m.Get("/updates/dolibarr.json", repo.ServeDolibarrJSON)
|
||||
m.Get("/updates/wordpress.json", repo.ServeWordPressJSON)
|
||||
m.Get("/updates/packages.json", repo.ServeComposerJSON)
|
||||
m.Get("/changelog.xml", repo.ServeChangelogXML)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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)
|
||||
|
||||
// Composer package name: vendor/package
|
||||
packageName := fmt.Sprintf("%s/%s", strings.ToLower(repo.Owner.Name), strings.ToLower(repo.Name))
|
||||
if cfg != nil && cfg.ExtensionName != "" {
|
||||
packageName = cfg.ExtensionName
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
description := ""
|
||||
if cfg != nil && cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
|
||||
phpMin := ""
|
||||
if cfg != nil && cfg.PHPMinimum != "" {
|
||||
phpMin = ">=" + cfg.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
|
||||
}
|
||||
@@ -113,7 +113,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}}
|
||||
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo) .IsSigned}}
|
||||
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions">
|
||||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}
|
||||
{{if .Repository.NumOpenActionRuns}}
|
||||
@@ -128,7 +128,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .LicensingEnabled}}
|
||||
{{if and .LicensingEnabled .IsSigned}}
|
||||
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
{{if .NumLicensePackages}}
|
||||
|
||||
Reference in New Issue
Block a user