From 1935889f6b71bc1706a0bdcc07da26d60e849c8a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 21:48:14 -0500 Subject: [PATCH] feat(updateserver): resolve extension metadata from custom fields with config fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add resolveExtensionMetadata() with cascading priority: org-level repo-scoped custom fields → update_stream_config table → repo-derived defaults. All six feed generators (Joomla, WordPress, Composer, Drupal, PrestaShop, WHMCS) now use this unified resolver. Repos can be migrated to custom fields gradually since the config table remains as fallback. Ref #492 Co-Authored-By: Claude Opus 4.6 (1M context) --- services/updateserver/composer.go | 13 +-- services/updateserver/drupal.go | 13 +-- services/updateserver/joomla.go | 137 ++++++++++++++++++++++------ services/updateserver/prestashop.go | 22 ++--- services/updateserver/whmcs.go | 11 +-- services/updateserver/wordpress.go | 28 +++--- 6 files changed, 136 insertions(+), 88 deletions(-) diff --git a/services/updateserver/composer.go b/services/updateserver/composer.go index 4353daa6d1..ad9326acfc 100644 --- a/services/updateserver/composer.go +++ b/services/updateserver/composer.go @@ -68,13 +68,15 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice 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 + // Composer package name: vendor/package (override with custom field "Extension Name") packageName := fmt.Sprintf("%s/%s", strings.ToLower(repo.Owner.Name), strings.ToLower(repo.Name)) - if cfg != nil && cfg.ExtensionName != "" { - packageName = cfg.ExtensionName + 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 != "" { @@ -84,11 +86,6 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice maintainerURL = cfg.MaintainerURL } - description := "" - if cfg != nil && cfg.Description != "" { - description = cfg.Description - } - phpMin := "" if cfg != nil && cfg.PHPMinimum != "" { phpMin = ">=" + cfg.PHPMinimum diff --git a/services/updateserver/drupal.go b/services/updateserver/drupal.go index 1c9495021c..8e5472d4d7 100644 --- a/services/updateserver/drupal.go +++ b/services/updateserver/drupal.go @@ -66,16 +66,9 @@ func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowed repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) - shortName := strings.ToLower(repo.Name) - title := repo.Name - if cfg != nil { - if cfg.ExtensionName != "" { - shortName = cfg.ExtensionName - } - if cfg.DisplayName != "" { - title = cfg.DisplayName - } - } + meta := resolveExtensionMetadata(ctx, repo, cfg) + shortName := meta.Element + title := meta.DisplayName streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) diff --git a/services/updateserver/joomla.go b/services/updateserver/joomla.go index 11139ddbc7..51319bc028 100644 --- a/services/updateserver/joomla.go +++ b/services/updateserver/joomla.go @@ -13,6 +13,7 @@ import ( "time" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" + issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage" @@ -160,9 +161,102 @@ func NormalizeChannel(ch string) string { } } +// extensionMetadata holds resolved metadata for feed generation. +// Fields are resolved with priority: custom field → config table → default. +type extensionMetadata struct { + Element string + DisplayName string + ExtType string + TargetVersion string + PHPMinimum string + Description string + SupportURL string +} + +// resolveExtensionMetadata loads extension metadata with cascading fallback: +// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults. +func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata { + m := extensionMetadata{ + Element: strings.ToLower(repo.Name), + DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name), + ExtType: "component", + TargetVersion: "(5|6)\\..*", + } + + // Apply config table values. + if cfg != nil { + if cfg.ExtensionName != "" { + m.Element = cfg.ExtensionName + } + if cfg.DisplayName != "" { + m.DisplayName = cfg.DisplayName + } + if cfg.ExtensionType != "" { + m.ExtType = cfg.ExtensionType + } + if cfg.TargetVersion != "" { + m.TargetVersion = cfg.TargetVersion + } + if cfg.PHPMinimum != "" { + m.PHPMinimum = cfg.PHPMinimum + } + if cfg.Description != "" { + m.Description = cfg.Description + } + if cfg.SupportURL != "" { + m.SupportURL = cfg.SupportURL + } + } + + // Override with custom field values (highest priority). + fields, err := issues_model.GetCustomFieldsByOwner(ctx, repo.OwnerID, issues_model.CustomFieldScopeRepo) + if err != nil || len(fields) == 0 { + return m + } + values, err := issues_model.GetCustomFieldValuesMap(ctx, repo.ID) + if err != nil || len(values) == 0 { + return m + } + + // Build name → value map from field definitions + values. + named := make(map[string]string, len(fields)) + for _, f := range fields { + if v, ok := values[f.ID]; ok && v != "" { + named[f.Name] = v + } + } + + if v := named["Extension Name"]; v != "" { + m.Element = v + } + if v := named["Display Name"]; v != "" { + m.DisplayName = v + } + if v := named["Extension Type"]; v != "" { + m.ExtType = v + } + if v := named["Target Version"]; v != "" { + m.TargetVersion = v + } + if v := named["PHP Minimum"]; v != "" { + m.PHPMinimum = v + } + if v := named["Support URL"]; v != "" { + m.SupportURL = v + } + if v := named["Download Gating"]; v != "" && cfg != nil { + cfg.DownloadGating = v + } + if v := named["Key Prefix"]; v != "" && cfg != nil { + cfg.KeyPrefix = v + } + + return m +} + // GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases. -// It returns the raw XML bytes. Extension metadata is read from the update stream config; -// falls back to repo name/owner when not configured. +// It returns the raw XML bytes. Extension metadata is resolved from custom fields first, +// then the update stream config, then repo-derived defaults. // allowedChannels optionally restricts output to specific channels (nil = all). func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, requireKey, stripDownloads bool, allowedChannels ...string) ([]byte, error) { releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ @@ -185,21 +279,18 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require } repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) - // Load extension metadata from config (falls back to repo-derived values). + // Load extension metadata with cascading fallback: + // custom fields → config table → repo-derived defaults. cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) + meta := resolveExtensionMetadata(ctx, repo, cfg) + + element := meta.Element + displayName := meta.DisplayName + extType := meta.ExtType + targetVersion := meta.TargetVersion + phpMinimum := meta.PHPMinimum + feedDescription := meta.Description - element := strings.ToLower(repo.Name) - if cfg != nil && cfg.ExtensionName != "" { - element = cfg.ExtensionName - } - displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name) - if cfg != nil && cfg.DisplayName != "" { - displayName = cfg.DisplayName - } - extType := "component" - if cfg != nil && cfg.ExtensionType != "" { - extType = cfg.ExtensionType - } // Maintainer and URL always come from the org profile. maintainer := repo.Owner.FullName if maintainer == "" { @@ -209,18 +300,6 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require if maintainerURL == "" { maintainerURL = fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name) } - targetVersion := "(5|6)\\..*" - if cfg != nil && cfg.TargetVersion != "" { - targetVersion = cfg.TargetVersion - } - phpMinimum := "" - if cfg != nil && cfg.PHPMinimum != "" { - phpMinimum = cfg.PHPMinimum - } - feedDescription := "" - if cfg != nil && cfg.Description != "" { - feedDescription = cfg.Description - } // Resolve effective streams (repo override → org default → Joomla default). streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) @@ -319,8 +398,8 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require // Info URL: use support_url (product page), fall back to releases page. infoURL := fmt.Sprintf("%s/releases", repoLink) - if cfg != nil && cfg.SupportURL != "" { - infoURL = cfg.SupportURL + if meta.SupportURL != "" { + infoURL = meta.SupportURL } // Joomla element: packages use client_id=0 in #__extensions, diff --git a/services/updateserver/prestashop.go b/services/updateserver/prestashop.go index 81442f756e..8795500c71 100644 --- a/services/updateserver/prestashop.go +++ b/services/updateserver/prestashop.go @@ -55,23 +55,13 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) - moduleName := strings.ToLower(repo.Name) - displayName := repo.Name + meta := resolveExtensionMetadata(ctx, repo, cfg) + moduleName := meta.Element + displayName := meta.DisplayName + description := meta.Description maintainer := repo.Owner.Name - description := "" - if cfg != nil { - if cfg.ExtensionName != "" { - moduleName = cfg.ExtensionName - } - if cfg.DisplayName != "" { - displayName = cfg.DisplayName - } - if cfg.Maintainer != "" { - maintainer = cfg.Maintainer - } - if cfg.Description != "" { - description = cfg.Description - } + if cfg != nil && cfg.Maintainer != "" { + maintainer = cfg.Maintainer } streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) diff --git a/services/updateserver/whmcs.go b/services/updateserver/whmcs.go index a3a95a50cb..da188162f9 100644 --- a/services/updateserver/whmcs.go +++ b/services/updateserver/whmcs.go @@ -50,23 +50,18 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) - displayName := repo.Name + meta := resolveExtensionMetadata(ctx, repo, cfg) + displayName := meta.DisplayName + description := meta.Description maintainer := repo.Owner.Name maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name) - description := "" if cfg != nil { - if cfg.DisplayName != "" { - displayName = cfg.DisplayName - } if cfg.Maintainer != "" { maintainer = cfg.Maintainer } if cfg.MaintainerURL != "" { maintainerURL = cfg.MaintainerURL } - if cfg.Description != "" { - description = cfg.Description - } } streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) diff --git a/services/updateserver/wordpress.go b/services/updateserver/wordpress.go index d17ef645d0..619ffa04ba 100644 --- a/services/updateserver/wordpress.go +++ b/services/updateserver/wordpress.go @@ -57,36 +57,30 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic baseURL := strings.TrimSuffix(setting.AppURL, "/") repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) - // Load extension metadata. + // Load extension metadata with cascading fallback: + // custom fields → config table → repo-derived defaults. cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) + meta := resolveExtensionMetadata(ctx, repo, cfg) - slug := strings.ToLower(repo.Name) - displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name) + slug := meta.Element + displayName := meta.DisplayName + requiresPHP := meta.PHPMinimum + homepage := repoLink + if meta.SupportURL != "" { + homepage = meta.SupportURL + } maintainer := repo.Owner.Name maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name) - homepage := repoLink - requiresPHP := "" if cfg != nil { - if cfg.ExtensionName != "" { - slug = cfg.ExtensionName - } - if cfg.DisplayName != "" { - displayName = cfg.DisplayName - } if cfg.Maintainer != "" { maintainer = cfg.Maintainer } if cfg.MaintainerURL != "" { maintainerURL = cfg.MaintainerURL } - if cfg.SupportURL != "" { - homepage = cfg.SupportURL - } else if cfg.InfoURL != "" { + if homepage == repoLink && cfg.InfoURL != "" { homepage = cfg.InfoURL } - if cfg.PHPMinimum != "" { - requiresPHP = cfg.PHPMinimum - } } // Resolve streams and find the latest stable release.