Compare commits

...

7 Commits

Author SHA1 Message Date
jmiller 1bf7313e40 chore: sync updates.xml 05.05.00 from main [skip ci] 2026-05-30 17:59:40 +00:00
Jonathan Miller 6c06384966 feat(updates): built-in Joomla update server endpoint
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
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m4s
Add GET /{owner}/{repo}/updates.xml that dynamically generates a
Joomla-compatible updates.xml from the repository's releases.

Features:
- Automatically maps release tags to channels (stable/rc/beta/alpha/dev)
- Finds .zip attachments for download URLs, falls back to archive URL
- Emits one entry per channel (latest release wins)
- Extracts version from tag names, strips common prefixes
- Publicly accessible (no auth required) for Joomla update clients

This is Phase 1 of #239 — the core dynamic update feed generation.
Future phases will add license key gating, Dolibarr support, and
repo settings UI.

Ref #239

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:54:31 -05:00
jmiller 4b6df79ae0 Merge pull request 'feat(admin): configurable default landing page from site administration' (#241) from feat/admin-landing-page into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Build & Release / Promote to RC (pull_request) Failing after 12s
PR RC Release / Build RC Release (pull_request) Successful in 29s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 8m23s
feat(admin): configurable default landing page (#240) (#241)
2026-05-30 17:18:16 +00:00
Jonathan Miller 1d1482a3dc feat(admin): configurable default landing page from site administration
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
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m6s
Add a dynamic config option to set the default landing page for
unauthenticated visitors from Site Administration > Settings. Options:
- Home (default explore page)
- Explore (repository explore)
- Organizations (org explore)
- Login (redirect to login)
- Custom path (any internal URL like /MokoConsulting)

The setting takes effect immediately without restart, using the same
dynamic config system as maintenance mode and web banner. Falls back
to the static LANDING_PAGE setting from app.ini if not configured.

Closes #240

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:17:40 -05:00
jmiller fba9c7eed9 chore: sync updates.xml 05.02.00 from main [skip ci] 2026-05-30 16:12:32 +00:00
jmiller 086c506a10 Merge pull request 'docs: update CHANGELOG for actions bot rebrand and deploy trigger' (#236) from chore/changelog-actions-bot into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
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
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
PR RC Release / Build RC Release (pull_request) Successful in 33s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 8m28s
docs: update CHANGELOG for actions bot rebrand and deploy trigger (#236)
2026-05-30 16:07:31 +00:00
jmiller d3bca854e5 chore: add .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-05-30 16:03:30 +00:00
11 changed files with 658 additions and 61 deletions
+236
View File
@@ -0,0 +1,236 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+1
View File
@@ -81,6 +81,7 @@ func initDefaultConfig() {
Instance: &InstanceStruct{
WebBanner: config.NewOption[WebBannerType]("instance.web_banner"),
MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"),
LandingPage: config.NewOption[LandingPageType]("instance.landing_page").WithFileConfig(config.CfgSecKey{Sec: "server", Key: "LANDING_PAGE"}),
},
}
}
+28
View File
@@ -52,7 +52,35 @@ func (m MaintenanceModeType) IsActive() bool {
return true
}
// LandingPageType configures the default page for unauthenticated visitors.
// Mode values: "home", "explore", "organizations", "login", or "custom".
// When Mode is "custom", CustomPath holds the redirect target (e.g. "/MokoConsulting").
type LandingPageType struct {
Mode string // home, explore, organizations, login, custom
CustomPath string // only used when Mode == "custom"
}
// URL returns the redirect path for the configured landing page.
func (lp LandingPageType) URL() string {
switch lp.Mode {
case "explore":
return "/explore"
case "organizations":
return "/explore/organizations"
case "login":
return "/user/login"
case "custom":
if lp.CustomPath != "" {
return lp.CustomPath
}
return "/"
default:
return "/"
}
}
type InstanceStruct struct {
WebBanner *config.Option[WebBannerType]
MaintenanceMode *config.Option[MaintenanceModeType]
LandingPage *config.Option[LandingPageType]
}
+8
View File
@@ -3328,6 +3328,14 @@
"admin.config.common.start_time": "Start time",
"admin.config.common.end_time": "End time",
"admin.config.common.skip_time_check": "Leave time empty (clear the field) to skip time check",
"admin.config.instance_landing_page": "Default Landing Page",
"admin.config.landing_page.home": "Home — default home page",
"admin.config.landing_page.explore": "Explore — repository explore page",
"admin.config.landing_page.organizations": "Organizations — organization explore page",
"admin.config.landing_page.login": "Login — redirect to login page",
"admin.config.landing_page.custom": "Custom path — redirect to a specific URL path",
"admin.config.landing_page.custom_path": "Custom path",
"admin.config.landing_page.custom_path_help": "Internal path to redirect unauthenticated visitors to (e.g. /MokoConsulting or /MokoConsulting/MokoGitea/wiki).",
"admin.config.instance_maintenance": "Instance Maintenance",
"admin.config.instance_maintenance_mode.admin_web_access_only": "Only allow admin to access the web UI",
"admin.config.instance_web_banner.enabled": "Show banner",
+12 -3
View File
@@ -48,9 +48,18 @@ func Home(ctx *context.Context) {
}
return
// Check non-logged users landing page.
} else if setting.LandingPageURL != setting.LandingPageHome {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
return
} else {
// Dynamic landing page from admin config takes priority.
landingPage := setting.Config().Instance.LandingPage.Value(ctx)
if landingPage.Mode != "" && landingPage.Mode != "home" {
ctx.Redirect(setting.AppSubURL + landingPage.URL())
return
}
// Fall back to static app.ini setting.
if setting.LandingPageURL != setting.LandingPageHome {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
return
}
}
// Check auto-login.
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"net/http"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/updateserver"
)
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
// from the repository's releases.
func ServeUpdatesXML(ctx *context.Context) {
xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GenerateJoomlaXML", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(xmlData)
}
+6
View File
@@ -1494,6 +1494,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
// end "/{username}/{reponame}": repo releases
// "/{username}/{reponame}": update server (Joomla-compatible updates.xml)
m.Group("/{username}/{reponame}", func() {
m.Get("/updates.xml", repo.ServeUpdatesXML)
}, optSignIn, context.RepoAssignment)
// end "/{username}/{reponame}": update server
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
}, optSignIn, context.RepoAssignment)
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updateserver
import (
"context"
"encoding/xml"
"fmt"
"strings"
"time"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// Joomla-compatible updates.xml structures for XML marshaling.
type xmlUpdates struct {
XMLName xml.Name `xml:"updates"`
Updates []xmlUpdate `xml:"update"`
}
type xmlUpdate struct {
Name string `xml:"name"`
Description string `xml:"description"`
Element string `xml:"element"`
Type string `xml:"type"`
Client string `xml:"client"`
Version string `xml:"version"`
CreationDate string `xml:"creationDate"`
InfoURL xmlInfoURL `xml:"infourl"`
Downloads xmlDownloads `xml:"downloads"`
SHA256 string `xml:"sha256,omitempty"`
Tags xmlTags `xml:"tags"`
ChangelogURL string `xml:"changelogurl,omitempty"`
Maintainer string `xml:"maintainer,omitempty"`
MaintainerURL string `xml:"maintainerurl,omitempty"`
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
}
type xmlInfoURL struct {
Title string `xml:"title,attr"`
URL string `xml:",chardata"`
}
type xmlDownloads struct {
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
}
type xmlDownloadURL struct {
Type string `xml:"type,attr"`
Format string `xml:"format,attr"`
URL string `xml:",chardata"`
}
type xmlTags struct {
Tag string `xml:"tag"`
}
type xmlTargetPlat struct {
Name string `xml:"name,attr"`
Version string `xml:"version,attr"`
}
// channelFromTag maps a release tag name to a Joomla update channel.
func channelFromTag(tagName string, isPrerelease bool) string {
lower := strings.ToLower(tagName)
switch {
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
return "dev"
case strings.Contains(lower, "-alpha") || strings.Contains(lower, "alpha"):
return "alpha"
case strings.Contains(lower, "-beta") || strings.Contains(lower, "beta"):
return "beta"
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
return "rc"
case isPrerelease:
return "rc"
default:
return "stable"
}
}
// 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.
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IncludeDrafts: false,
IncludeTags: false,
})
if err != nil {
return nil, fmt.Errorf("GetReleasesByRepoID: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
baseURL := setting.AppURL
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
element := strings.ToLower(repo.Name)
// Track best (latest) release per channel to emit one entry per channel.
bestByChannel := make(map[string]*repo_model.Release)
for _, rel := range releases {
if rel.IsDraft || rel.IsTag {
continue
}
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel
}
}
var updates xmlUpdates
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
rel, ok := bestByChannel[ch]
if !ok {
continue
}
// Load attachments for download URLs.
if err := rel.LoadAttributes(ctx); err != nil {
continue
}
// Find the first .zip attachment as the download URL.
var downloadURL string
for _, att := range rel.Attachments {
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
break
}
}
// Fall back to the release tag archive if no zip attachment.
if downloadURL == "" {
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
}
version := extractVersion(rel.TagName)
suffix := channelSuffix(ch)
if suffix != "" {
version = version + suffix
}
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),
Element: element,
Type: "component",
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),
},
Downloads: xmlDownloads{
DownloadURL: []xmlDownloadURL{
{Type: "full", Format: "zip", URL: downloadURL},
},
},
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),
TargetPlatform: xmlTargetPlat{
Name: "joomla",
Version: ".*",
},
}
updates.Updates = append(updates.Updates, u)
}
output, err := xml.MarshalIndent(updates, "", " ")
if err != nil {
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
}
return append([]byte(xml.Header), output...), nil
}
// extractVersion strips common tag prefixes (v, release-, etc.) to get the version.
func extractVersion(tagName string) string {
v := tagName
v = strings.TrimPrefix(v, "v")
v = strings.TrimPrefix(v, "release-")
v = strings.TrimPrefix(v, "release/")
// Strip channel suffixes to get base version.
for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
v = v[:idx]
break
}
}
return v
}
// channelSuffix returns the version suffix for a channel.
func channelSuffix(channel string) string {
switch channel {
case "dev":
return "-dev"
case "alpha":
return "-alpha"
case "beta":
return "-beta"
case "rc":
return "-rc"
default:
return ""
}
}
@@ -2,6 +2,7 @@
{{template "admin/config_settings/avatars" .}}
{{template "admin/config_settings/repository" .}}
{{template "admin/config_settings/landing_page" .}}
{{template "admin/config_settings/instance" .}}
{{template "admin/layout_footer" .}}
@@ -0,0 +1,49 @@
<h4 class="ui top attached header">{{ctx.Locale.Tr "admin.config.instance_landing_page"}}</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty system-config-form" method="post" action="{{AppSubUrl}}/-/admin/config">
{{$cfgOpt := $.SystemConfig.Instance.LandingPage}}
{{$cfgKey := $cfgOpt.DynKey}}
{{$landingPage := $cfgOpt.Value ctx}}
<input type="hidden" data-config-dyn-key="{{$cfgKey}}" data-config-value-json="{{JsonUtils.EncodeToString $landingPage}}">
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="home" {{if or (eq $landingPage.Mode "") (eq $landingPage.Mode "home")}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.home"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="explore" {{if eq $landingPage.Mode "explore"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.explore"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="organizations" {{if eq $landingPage.Mode "organizations"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.organizations"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="login" {{if eq $landingPage.Mode "login"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.login"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="custom" {{if eq $landingPage.Mode "custom"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.custom"}}</label>
</div>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.landing_page.custom_path"}}</label>
<input type="text" name="{{$cfgKey}}.CustomPath" value="{{$landingPage.CustomPath}}" placeholder="/MokoConsulting">
<div class="help">{{ctx.Locale.Tr "admin.config.landing_page.custom_path_help"}}</div>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
</div>
+68 -58
View File
@@ -1,93 +1,103 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 05.01.02
VERSION: 05.05.00
-->
<updates>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<description>MokoGitea dev build.</description>
<element>mokogitea</element>
<type>application</type>
<version>05.01.02</version>
<client>server</client>
<tags><tag>stable</tag></tags>
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.01.02</infourl>
<client>site</client>
<version>05.05.00-dev</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.01.02</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.05.00-dev.zip</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>dev</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<description>MokoGitea alpha build.</description>
<element>mokogitea</element>
<type>application</type>
<version>05.01.02</version>
<client>server</client>
<tags><tag>rc</tag></tags>
<infourl title="MokoGitea RC">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.01.02</infourl>
<client>site</client>
<version>05.05.00-alpha</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.01.02</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.05.00-alpha.zip</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<element>mokogitea</element>
<type>application</type>
<version>05.00.00</version>
<client>server</client>
<tags><tag>beta</tag></tags>
<infourl title="MokoGitea Beta">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<element>mokogitea</element>
<type>application</type>
<version>05.00.00</version>
<client>server</client>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>alpha</tag></tags>
<infourl title="MokoGitea Alpha">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.05.00.00</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.05.00.00</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<description>MokoGitea beta build.</description>
<element>mokogitea</element>
<type>application</type>
<version>06.00.00-dev</version>
<client>server</client>
<tags><tag>development</tag></tags>
<infourl title="MokoGitea Dev">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/dev</infourl>
<client>site</client>
<version>05.05.00-beta</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.06.00.00-dev</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.05.00-beta.zip</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>beta</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea rc build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00-rc</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.05.00-rc.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>rc</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea stable build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00</version>
<creationDate>2026-05-30</creationDate>
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.05.00.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*" />
</update>
</updates>