feat(updates): extension metadata settings for update feed generation
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 7s
PR RC Release / Build RC Release (pull_request) Failing after 27s

- Add configurable fields: element name, display name, description,
  extension type, maintainer, maintainer URL, info URL, target version,
  PHP minimum
- Add platform dropdown: joomla, dolibarr, wordpress, prestashop,
  drupal, composer, both
- Update Joomla XML generator to use metadata from config (falls back
  to repo-derived values when not set)
- Add GetEffectiveConfig() for resolving repo → org → nil config chain
- Add locale keys for all new settings fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-31 10:58:45 -05:00
parent e0c69d792c
commit d30e7d7a5a
5 changed files with 185 additions and 13 deletions
+24 -1
View File
@@ -24,9 +24,19 @@ type UpdateStreamConfig struct {
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` // 0 = org-level default
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, custom
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both, wordpress, prestashop, drupal
LicensingEnabled bool `xorm:"NOT NULL DEFAULT false"` // master toggle for licensing system
RequireKey bool `xorm:"NOT NULL DEFAULT false"` // require license key for update feed
// Extension metadata — used in update feed generation.
ExtensionName string `xorm:"TEXT"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
DisplayName string `xorm:"TEXT"` // human-readable name (e.g. "Package - MokoWaaS")
Description string `xorm:"TEXT"` // short description for update feeds
ExtensionType string `xorm:"VARCHAR(50)"` // component, module, plugin, package, template, library
Maintainer string `xorm:"TEXT"` // maintainer/author name
MaintainerURL string `xorm:"TEXT"` // maintainer website
InfoURL string `xorm:"TEXT"` // extension info/product page URL
TargetVersion string `xorm:"TEXT"` // target platform version regex (e.g. "(5|6)\..*")
PHPMinimum string `xorm:"VARCHAR(20)"` // minimum PHP version (e.g. "8.1")
// CustomStreams is a JSON array of stream definitions.
// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
CustomStreams string `xorm:"TEXT"`
@@ -121,6 +131,19 @@ func GetEffectiveStreams(ctx context.Context, ownerID, repoID int64) []StreamDef
return DefaultJoomlaStreams()
}
// GetEffectiveConfig returns the full config for a repo: repo override → org default.
func GetEffectiveConfig(ctx context.Context, ownerID, repoID int64) *UpdateStreamConfig {
repoCfg, err := GetRepoConfig(ctx, repoID)
if err == nil && repoCfg != nil {
return repoCfg
}
orgCfg, err := GetOrgConfig(ctx, ownerID)
if err == nil && orgCfg != nil {
return orgCfg
}
return nil
}
// SaveConfig creates or updates an update stream config.
func SaveConfig(ctx context.Context, cfg *UpdateStreamConfig) error {
existing := new(UpdateStreamConfig)
+15
View File
@@ -2837,6 +2837,21 @@
"org.settings.enable_licensing_help": "Show the Licenses page in the org menu and enable license key management. Individual repos can also enable licensing independently.",
"org.settings.require_key": "Require license key for all update feeds",
"org.settings.require_key_help": "Update feeds return empty results unless a valid key is provided. Joomla clients will see a Download Key field. Individual repos can override this.",
"org.settings.extension_metadata": "Extension Metadata",
"org.settings.extension_metadata_desc": "Configure how this extension appears in update feeds. These fields are used when generating updates.xml, JSON feeds, and package metadata.",
"org.settings.update_platform": "Update Feed Format",
"org.settings.extension_name": "Element Name",
"org.settings.extension_name_help": "The unique extension identifier as registered in the CMS (e.g. pkg_mokowaas, com_akeebabackup).",
"org.settings.display_name": "Display Name",
"org.settings.display_name_help": "Human-readable name shown in the CMS update manager.",
"org.settings.extension_type": "Extension Type",
"org.settings.target_version": "Target Platform Version",
"org.settings.target_version_help": "Regex pattern for compatible CMS versions (e.g. (5|6)\\..*). Leave empty for all versions.",
"org.settings.maintainer": "Maintainer",
"org.settings.maintainer_url": "Maintainer URL",
"org.settings.info_url": "Info/Product URL",
"org.settings.info_url_help": "Link to the extension's product or documentation page.",
"org.settings.php_minimum": "Minimum PHP Version",
"org.settings.update_streams_heading": "Update Streams",
"org.settings.update_streams_desc": "Configure the default update streams for all repositories. Release tags are matched to streams by their suffix. Repos can override with per-repo settings.",
"org.settings.stream_mode": "Stream Mode",
+10
View File
@@ -40,9 +40,19 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
OwnerID: orgID,
RepoID: 0,
StreamMode: ctx.FormString("stream_mode"),
Platform: ctx.FormString("platform"),
CustomStreams: ctx.FormString("custom_streams"),
LicensingEnabled: ctx.FormString("licensing_enabled") == "on",
RequireKey: ctx.FormString("require_key") == "on",
ExtensionName: ctx.FormString("extension_name"),
DisplayName: ctx.FormString("display_name"),
Description: ctx.FormString("feed_description"),
ExtensionType: ctx.FormString("extension_type"),
Maintainer: ctx.FormString("maintainer"),
MaintainerURL: ctx.FormString("maintainer_url"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
}
if cfg.StreamMode == "" {
+56 -11
View File
@@ -125,8 +125,8 @@ func NormalizeChannel(ch string) string {
}
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
// It returns the raw XML bytes. The element, maintainer, and target platform
// are derived from the repo name and owner.
// It returns the raw XML bytes. Extension metadata is read from the update stream config;
// falls back to repo name/owner when not configured.
// allowedChannels optionally restricts output to specific channels (nil = all).
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, requireKey bool, allowedChannels ...string) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
@@ -149,7 +149,41 @@ 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).
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
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 := repo.Owner.Name
if cfg != nil && cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
if cfg != nil && cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
targetVersion := ".*"
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)
@@ -215,30 +249,41 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
version = version + suffix
}
desc := feedDescription
if desc == "" {
desc = fmt.Sprintf("%s %s build.", displayName, ch)
}
infoURL := fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName)
if cfg != nil && cfg.InfoURL != "" {
infoURL = cfg.InfoURL
}
u := xmlUpdate{
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
Name: displayName,
Description: desc,
Element: element,
Type: "component",
Type: extType,
Client: "site",
Version: version,
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
InfoURL: xmlInfoURL{
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
Title: displayName,
URL: infoURL,
},
Downloads: xmlDownloads{
DownloadURL: []xmlDownloadURL{
{Type: "full", Format: "zip", URL: downloadURL},
},
},
Tags: xmlTags{Tag: ch},
Tags: xmlTags{Tag: ch},
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
Maintainer: repo.Owner.Name,
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
Maintainer: maintainer,
MaintainerURL: maintainerURL,
PHPMinimum: phpMinimum,
TargetPlatform: xmlTargetPlat{
Name: "joomla",
Version: ".*",
Version: targetVersion,
},
}
+80 -1
View File
@@ -29,7 +29,86 @@
<div class="ui divider"></div>
{{/* Section 2: Update Streams */}}
{{/* Section 2: Extension Metadata */}}
<h5>{{svg "octicon-package" 14}} {{ctx.Locale.Tr "org.settings.extension_metadata"}}</h5>
<p>{{ctx.Locale.Tr "org.settings.extension_metadata_desc"}}</p>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.update_platform"}}</label>
<select name="platform" class="ui dropdown">
<option value="joomla" {{if or (eq .StreamConfig.Platform "") (eq .StreamConfig.Platform "joomla")}}selected{{end}}>Joomla</option>
<option value="dolibarr" {{if eq .StreamConfig.Platform "dolibarr"}}selected{{end}}>Dolibarr</option>
<option value="wordpress" {{if eq .StreamConfig.Platform "wordpress"}}selected{{end}}>WordPress</option>
<option value="prestashop" {{if eq .StreamConfig.Platform "prestashop"}}selected{{end}}>PrestaShop</option>
<option value="drupal" {{if eq .StreamConfig.Platform "drupal"}}selected{{end}}>Drupal</option>
<option value="composer" {{if eq .StreamConfig.Platform "composer"}}selected{{end}}>Composer (PHP)</option>
<option value="both" {{if eq .StreamConfig.Platform "both"}}selected{{end}}>Joomla + Dolibarr</option>
</select>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_name"}}</label>
<input name="extension_name" value="{{.StreamConfig.ExtensionName}}" placeholder="pkg_mokowaas">
<p class="help">{{ctx.Locale.Tr "org.settings.extension_name_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.display_name"}}</label>
<input name="display_name" value="{{.StreamConfig.DisplayName}}" placeholder="Package - MokoWaaS">
<p class="help">{{ctx.Locale.Tr "org.settings.display_name_help"}}</p>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="feed_description" value="{{.StreamConfig.Description}}" placeholder="MokoWaaS stable build.">
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_type"}}</label>
<select name="extension_type" class="ui dropdown">
<option value="package" {{if eq .StreamConfig.ExtensionType "package"}}selected{{end}}>Package</option>
<option value="component" {{if or (eq .StreamConfig.ExtensionType "") (eq .StreamConfig.ExtensionType "component")}}selected{{end}}>Component</option>
<option value="module" {{if eq .StreamConfig.ExtensionType "module"}}selected{{end}}>Module</option>
<option value="plugin" {{if eq .StreamConfig.ExtensionType "plugin"}}selected{{end}}>Plugin</option>
<option value="template" {{if eq .StreamConfig.ExtensionType "template"}}selected{{end}}>Template</option>
<option value="library" {{if eq .StreamConfig.ExtensionType "library"}}selected{{end}}>Library</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.target_version"}}</label>
<input name="target_version" value="{{.StreamConfig.TargetVersion}}" placeholder="(5|6)\..*">
<p class="help">{{ctx.Locale.Tr "org.settings.target_version_help"}}</p>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.maintainer"}}</label>
<input name="maintainer" value="{{.StreamConfig.Maintainer}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.maintainer_url"}}</label>
<input name="maintainer_url" value="{{.StreamConfig.MaintainerURL}}" placeholder="https://mokoconsulting.tech">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.info_url"}}</label>
<input name="info_url" value="{{.StreamConfig.InfoURL}}" placeholder="https://mokoconsulting.tech/products/mokowaas">
<p class="help">{{ctx.Locale.Tr "org.settings.info_url_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.php_minimum"}}</label>
<input name="php_minimum" value="{{.StreamConfig.PHPMinimum}}" placeholder="8.1">
</div>
</div>
<div class="ui divider"></div>
{{/* Section 3: Update Streams */}}
<h5>{{svg "octicon-rss" 14}} {{ctx.Locale.Tr "org.settings.update_streams_heading"}}</h5>
<p>{{ctx.Locale.Tr "org.settings.update_streams_desc"}}</p>