From 38aa4de4122d4a2156bb4d37c9632846ec416035 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Sat, 18 Jul 2026 21:29:07 -0500 Subject: [PATCH] feat(platform): structured platform registry + /api/v1/platforms; update server reads registry [#367] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn the flat admin [metadata] PLATFORM_OPTIONS key list into a structured platform registry where each key carries capability attributes (family, artifact, update_generator, feed_format, template_repo, manifest_based, publish_target, detect_globs, aliases), and expose it so CI / the update server / mokocli can read platform behavior from config instead of hardcoding. Storage: new [metadata] PLATFORM_REGISTRY key in app.ini — inline JSON array or a path to a JSON file (e.g. custom/platforms.json), parsed in modules/setting. No DB table (idiomatic: mirrors existing app.ini + file-path config patterns; non-secret capability config only). Built-in DEFAULT registry (joomla, dolibarr, go, npm, mcp, generic) keeps behavior unchanged when unconfigured, and reconciles with PLATFORM_OPTIONS. API: GET /api/v1/platforms (whole registry) and GET /api/v1/platforms/{key} (one), wired into the public misc group. Update server: additive registry-driven helpers in services/updateserver (GeneratorForPlatform / FeedFormatForPlatform / ContentTypeForPlatform / PlatformServesGenerator) that replace hardcoded per-platform generator/feed decisions with registry lookups. Handler bodies left untouched to avoid conflicting with PR #825 (feature/updateserver-npm-mcp), which is concurrently rewiring routers/web/repo/updateserver.go and adding models/updateserver/platform.go; helpers are drop-in for adoption once #825 lands. Admin UI: read-only resolved-registry table added to Admin -> Metadata; the textarea still edits the flat key list. Capabilities edited via config for now. Does NOT touch models/repo/repo_manifest.go or the per-repo apiMetadata struct (owned by a separate PR). Authored-by: Moko Consulting --- custom/conf/app.example.ini | 20 +++ modules/setting/metadata.go | 4 + modules/setting/platform_registry.go | 255 +++++++++++++++++++++++++++ modules/structs/miscellaneous.go | 28 +++ routers/api/v1/api.go | 4 + routers/api/v1/misc/platforms.go | 78 ++++++++ routers/api/v1/swagger/misc.go | 14 ++ routers/web/admin/metadata.go | 4 + services/updateserver/registry.go | 75 ++++++++ templates/admin/metadata.tmpl | 35 ++++ 10 files changed, 517 insertions(+) create mode 100644 modules/setting/platform_registry.go create mode 100644 routers/api/v1/misc/platforms.go create mode 100644 services/updateserver/registry.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index de44e2783b..8fdcd1e030 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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}] diff --git a/modules/setting/metadata.go b/modules/setting/metadata.go index 777f681e35..5b1025151a 100644 --- a/modules/setting/metadata.go +++ b/modules/setting/metadata.go @@ -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) } diff --git a/modules/setting/platform_registry.go b/modules/setting/platform_registry.go new file mode 100644 index 0000000000..c248373d1e --- /dev/null +++ b/modules/setting/platform_registry.go @@ -0,0 +1,255 @@ +// Copyright 2026 Moko Consulting +// 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 + } +} diff --git a/modules/structs/miscellaneous.go b/modules/structs/miscellaneous.go index 293ba99579..168f9803fa 100644 --- a/modules/structs/miscellaneous.go +++ b/modules/structs/miscellaneous.go @@ -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 diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9cac166ec7..c0f547a408 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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) diff --git a/routers/api/v1/misc/platforms.go b/routers/api/v1/misc/platforms.go new file mode 100644 index 0000000000..343428911a --- /dev/null +++ b/routers/api/v1/misc/platforms.go @@ -0,0 +1,78 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/routers/api/v1/swagger/misc.go b/routers/api/v1/swagger/misc.go index a16453a03a..3b201e4fb4 100644 --- a/routers/api/v1/swagger/misc.go +++ b/routers/api/v1/swagger/misc.go @@ -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"` +} diff --git a/routers/web/admin/metadata.go b/routers/web/admin/metadata.go index b55f7c0389..be13d37913 100644 --- a/routers/web/admin/metadata.go +++ b/routers/web/admin/metadata.go @@ -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) } diff --git a/services/updateserver/registry.go b/services/updateserver/registry.go new file mode 100644 index 0000000000..a840d01a99 --- /dev/null +++ b/services/updateserver/registry.go @@ -0,0 +1,75 @@ +// Copyright 2026 Moko Consulting +// 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 +} diff --git a/templates/admin/metadata.tmpl b/templates/admin/metadata.tmpl index e9061e36d9..b3dd6879e9 100644 --- a/templates/admin/metadata.tmpl +++ b/templates/admin/metadata.tmpl @@ -15,5 +15,40 @@

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.

+ +

+ {{svg "octicon-table" 16}} Platform Registry +

+
+

Resolved capability attributes per platform. These drive CI, the update server, and workflow sync (exposed at GET /api/v1/platforms). Capabilities are edited via app.ini [metadata] PLATFORM_REGISTRY (inline JSON array or a path to a JSON file such as custom/platforms.json); when unset, a built-in default registry is used.

+ + + + + + + + + + + + + + + {{range .PlatformRegistry}} + + + + + + + + + + + {{end}} + +
KeyLabelFamilyArtifactUpdate GeneratorFeed FormatManifestTemplate Repo
{{.Key}}{{.Label}}{{.Family}}{{if .Artifact}}yes{{else}}no{{end}}{{.UpdateGenerator}}{{.FeedFormat}}{{if .ManifestBased}}yes{{else}}no{{end}}{{.TemplateRepo}}
+
{{template "admin/layout_footer" .}} -- 2.52.0