feat(platform): structured platform registry + /api/v1/platforms; update server reads registry [#367] #826
@@ -2999,3 +2999,23 @@ LEVEL = Info
|
||||
;SERVICE_TYPE = memory
|
||||
;; Ignored for the "memory" type. For "redis" use something like `redis://127.0.0.1:6379/0`
|
||||
;SERVICE_CONN_STR =
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Repository metadata / platform registry
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[metadata]
|
||||
;; Comma-separated list of allowed values for the repo metadata "platform" field.
|
||||
;; Also editable via Admin -> Metadata (persisted here). Default taxonomy:
|
||||
;PLATFORM_OPTIONS = joomla,dolibarr,go,npm,generic
|
||||
;;
|
||||
;; Structured platform registry: capability attributes per platform key, read by
|
||||
;; CI, the update server, and workflow sync (exposed at GET /api/v1/platforms).
|
||||
;; Value is EITHER an inline JSON array of platform objects OR a path to a JSON
|
||||
;; file (relative paths resolve against the custom/ dir). When unset, a built-in
|
||||
;; default registry is used (joomla, dolibarr, go, npm, mcp, generic), so
|
||||
;; behavior is unchanged if this is not configured.
|
||||
;; Example (file path):
|
||||
;PLATFORM_REGISTRY = custom/platforms.json
|
||||
;; Example (inline JSON):
|
||||
;;PLATFORM_REGISTRY = [{"key":"joomla","label":"Joomla Extension","family":"joomla","artifact":true,"update_generator":"joomla","feed_format":"xml","template_repo":"Template-Joomla","manifest_based":true}]
|
||||
|
||||
@@ -19,4 +19,8 @@ func loadMetadataFrom(rootCfg ConfigProvider) {
|
||||
if opts := sec.Key("PLATFORM_OPTIONS").Strings(","); len(opts) > 0 {
|
||||
Metadata.PlatformOptions = opts
|
||||
}
|
||||
|
||||
// Load the structured platform registry ([metadata] PLATFORM_REGISTRY).
|
||||
// Falls back to a built-in default reconciled with PLATFORM_OPTIONS.
|
||||
loadPlatformRegistryFrom(rootCfg)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PlatformDef is a single platform's capability record in the platform registry.
|
||||
//
|
||||
// A platform key (matching repo_manifest.platform and UpdateStreamConfig.Platform)
|
||||
// carries the capability attributes that CI, the update server, and the workflow
|
||||
// sync tooling read instead of hardcoding per-platform behavior. Only Key is
|
||||
// required; every other field has a sensible zero/default so a bare key still
|
||||
// yields a usable (generic) record.
|
||||
//
|
||||
// This registry is NON-SECRET capability config only. It never carries
|
||||
// credentials, tokens, or deploy keys.
|
||||
type PlatformDef struct {
|
||||
// Key is the stable lowercase identifier (e.g. "joomla"). Matches
|
||||
// repo_manifest.platform and UpdateStreamConfig.Platform.
|
||||
Key string `json:"key"`
|
||||
// Label is the human-readable display name.
|
||||
Label string `json:"label,omitempty"`
|
||||
// Aliases are alternate keys matched (by prefix) in the release gate,
|
||||
// e.g. joomla5, joomla6.
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
// Family groups related platforms: joomla | dolibarr | node | go | generic | dot.
|
||||
Family string `json:"family,omitempty"`
|
||||
// Artifact reports whether the platform produces a build artifact.
|
||||
Artifact bool `json:"artifact"`
|
||||
// UpdateGenerator names the update-feed generator the update server uses:
|
||||
// joomla | dolibarr | wordpress | composer | prestashop | drupal | whmcs | npm | none.
|
||||
UpdateGenerator string `json:"update_generator,omitempty"`
|
||||
// FeedFormat is the update-feed wire format: xml | json | none.
|
||||
FeedFormat string `json:"feed_format,omitempty"`
|
||||
// TemplateRepo is the workflow-template source repo (e.g. Template-Joomla).
|
||||
TemplateRepo string `json:"template_repo,omitempty"`
|
||||
// ManifestBased reports whether a manifest is parsed for element/version.
|
||||
ManifestBased bool `json:"manifest_based"`
|
||||
// PublishTarget names where artifacts are published (e.g. npm, updateserver, git-release).
|
||||
PublishTarget string `json:"publish_target,omitempty"`
|
||||
// DetectGlobs are file-pattern globs used to auto-detect this platform.
|
||||
DetectGlobs []string `json:"detect_globs,omitempty"`
|
||||
}
|
||||
|
||||
// Platforms holds the loaded platform registry indexed by key (lowercase).
|
||||
// It is populated by loadMetadataFrom at startup. Callers should use
|
||||
// PlatformRegistry(), GetPlatform() and PlatformList() rather than reading the
|
||||
// map directly.
|
||||
var platformRegistry map[string]*PlatformDef
|
||||
|
||||
// PlatformRegistry returns the loaded registry as a key->def map. Never nil.
|
||||
func PlatformRegistry() map[string]*PlatformDef {
|
||||
if platformRegistry == nil {
|
||||
platformRegistry = buildDefaultRegistry(Metadata.PlatformOptions)
|
||||
}
|
||||
return platformRegistry
|
||||
}
|
||||
|
||||
// PlatformList returns registry entries in the order of Metadata.PlatformOptions
|
||||
// (falling back to a stable built-in order for any extra keys).
|
||||
func PlatformList() []*PlatformDef {
|
||||
reg := PlatformRegistry()
|
||||
out := make([]*PlatformDef, 0, len(reg))
|
||||
seen := make(map[string]bool, len(reg))
|
||||
// Preserve admin-configured order first.
|
||||
for _, k := range Metadata.PlatformOptions {
|
||||
k = strings.ToLower(strings.TrimSpace(k))
|
||||
if def, ok := reg[k]; ok && !seen[k] {
|
||||
out = append(out, def)
|
||||
seen[k] = true
|
||||
}
|
||||
}
|
||||
// Append any registry-only keys not present in PlatformOptions.
|
||||
for k, def := range reg {
|
||||
if !seen[k] {
|
||||
out = append(out, def)
|
||||
seen[k] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetPlatform returns the definition for a platform key. If the key is unknown,
|
||||
// it synthesizes a generic default so callers never get a nil (matches the
|
||||
// back-compat guarantee that an unlisted platform behaves like "generic").
|
||||
// The bool reports whether the key was explicitly present in the registry.
|
||||
func GetPlatform(key string) (*PlatformDef, bool) {
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
reg := PlatformRegistry()
|
||||
if def, ok := reg[key]; ok {
|
||||
return def, true
|
||||
}
|
||||
return genericPlatformDef(key), false
|
||||
}
|
||||
|
||||
// genericPlatformDef returns a safe generic capability record for an unknown key.
|
||||
func genericPlatformDef(key string) *PlatformDef {
|
||||
return &PlatformDef{
|
||||
Key: key,
|
||||
Label: key,
|
||||
Family: "generic",
|
||||
Artifact: false,
|
||||
UpdateGenerator: "none",
|
||||
FeedFormat: "none",
|
||||
TemplateRepo: "Template-Generic",
|
||||
ManifestBased: false,
|
||||
PublishTarget: "git-release",
|
||||
}
|
||||
}
|
||||
|
||||
// builtinRegistry is the default platform registry. It reproduces current
|
||||
// behavior so an unconfigured instance is unchanged: Joomla still maps to the
|
||||
// joomla generator (xml), Dolibarr to dolibarr (json), npm/mcp are feedless, etc.
|
||||
func builtinRegistry() []*PlatformDef {
|
||||
return []*PlatformDef{
|
||||
{
|
||||
Key: "joomla", Label: "Joomla Extension", Aliases: []string{"joomla5", "joomla6"},
|
||||
Family: "joomla", Artifact: true, UpdateGenerator: "joomla", FeedFormat: "xml",
|
||||
TemplateRepo: "Template-Joomla", ManifestBased: true, PublishTarget: "updateserver",
|
||||
DetectGlobs: []string{"source/pkg_*.xml", "**/templateDetails.xml"},
|
||||
},
|
||||
{
|
||||
Key: "dolibarr", Label: "Dolibarr Module",
|
||||
Family: "dolibarr", Artifact: true, UpdateGenerator: "dolibarr", FeedFormat: "json",
|
||||
TemplateRepo: "Template-Dolibarr", ManifestBased: true, PublishTarget: "updateserver",
|
||||
DetectGlobs: []string{"htdocs/**/core/modules/mod*.class.php"},
|
||||
},
|
||||
{
|
||||
Key: "go", Label: "Go Module",
|
||||
Family: "go", Artifact: false, UpdateGenerator: "none", FeedFormat: "none",
|
||||
TemplateRepo: "Template-Go", ManifestBased: false, PublishTarget: "git-release",
|
||||
DetectGlobs: []string{"go.mod"},
|
||||
},
|
||||
{
|
||||
Key: "npm", Label: "NPM Package",
|
||||
Family: "node", Artifact: true, UpdateGenerator: "npm", FeedFormat: "none",
|
||||
TemplateRepo: "Template-NPM", ManifestBased: true, PublishTarget: "npm",
|
||||
DetectGlobs: []string{"package.json"},
|
||||
},
|
||||
{
|
||||
Key: "mcp", Label: "MCP Server",
|
||||
Family: "node", Artifact: true, UpdateGenerator: "npm", FeedFormat: "none",
|
||||
TemplateRepo: "Template-NPM", ManifestBased: true, PublishTarget: "npm",
|
||||
DetectGlobs: []string{"**/mcp.json", "**/.mcp.json"},
|
||||
},
|
||||
{
|
||||
Key: "generic", Label: "Generic", Family: "generic", Artifact: false,
|
||||
UpdateGenerator: "none", FeedFormat: "none", TemplateRepo: "Template-Generic",
|
||||
ManifestBased: false, PublishTarget: "git-release",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildDefaultRegistry indexes the built-in registry by key and then ensures a
|
||||
// record exists for every key in the admin PLATFORM_OPTIONS list (synthesizing a
|
||||
// generic default for any key without a built-in entry). This keeps the flat
|
||||
// PLATFORM_OPTIONS list and the structured registry consistent.
|
||||
func buildDefaultRegistry(options []string) map[string]*PlatformDef {
|
||||
reg := make(map[string]*PlatformDef)
|
||||
for _, def := range builtinRegistry() {
|
||||
d := def
|
||||
reg[d.Key] = d
|
||||
}
|
||||
for _, opt := range options {
|
||||
k := strings.ToLower(strings.TrimSpace(opt))
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := reg[k]; !ok {
|
||||
reg[k] = genericPlatformDef(k)
|
||||
}
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
// loadPlatformRegistryFrom loads the structured platform registry.
|
||||
//
|
||||
// Storage: [metadata] PLATFORM_REGISTRY in app.ini. The value is EITHER an
|
||||
// inline JSON array of PlatformDef objects, OR a path to a JSON file (relative
|
||||
// paths are resolved against CustomPath, e.g. custom/platforms.json). When
|
||||
// unset, the built-in default registry is used, merged with any extra keys from
|
||||
// PLATFORM_OPTIONS. This keeps behavior unchanged for unconfigured instances and
|
||||
// avoids a DB schema change (idiomatic: mirrors the existing app.ini + file-path
|
||||
// config pattern used elsewhere in modules/setting).
|
||||
func loadPlatformRegistryFrom(rootCfg ConfigProvider) {
|
||||
// Base defaults, reconciled with the flat PLATFORM_OPTIONS list.
|
||||
platformRegistry = buildDefaultRegistry(Metadata.PlatformOptions)
|
||||
|
||||
raw := strings.TrimSpace(rootCfg.Section("metadata").Key("PLATFORM_REGISTRY").String())
|
||||
if raw == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if strings.HasPrefix(raw, "[") {
|
||||
// Inline JSON array.
|
||||
data = []byte(raw)
|
||||
} else {
|
||||
// Treat as a file path (resolve relative to CustomPath).
|
||||
path := raw
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(CustomPath, path)
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
// Non-fatal: keep the default registry so the forge still boots.
|
||||
return
|
||||
}
|
||||
data = b
|
||||
}
|
||||
|
||||
var defs []*PlatformDef
|
||||
if err := json.Unmarshal(data, &defs); err != nil {
|
||||
// Non-fatal: malformed registry falls back to the default.
|
||||
return
|
||||
}
|
||||
if len(defs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Replace the registry with the configured set, filling defaults per entry.
|
||||
reg := make(map[string]*PlatformDef, len(defs))
|
||||
for _, def := range defs {
|
||||
if def == nil {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(def.Key))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
def.Key = key
|
||||
if def.Label == "" {
|
||||
def.Label = key
|
||||
}
|
||||
if def.Family == "" {
|
||||
def.Family = "generic"
|
||||
}
|
||||
if def.UpdateGenerator == "" {
|
||||
def.UpdateGenerator = "none"
|
||||
}
|
||||
if def.FeedFormat == "" {
|
||||
def.FeedFormat = "none"
|
||||
}
|
||||
reg[key] = def
|
||||
}
|
||||
if len(reg) > 0 {
|
||||
platformRegistry = reg
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,34 @@ type LicenseTemplateInfo struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// Platform is a single platform capability record from the platform registry.
|
||||
// It exposes the non-secret capability attributes CI, the update server, and the
|
||||
// workflow sync tooling read instead of hardcoding per-platform behavior.
|
||||
type Platform struct {
|
||||
// Key is the stable lowercase platform identifier
|
||||
Key string `json:"key"`
|
||||
// Label is the human-readable display name
|
||||
Label string `json:"label"`
|
||||
// Aliases are alternate keys matched by prefix in the release gate
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
// Family groups related platforms (joomla, dolibarr, node, go, generic, dot)
|
||||
Family string `json:"family"`
|
||||
// Artifact reports whether the platform produces a build artifact
|
||||
Artifact bool `json:"artifact"`
|
||||
// UpdateGenerator names the update-feed generator (joomla, dolibarr, npm, none, ...)
|
||||
UpdateGenerator string `json:"update_generator"`
|
||||
// FeedFormat is the update-feed wire format (xml, json, none)
|
||||
FeedFormat string `json:"feed_format"`
|
||||
// TemplateRepo is the workflow-template source repo
|
||||
TemplateRepo string `json:"template_repo,omitempty"`
|
||||
// ManifestBased reports whether a manifest is parsed for element/version
|
||||
ManifestBased bool `json:"manifest_based"`
|
||||
// PublishTarget names where artifacts are published
|
||||
PublishTarget string `json:"publish_target,omitempty"`
|
||||
// DetectGlobs are file-pattern globs used to auto-detect this platform
|
||||
DetectGlobs []string `json:"detect_globs,omitempty"`
|
||||
}
|
||||
|
||||
// APIError is an api error with a message
|
||||
type APIError struct {
|
||||
// Message contains the error description
|
||||
|
||||
@@ -1009,6 +1009,10 @@ func Routes() *web.Router {
|
||||
m.Get("/label/templates", misc.ListLabelTemplates)
|
||||
m.Get("/label/templates/{name}", misc.GetLabelTemplate)
|
||||
|
||||
// Platform registry (non-secret capability config read by CI / mokocli).
|
||||
m.Get("/platforms", misc.ListPlatforms)
|
||||
m.Get("/platforms/{key}", misc.GetPlatform)
|
||||
|
||||
m.Group("/settings", func() {
|
||||
m.Get("/ui", settings.GetGeneralUISettings)
|
||||
m.Get("/api", settings.GetGeneralAPISettings)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package misc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
|
||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/services/context"
|
||||
)
|
||||
|
||||
// toAPIPlatform converts a setting.PlatformDef into its API representation.
|
||||
func toAPIPlatform(def *setting.PlatformDef) *api.Platform {
|
||||
return &api.Platform{
|
||||
Key: def.Key,
|
||||
Label: def.Label,
|
||||
Aliases: def.Aliases,
|
||||
Family: def.Family,
|
||||
Artifact: def.Artifact,
|
||||
UpdateGenerator: def.UpdateGenerator,
|
||||
FeedFormat: def.FeedFormat,
|
||||
TemplateRepo: def.TemplateRepo,
|
||||
ManifestBased: def.ManifestBased,
|
||||
PublishTarget: def.PublishTarget,
|
||||
DetectGlobs: def.DetectGlobs,
|
||||
}
|
||||
}
|
||||
|
||||
// ListPlatforms returns the whole platform registry.
|
||||
func ListPlatforms(ctx *context.APIContext) {
|
||||
// swagger:operation GET /platforms miscellaneous listPlatforms
|
||||
// ---
|
||||
// summary: Returns the platform registry (all platform capability records)
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PlatformList"
|
||||
defs := setting.PlatformList()
|
||||
response := make([]*api.Platform, len(defs))
|
||||
for i, def := range defs {
|
||||
response[i] = toAPIPlatform(def)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetPlatform returns a single platform record by key.
|
||||
func GetPlatform(ctx *context.APIContext) {
|
||||
// swagger:operation GET /platforms/{key} miscellaneous getPlatform
|
||||
// ---
|
||||
// summary: Returns a single platform capability record by key
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: key
|
||||
// in: path
|
||||
// description: platform key
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Platform"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
key := strings.ToLower(strings.TrimSpace(ctx.PathParam("key")))
|
||||
def, ok := setting.GetPlatform(key)
|
||||
if !ok {
|
||||
// Unknown key: only 404 when it is not an admin-listed platform.
|
||||
// (GetPlatform synthesizes a generic default for unknown keys, but the
|
||||
// endpoint should be honest about what is actually configured.)
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, toAPIPlatform(def))
|
||||
}
|
||||
@@ -62,3 +62,17 @@ type swaggerResponseLabelTemplateInfo struct {
|
||||
// in:body
|
||||
Body []api.LabelTemplate `json:"body"`
|
||||
}
|
||||
|
||||
// PlatformList
|
||||
// swagger:response PlatformList
|
||||
type swaggerResponsePlatformList struct {
|
||||
// in:body
|
||||
Body []api.Platform `json:"body"`
|
||||
}
|
||||
|
||||
// Platform
|
||||
// swagger:response Platform
|
||||
type swaggerResponsePlatform struct {
|
||||
// in:body
|
||||
Body api.Platform `json:"body"`
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ func Metadata(ctx *context.Context) {
|
||||
ctx.Data["Title"] = "Metadata"
|
||||
ctx.Data["PageIsAdminMetadata"] = true
|
||||
ctx.Data["PlatformOptions"] = strings.Join(setting.Metadata.PlatformOptions, "\n")
|
||||
// Resolved platform registry (read-only view). Capability attributes are
|
||||
// edited via app.ini [metadata] PLATFORM_REGISTRY; the textarea above still
|
||||
// edits the flat key list (PLATFORM_OPTIONS).
|
||||
ctx.Data["PlatformRegistry"] = setting.PlatformList()
|
||||
ctx.HTML(http.StatusOK, tplMetadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
|
||||
)
|
||||
|
||||
// This file rewires the update server's per-platform behavior onto the platform
|
||||
// registry (modules/setting) so that the update-feed generator and wire format
|
||||
// for a platform are looked up from config instead of hardcoded.
|
||||
//
|
||||
// Overlap note (for reviewer): PR #825 (feature/updateserver-npm-mcp) also
|
||||
// touches routers/web/repo/updateserver.go and adds models/updateserver/
|
||||
// platform.go (platform constants + ResolvePlatform + IsFeedlessPlatform). To
|
||||
// stay compatible and avoid a heavy conflict, this change is intentionally
|
||||
// ADDITIVE: it adds registry lookups here rather than rewriting the Serve*
|
||||
// handler bodies. The helpers below are drop-in replacements for the hardcoded
|
||||
// per-platform decisions and can be adopted by the handlers once #825 lands.
|
||||
|
||||
// GeneratorForPlatform returns the update-feed generator name for a platform
|
||||
// key, read from the registry. Falls back to "none" for unknown platforms.
|
||||
//
|
||||
// This replaces the hardcoded per-platform generator selection: instead of a
|
||||
// switch on the platform string, callers read registry[platform].UpdateGenerator.
|
||||
func GeneratorForPlatform(platform string) string {
|
||||
def, _ := setting.GetPlatform(platform)
|
||||
if def.UpdateGenerator == "" {
|
||||
return "none"
|
||||
}
|
||||
return def.UpdateGenerator
|
||||
}
|
||||
|
||||
// FeedFormatForPlatform returns the update-feed wire format (xml|json|none) for a
|
||||
// platform key, read from the registry. Content-Type is derived from this.
|
||||
func FeedFormatForPlatform(platform string) string {
|
||||
def, _ := setting.GetPlatform(platform)
|
||||
if def.FeedFormat == "" {
|
||||
return "none"
|
||||
}
|
||||
return def.FeedFormat
|
||||
}
|
||||
|
||||
// ContentTypeForPlatform returns the HTTP Content-Type for a platform's update
|
||||
// feed, derived from the registry feed_format. Empty string when the platform
|
||||
// emits no feed (feed_format=none), letting callers 404.
|
||||
func ContentTypeForPlatform(platform string) string {
|
||||
switch FeedFormatForPlatform(platform) {
|
||||
case "xml":
|
||||
return "application/xml; charset=utf-8"
|
||||
case "json":
|
||||
return "application/json; charset=utf-8"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// PlatformServesGenerator reports whether the given platform key resolves (via
|
||||
// the registry) to the named update-feed generator. This lets a Serve* handler
|
||||
// gate on capability config instead of a hardcoded string compare, e.g.
|
||||
//
|
||||
// if !PlatformServesGenerator(platform, "joomla") { ctx.NotFound(nil); return }
|
||||
//
|
||||
// The empty platform string is treated as "joomla" for backwards compatibility
|
||||
// with legacy Joomla repos that predate the registry (mirrors the historic
|
||||
// "empty ⇒ joomla" default; PR #825's ResolvePlatform centralizes this).
|
||||
func PlatformServesGenerator(platform, generator string) bool {
|
||||
if strings.TrimSpace(platform) == "" {
|
||||
platform = "joomla"
|
||||
}
|
||||
return GeneratorForPlatform(platform) == generator
|
||||
}
|
||||
@@ -15,5 +15,40 @@
|
||||
</form>
|
||||
<p class="tw-text-text-light tw-text-sm tw-mt-3">Existing repositories keep their stored platform value even if it is later removed from this list; it simply won't be offered to other repos.</p>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-table" 16}} Platform Registry
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p class="tw-text-text-light tw-text-sm">Resolved capability attributes per platform. These drive CI, the update server, and workflow sync (exposed at <code>GET /api/v1/platforms</code>). Capabilities are edited via <code>app.ini</code> <code>[metadata] PLATFORM_REGISTRY</code> (inline JSON array or a path to a JSON file such as <code>custom/platforms.json</code>); when unset, a built-in default registry is used.</p>
|
||||
<table class="ui small compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Label</th>
|
||||
<th>Family</th>
|
||||
<th>Artifact</th>
|
||||
<th>Update Generator</th>
|
||||
<th>Feed Format</th>
|
||||
<th>Manifest</th>
|
||||
<th>Template Repo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .PlatformRegistry}}
|
||||
<tr>
|
||||
<td><code>{{.Key}}</code></td>
|
||||
<td>{{.Label}}</td>
|
||||
<td>{{.Family}}</td>
|
||||
<td>{{if .Artifact}}yes{{else}}no{{end}}</td>
|
||||
<td>{{.UpdateGenerator}}</td>
|
||||
<td>{{.FeedFormat}}</td>
|
||||
<td>{{if .ManifestBased}}yes{{else}}no{{end}}</td>
|
||||
<td>{{.TemplateRepo}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
|
||||
Reference in New Issue
Block a user