diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 2325032b83..44a2d64a11 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -102,13 +102,14 @@ jobs: run: | php /tmp/moko-platform-api/cli/release_publish.php \ --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream - name: Summary if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: @@ -167,7 +168,8 @@ jobs: run: | php /tmp/moko-platform-api/cli/release_publish.php \ --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 0ac0ef1188..4d78d7a445 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -147,6 +147,98 @@ jobs: echo "PHP lint: ${ERRORS} error(s)" [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + echo "::error file=${file}::Missing JEXEC guard: ${file}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) + if [ "$ERRORS" -gt 0 ]; then + echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" + echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in src/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + - name: Validate platform manifest run: | PLATFORM="${{ steps.platform.outputs.platform }}" @@ -164,6 +256,13 @@ jobs: for ELEMENT in name version description; do grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi echo "Joomla manifest valid" ;; dolibarr) @@ -196,6 +295,138 @@ jobs: ;; esac + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + - name: Check changelog has unreleased entry run: | if [ ! -f "CHANGELOG.md" ]; then diff --git a/services/updateserver/joomla.go b/services/updateserver/joomla.go index 9d8e7fad5b..0d7ffd3bec 100644 --- a/services/updateserver/joomla.go +++ b/services/updateserver/joomla.go @@ -37,7 +37,7 @@ type xmlUpdate struct { TargetPlatform xmlTargetPlat `xml:"targetplatform"` SHA256 string `xml:"sha256,omitempty"` SHA512 string `xml:"sha512,omitempty"` - Client string `xml:"client"` + Client string `xml:"client,omitempty"` PHPMinimum string `xml:"php_minimum,omitempty"` Description string `xml:"description,omitempty"` CreationDate string `xml:"creationDate,omitempty"` @@ -286,7 +286,15 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName) } - version := extractVersion(rel.TagName) + // Extract version from the asset filename first (most accurate), + // then fall back to tag name, then release title. + version := "" + if zipName != "" { + version = extractVersion(zipName) + } + if version == "" { + version = extractVersion(rel.TagName) + } // If the tag is a stream name (not a version), try the release title instead. if version == "" || isStreamName(rel.TagName, streams) { version = extractVersion(rel.Title) @@ -313,12 +321,19 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require infoURL = cfg.InfoURL } + // Joomla element: only relevant for plugins/modules (site vs administrator). + // Packages manage their own sub-extension clients; omit for package type. + client := "site" + if extType == "package" { + client = "" + } + u := xmlUpdate{ Name: displayName, Description: desc, Element: element, Type: extType, - Client: "site", + Client: client, Version: version, CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"), InfoURL: xmlInfoURL{ diff --git a/updates.xml b/updates.xml deleted file mode 100644 index 195a5e304c..0000000000 --- a/updates.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - MokoGitea - MokoGitea dev build. - mokogitea - application - site - 05.05.00-dev - 2026-05-30 - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development - - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.05.00-dev.zip - - 4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e - dev - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - - - MokoGitea - MokoGitea alpha build. - mokogitea - application - site - 05.05.00-alpha - 2026-05-30 - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha - - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.05.00-alpha.zip - - 4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e - alpha - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - - - MokoGitea - MokoGitea beta build. - mokogitea - application - site - 05.05.00-beta - 2026-05-30 - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta - - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.05.00-beta.zip - - 4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e - beta - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - - - MokoGitea - MokoGitea rc build. - mokogitea - application - site - 05.05.00-rc - 2026-05-30 - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate - - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.05.00-rc.zip - - 4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e - rc - https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - - - MokoGitea - MokoGitea stable build. - mokogitea - application - site - 05.28.00 - 2026-06-04 - https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable - - https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.28.00.zip - - 302a36af1eb50d87977f85fa566b9e83fb95fb0b0cf48fd8c652c341a42fef72 - stable - https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - -