Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b1278e7e2 | |||
| 7756ba2c2c | |||
| b115fa324b |
@@ -1,236 +0,0 @@
|
||||
# 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
|
||||
@@ -81,7 +81,6 @@ 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"}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,35 +52,7 @@ 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]
|
||||
}
|
||||
|
||||
@@ -3328,14 +3328,6 @@
|
||||
"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",
|
||||
|
||||
+3
-12
@@ -48,18 +48,9 @@ func Home(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
// Check non-logged users landing page.
|
||||
} 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
|
||||
}
|
||||
} else if setting.LandingPageURL != setting.LandingPageHome {
|
||||
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
|
||||
return
|
||||
}
|
||||
|
||||
// Check auto-login.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1494,12 +1494,6 @@ 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)
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
// 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,7 +2,6 @@
|
||||
|
||||
{{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" .}}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<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>
|
||||
+31
-31
@@ -1,23 +1,23 @@
|
||||
<?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.05.00
|
||||
VERSION: 05.04.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea dev build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea dev build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-dev</version>
|
||||
<version>05.04.00-dev</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.05.00-dev.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.04.00-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>dev</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -25,18 +25,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea alpha build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea alpha build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-alpha</version>
|
||||
<version>05.04.00-alpha</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.05.00-alpha.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.04.00-alpha.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -44,18 +44,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea beta build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea beta build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-beta</version>
|
||||
<version>05.04.00-beta</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.05.00-beta.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.04.00-beta.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>beta</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -63,18 +63,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea rc build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea rc build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-rc</version>
|
||||
<version>05.04.00-rc</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
|
||||
<infourl title="Application - 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>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.04.00-rc.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -82,18 +82,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea stable build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea stable build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00</version>
|
||||
<version>05.04.00</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<infourl title='Application - 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>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.04.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user