feat(updateserver): adopt platform registry + feedless metadata platforms return N/A over default-joomla config [#367 #811] #830

Merged
jmiller merged 1 commits from feature/updateserver-adopt-registry into dev 2026-07-19 06:12:09 +00:00
2 changed files with 97 additions and 40 deletions
+68 -16
View File
@@ -3,7 +3,11 @@
package updateserver
import "strings"
import (
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
)
// Update-feed platform identifiers. These are the recognized values for the
// UpdateStreamConfig.Platform column and for the resolved "RepoUpdatePlatform"
@@ -29,39 +33,87 @@ const (
PlatformMCP = "mcp"
)
// feedlessPlatforms are platforms that never emit a legacy update feed.
// feedlessPlatforms are the statically-known platforms that never emit a legacy
// update feed. This is a back-compat fallback for the registry lookup below: the
// authoritative signal is the platform registry's feed_format == "none", which
// also covers "go" and any admin-configured feedless platform. See #811.
var feedlessPlatforms = map[string]struct{}{
PlatformNPM: {},
PlatformMCP: {},
}
// IsFeedlessPlatform reports whether the given platform must not serve any
// update feed (npm / mcp). These repositories are published to an external
// registry, so every Serve* handler returns NotFound for them. See #810.
// update feed. A platform is feedless when the platform registry gives it no
// wire format (feed_format == "none"). In the built-in registry that is npm /
// mcp / go (npm & mcp publish to the npm registry; go publishes via git-release)
// plus any admin-configured feedless platform. These repositories are
// distributed through an external channel, so every Serve* handler returns
// NotFound for them. See #810 / #811.
//
// Note: feed_format (NOT update_generator) is the authoritative signal. npm/mcp
// carry update_generator == "npm" (their build/publish generator), but their
// feed_format is "none" because the update server emits no feed for them.
//
// The registry is the single source of truth. The static feedlessPlatforms map
// is only consulted as a defensive fallback (e.g. for a key unknown to the
// registry) so a bare "npm"/"mcp" is always treated as feedless.
func IsFeedlessPlatform(platform string) bool {
_, ok := feedlessPlatforms[strings.ToLower(strings.TrimSpace(platform))]
key := strings.ToLower(strings.TrimSpace(platform))
if key == "" {
return false
}
// Registry-driven: feed_format == "none" means no legacy feed is emitted.
// This is what makes npm/mcp/go feedless from a single config source.
if def, ok := setting.GetPlatform(key); ok {
if strings.EqualFold(strings.TrimSpace(def.FeedFormat), "none") {
return true
}
// Known registry entry that DOES serve a feed (joomla, dolibarr, …).
return false
}
// Unknown to the registry: fall back to the static feedless set.
_, ok := feedlessPlatforms[key]
return ok
}
// ResolvePlatform determines the effective update-feed platform for a repo.
//
// - If the update-stream config sets an explicit platform, that wins.
// - Otherwise the platform is derived from the repository metadata platform
// (metadata.platform), so the two stores can no longer drift. See #811.
// - Feedless metadata values (npm / mcp) are propagated verbatim so the
// handlers can return NotFound instead of falling back to a Joomla feed.
// - Only when neither source is set do we fall back to the historical
// "joomla" default (used by legacy Joomla extension repos).
// Precedence (see #811 for why metadata can override the config row):
//
// - If the repo metadata platform (metadata.platform) is a registry-recognized
// FEEDLESS platform (feed_format == "none": npm / mcp / go, plus any
// admin-configured feedless platform), it ALWAYS wins. This is the #811 fix:
// the update_stream_config.Platform column is `NOT NULL DEFAULT 'joomla'`, so
// any licensing-enabled repo has a config row that resolves to "joomla" even
// when it is really an npm/mcp/go repo. Letting a feedless metadata platform
// win means those repos return NotFound instead of a bogus Joomla feed.
// - Otherwise, an explicit update-stream config platform wins (this preserves a
// genuine, deliberate override such as dolibarr/wordpress/etc.).
// - Otherwise the platform is derived from the repository metadata platform, so
// the two stores can no longer drift. See #811.
// - Only when neither source is set do we fall back to the historical "joomla"
// default (used by legacy Joomla extension repos).
//
// Joomla-preservation reasoning: a genuine Joomla repo has metadata.platform ==
// "joomla" (or empty), which is NOT feedless, so the config platform still wins
// and the Joomla feed is served exactly as before. The only behavior that
// changes is that a repo whose metadata says npm/mcp/go no longer serves a feed
// just because it carries a default-'joomla' config row.
//
// This is the single source of truth for the old "empty ⇒ joomla" fallback,
// which previously caused npm/mcp repos with no config row to emit a Joomla
// feed. See #810 and #811.
// which previously caused npm/mcp repos to emit a Joomla feed. See #810 and #811.
func ResolvePlatform(cfgPlatform, metadataPlatform string) string {
meta := strings.ToLower(strings.TrimSpace(metadataPlatform))
// #811: a recognized feedless metadata platform overrides the config row,
// including the default-'joomla' row every licensing-enabled repo carries.
if meta != "" && IsFeedlessPlatform(meta) {
return meta
}
if p := strings.ToLower(strings.TrimSpace(cfgPlatform)); p != "" {
return p
}
if p := strings.ToLower(strings.TrimSpace(metadataPlatform)); p != "" {
return p
if meta != "" {
return meta
}
return PlatformJoomla
}
+29 -24
View File
@@ -78,13 +78,14 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
// from the repository's releases.
func ServeUpdatesXML(ctx *context.Context) {
// Only serve the Joomla feed for joomla/both platforms. The platform is
// resolved upstream (config override → repo metadata → "joomla" default),
// so an unset or feedless (npm/mcp) platform never reaches the Joomla
// generator. Any other platform — including npm/mcp — returns NotFound.
// See #810 / #811.
// Registry-driven gating: serve the Joomla feed only for platforms whose
// registry update_generator is "joomla" (plus the "both" compound platform,
// which shares the Joomla + Dolibarr generators and has no standalone
// registry entry). The platform is resolved upstream (metadata feedless wins
// → config override → repo metadata → "joomla" default), so a feedless
// (npm/mcp/go) platform never reaches the Joomla generator. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformJoomla && platform != updateserver_model.PlatformBoth {
if !updateserver.PlatformServesGenerator(platform, "joomla") && platform != updateserver_model.PlatformBoth {
ctx.NotFound(nil)
return
}
@@ -117,10 +118,12 @@ func ServeUpdatesXML(ctx *context.Context) {
// from the repository's releases. Uses the same license key validation as the
// Joomla XML feed — all platforms share the same licensing system.
func ServeDolibarrJSON(ctx *context.Context) {
// Only serve for dolibarr/both. Feedless platforms (npm/mcp) and any other
// platform return NotFound rather than a feed. See #810 / #811.
// Registry-driven gating: serve the Dolibarr feed only for platforms whose
// registry update_generator is "dolibarr" (plus the "both" compound
// platform). Feedless platforms (npm/mcp/go) and any other platform return
// NotFound rather than a feed. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformDolibarr && platform != updateserver_model.PlatformBoth {
if !updateserver.PlatformServesGenerator(platform, "dolibarr") && platform != updateserver_model.PlatformBoth {
ctx.NotFound(nil)
return
}
@@ -153,10 +156,12 @@ func ServeDolibarrJSON(ctx *context.Context) {
// ServeWordPressJSON generates and serves a WordPress PUC-compatible update feed.
// Compatible with the YahnisElsts plugin-update-checker library.
func ServeWordPressJSON(ctx *context.Context) {
// Only serve for wordpress/both. Feedless platforms (npm/mcp) and any other
// platform return NotFound. See #810.
// Only serve for wordpress/both. Registry-recognized feedless platforms
// (npm/mcp/go, feed_format=="none") and any other platform return
// NotFound. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformWordPress && platform != updateserver_model.PlatformBoth {
if updateserver_model.IsFeedlessPlatform(platform) ||
(platform != updateserver_model.PlatformWordPress && platform != updateserver_model.PlatformBoth) {
ctx.NotFound(nil)
return
}
@@ -200,10 +205,10 @@ func ServeWordPressJSON(ctx *context.Context) {
// ServeComposerJSON generates and serves a Composer packages.json feed.
func ServeComposerJSON(ctx *context.Context) {
// Only serve for composer. Feedless platforms (npm/mcp) and any other
// platform return NotFound. See #810.
// Only serve for composer. Registry-recognized feedless platforms
// (npm/mcp/go) and any other platform return NotFound. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformComposer {
if updateserver_model.IsFeedlessPlatform(platform) || platform != updateserver_model.PlatformComposer {
ctx.NotFound(nil)
return
}
@@ -240,10 +245,10 @@ func ServeComposerJSON(ctx *context.Context) {
// ServePrestaShopXML generates and serves a PrestaShop module update XML.
func ServePrestaShopXML(ctx *context.Context) {
// Only serve for prestashop. Feedless platforms (npm/mcp) and any other
// platform return NotFound. See #810.
// Only serve for prestashop. Registry-recognized feedless platforms
// (npm/mcp/go) and any other platform return NotFound. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformPrestaShop {
if updateserver_model.IsFeedlessPlatform(platform) || platform != updateserver_model.PlatformPrestaShop {
ctx.NotFound(nil)
return
}
@@ -269,10 +274,10 @@ func ServePrestaShopXML(ctx *context.Context) {
// ServeDrupalXML generates and serves a Drupal update status XML.
func ServeDrupalXML(ctx *context.Context) {
// Only serve for drupal. Feedless platforms (npm/mcp) and any other
// platform return NotFound. See #810.
// Only serve for drupal. Registry-recognized feedless platforms
// (npm/mcp/go) and any other platform return NotFound. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformDrupal {
if updateserver_model.IsFeedlessPlatform(platform) || platform != updateserver_model.PlatformDrupal {
ctx.NotFound(nil)
return
}
@@ -298,10 +303,10 @@ func ServeDrupalXML(ctx *context.Context) {
// ServeWHMCSJSON generates and serves a WHMCS module update JSON.
func ServeWHMCSJSON(ctx *context.Context) {
// Only serve for whmcs. Feedless platforms (npm/mcp) and any other
// platform return NotFound. See #810.
// Only serve for whmcs. Registry-recognized feedless platforms
// (npm/mcp/go) and any other platform return NotFound. See #810 / #811.
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
if platform != updateserver_model.PlatformWHMCS {
if updateserver_model.IsFeedlessPlatform(platform) || platform != updateserver_model.PlatformWHMCS {
ctx.NotFound(nil)
return
}