Compare commits

..

1 Commits

Author SHA1 Message Date
jmiller 506e4a49d6 feat(metadata): admin-configurable platform dropdown options
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 29s
Generic: Project CI / Tests (pull_request) Successful in 29s
Universal: PR Check / Validate PR (pull_request) Successful in 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 44s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The repo metadata 'platform' field was a hardcoded enum; changing the
taxonomy meant a code change + redeploy. Make it admin-configurable:

- modules/setting/metadata.go: new [metadata] PLATFORM_OPTIONS setting
  (default joomla,dolibarr,go,npm,generic), loaded in loadCommonSettingsFrom
- routers/web/admin/metadata.go + templates/admin/metadata.tmpl: new
  Admin -> Metadata page to edit the list (persisted to app.ini), with a
  nav entry and route
- routers/web/repo/setting/metadata.go + template: platform dropdown now
  reads from the setting; a repo's current value stays selectable even if
  removed from the list (no silent drop)

Closes #777
2026-07-13 17:27:03 -05:00
9 changed files with 147 additions and 6 deletions
+1
View File
@@ -3,6 +3,7 @@
## [Unreleased]
### Added
- Metadata platform options are now **admin-configurable** instead of hardcoded: a new **Admin → Metadata** page edits the allowed `platform` values (persisted to `app.ini` `[metadata] PLATFORM_OPTIONS`, default `joomla,dolibarr,go,npm,generic`), and the repo Settings → Metadata platform dropdown reads from it. Changing the taxonomy no longer needs a code change/redeploy. A repo's existing platform value stays selectable even if later removed from the list (#777)
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
// Metadata holds instance-level settings for the repository metadata feature.
var Metadata = struct {
// PlatformOptions is the admin-configurable list of allowed values for the
// repo metadata "platform" field. Editable via Admin -> Metadata (persisted
// to app.ini [metadata] PLATFORM_OPTIONS) so changing the taxonomy needs no
// code change or redeploy. See issue #777.
PlatformOptions []string
}{
PlatformOptions: []string{"joomla", "dolibarr", "go", "npm", "generic"},
}
func loadMetadataFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("metadata")
if opts := sec.Key("PLATFORM_OPTIONS").Strings(","); len(opts) > 0 {
Metadata.PlatformOptions = opts
}
}
+1
View File
@@ -168,6 +168,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
}
loadUIFrom(cfg)
loadAdminFrom(cfg)
loadMetadataFrom(cfg)
loadAPIFrom(cfg)
loadMetricsFrom(cfg)
loadCamoFrom(cfg)
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package admin
import (
"net/http"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplMetadata templates.TplName = "admin/metadata"
// Metadata shows the admin metadata-settings page.
func Metadata(ctx *context.Context) {
ctx.Data["Title"] = "Metadata"
ctx.Data["PageIsAdminMetadata"] = true
ctx.Data["PlatformOptions"] = strings.Join(setting.Metadata.PlatformOptions, "\n")
ctx.HTML(http.StatusOK, tplMetadata)
}
// parsePlatformOptions normalizes admin input (comma- or newline-separated) into
// a clean, lowercased, de-duplicated, order-preserving list of platform values.
func parsePlatformOptions(raw string) []string {
seen := make(map[string]bool)
var out []string
for _, tok := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' }) {
v := strings.ToLower(strings.TrimSpace(tok))
if v == "" || seen[v] {
continue
}
seen[v] = true
out = append(out, v)
}
return out
}
// MetadataSettings persists the admin metadata settings.
func MetadataSettings(ctx *context.Context) {
opts := parsePlatformOptions(ctx.FormString("platform_options"))
if len(opts) == 0 {
ctx.Flash.Error("Platform options cannot be empty")
ctx.Redirect(setting.AppSubURL + "/-/admin/metadata")
return
}
// Apply in-memory so the change takes effect without a restart.
setting.Metadata.PlatformOptions = opts
// Persist to app.ini [metadata] PLATFORM_OPTIONS.
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
ctx.Flash.Error("Failed to load config: " + err.Error())
ctx.Redirect(setting.AppSubURL + "/-/admin/metadata")
return
}
cfg.Section("metadata").Key("PLATFORM_OPTIONS").SetValue(strings.Join(opts, ","))
if err := cfg.SaveTo(setting.CustomConf); err != nil {
ctx.Flash.Error("Failed to save config: " + err.Error())
log.Error("SaveTo %s: %v", setting.CustomConf, err)
} else {
ctx.Flash.Success("Metadata settings saved")
log.Info("Metadata platform options updated: %v", opts)
}
ctx.Redirect(setting.AppSubURL + "/-/admin/metadata")
}
+23 -4
View File
@@ -10,6 +10,7 @@ import (
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
@@ -43,6 +44,25 @@ func Metadata(ctx *context.Context) {
// It is displayed read-only. See issue #771.
ctx.Data["DerivedOrg"] = ctx.Repo.Repository.DerivedOrgName(ctx)
// Platform dropdown options come from the admin-configurable setting
// (Admin -> Metadata). Keep the repo's current value selectable even if it
// was later removed from the list, so saving does not silently drop it.
// See issue #777.
platformOpts := append([]string(nil), setting.Metadata.PlatformOptions...)
if manifest.Platform != "" {
found := false
for _, p := range platformOpts {
if p == manifest.Platform {
found = true
break
}
}
if !found {
platformOpts = append([]string{manifest.Platform}, platformOpts...)
}
}
ctx.Data["PlatformOptions"] = platformOpts
// Load repo-scoped custom fields.
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
ctx.Data["CustomFieldDefs"] = fields
@@ -102,8 +122,8 @@ func saveMetadata(ctx *context.Context) {
existing, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
manifest := &repo_model.RepoMetadata{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
// org is not stored: it is derived from the org profile on read. See issue #771.
Description: ctx.Repo.Repository.Description,
LicenseSPDX: spdx,
@@ -113,7 +133,7 @@ func saveMetadata(ctx *context.Context) {
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
ExtensionType: ctx.FormString("extension_type"),
ExtensionType: ctx.FormString("extension_type"),
EntryPoint: ctx.FormString("entry_point"),
}
@@ -160,4 +180,3 @@ func saveCustomFields(ctx *context.Context) {
ctx.Flash.Success("Custom fields saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
+6 -1
View File
@@ -786,6 +786,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/reset", admin.BrandingReset)
})
m.Group("/metadata", func() {
m.Get("", admin.Metadata)
m.Post("/settings", admin.MetadataSettings)
})
m.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks)
@@ -1239,7 +1244,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, repo_setting.SettingsCtxData)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Combo("/updateserver").Get(repo_setting.UpdateServerSettings).Post(repo_setting.UpdateServerSettingsPost)
m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Group("/security", func() {
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
+19
View File
@@ -0,0 +1,19 @@
{{template "admin/layout_head" (dict "pageClass" "admin metadata")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{svg "octicon-list-unordered" 16}} Metadata
</h4>
<div class="ui attached segment">
<h5>Platform Options</h5>
<p class="tw-text-text-light tw-text-sm">Allowed values for the repository metadata <strong>Platform</strong> field. One per line (or comma-separated). Changes take effect immediately and are saved to <code>app.ini</code> <code>[metadata] PLATFORM_OPTIONS</code>.</p>
<form method="post" action="{{AppSubUrl}}/-/admin/metadata/settings">
{{.CsrfTokenHtml}}
<div class="field">
<textarea name="platform_options" rows="6" class="tw-w-full tw-font-mono" placeholder="joomla&#10;dolibarr&#10;go&#10;npm&#10;generic">{{.PlatformOptions}}</textarea>
</div>
<button type="submit" class="ui primary small button tw-mt-2">{{svg "octicon-check" 14}} Save Settings</button>
</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>
</div>
{{template "admin/layout_footer" .}}
+3
View File
@@ -87,6 +87,9 @@
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
{{svg "octicon-paintbrush" 16}} Branding
</a>
<a class="{{if .PageIsAdminMetadata}}active {{end}}item" href="{{AppSubUrl}}/-/admin/metadata">
{{svg "octicon-list-unordered" 16}} Metadata
</a>
<a class="{{if .PageIsAdminLicenseTiers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/license-tiers">
{{svg "octicon-key" 16}} License Tiers
</a>
+1 -1
View File
@@ -55,7 +55,7 @@
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
{{range $val := .PlatformOptions}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>