37322e4212
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 22s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Add release_stream_map table for explicitly assigning releases to update streams. When a mapping exists, it overrides automatic tag detection. When absent, falls back to tag name/suffix matching. New model: ReleaseStreamMap with SetReleaseStream, GetReleaseStream, ResolveReleaseStream (manual first, auto fallback). UI: stream selector dropdown on release create/edit page, shown when licensing is enabled. Options: auto-detect (default) or any configured stream (stable, release-candidate, beta, etc.). All three feed generators (Joomla, Dolibarr, WordPress) now use ResolveReleaseStream instead of MatchStreamFromTag. Migration v340 updated with release_stream_map table creation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
3.9 KiB
Go
138 lines
3.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"
|
|
)
|
|
|
|
// DolibarrUpdate represents a single module update entry in Dolibarr format.
|
|
type DolibarrUpdate struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Channel string `json:"channel"`
|
|
DownloadURL string `json:"url"`
|
|
ChangelogURL string `json:"changelog"`
|
|
ReleaseURL string `json:"release_url"`
|
|
Requires string `json:"requires,omitempty"`
|
|
Date string `json:"date"`
|
|
SHA256 string `json:"sha256,omitempty"`
|
|
}
|
|
|
|
// DolibarrUpdates holds the full update feed response.
|
|
type DolibarrUpdates struct {
|
|
Module string `json:"module"`
|
|
Updates []DolibarrUpdate `json:"updates"`
|
|
}
|
|
|
|
// GenerateDolibarrJSON builds a Dolibarr-compatible update feed from releases.
|
|
// allowedChannels optionally restricts output to specific channels (nil = all).
|
|
func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allowedChannels ...string) (*DolibarrUpdates, 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)
|
|
|
|
result := &DolibarrUpdates{
|
|
Module: repo.Name,
|
|
}
|
|
|
|
// Resolve effective streams.
|
|
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
|
|
|
// Track best release per channel.
|
|
bestByChannel := make(map[string]*repo_model.Release)
|
|
for _, rel := range releases {
|
|
if rel.IsDraft || rel.IsTag {
|
|
continue
|
|
}
|
|
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
|
existing, ok := bestByChannel[ch]
|
|
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
|
bestByChannel[ch] = rel
|
|
}
|
|
}
|
|
|
|
// Build allowed channel set for filtering.
|
|
channelAllowed := make(map[string]bool)
|
|
if len(allowedChannels) > 0 {
|
|
for _, c := range allowedChannels {
|
|
channelAllowed[NormalizeChannel(c)] = true
|
|
}
|
|
}
|
|
|
|
for _, stream := range streams {
|
|
ch := stream.Name
|
|
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
|
continue
|
|
}
|
|
rel, ok := bestByChannel[ch]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if err := rel.LoadAttributes(ctx); err != nil {
|
|
continue
|
|
}
|
|
|
|
var downloadURL string
|
|
for _, att := range rel.Attachments {
|
|
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
|
|
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)
|
|
}
|
|
|
|
version := extractVersion(rel.TagName)
|
|
if version == "" || isStreamName(rel.TagName, streams) {
|
|
version = extractVersion(rel.Title)
|
|
}
|
|
if version == "" {
|
|
version = rel.TagName
|
|
}
|
|
suffix := stream.Suffix
|
|
if suffix == "" {
|
|
suffix = channelSuffix(ch)
|
|
}
|
|
if suffix != "" {
|
|
version = version + suffix
|
|
}
|
|
|
|
result.Updates = append(result.Updates, DolibarrUpdate{
|
|
Name: repo.Name,
|
|
Version: version,
|
|
Channel: ch,
|
|
DownloadURL: downloadURL,
|
|
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
|
|
ReleaseURL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
|
Date: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|