diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 727f661..6f184a2 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -164,6 +164,75 @@ jobs: echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY fi + - name: Update server & packaging checks + continue-on-error: true + run: | + echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + + # 1. Check exists and uses MokoGitea update server + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` tag — extension will not receive OTA updates" + echo "- **Missing** \`\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + SERVER_URL=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$SERVER_URL" ]; then + echo "::warning file=${MANIFEST}::\`\` is empty — no server URL defined" + echo "- **Empty** \`\` — no server URL defined" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then + echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}" + echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + # 2. Check tag exists + if ! grep -q '/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` tag — download ID authentication is not configured" + echo "- **Missing** \`\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + + # 3. For packages: check tag + if [ "$EXT_TYPE" = "package" ]; then + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Package is missing \`\` — child extensions will not be removed on uninstall" + echo "- **Missing** \`\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY + else + echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY + fi + - name: Check language files referenced in manifest run: | echo "### Language File Check" >> $GITHUB_STEP_SUMMARY @@ -647,6 +716,268 @@ jobs: echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY fi + - name: Script file reference check + run: | + echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + SCRIPT_FILE=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$SCRIPT_FILE" ]; then + echo "No \`\` referenced — skipping." >> $GITHUB_STEP_SUMMARY + elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then + echo "::error file=${MANIFEST}::Manifest references \`${SCRIPT_FILE}\` but file does not exist" + echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`\` but not found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Media folder validation + run: | + echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check tag and its folder/filename children + MEDIA_DEST=$(grep -oP ']*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + MEDIA_FOLDER=$(grep -oP ']*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + + if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then + echo "No \`\` tag found — skipping." >> $GITHUB_STEP_SUMMARY + else + if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then + echo "::error file=${MANIFEST}::\`\` references missing directory" + echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY + + # Check child references inside block + if [ -n "$MEDIA_FOLDER" ]; then + MEDIA_FOLDERS=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) + for F in $MEDIA_FOLDERS; do + if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then + echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + MEDIA_FILES=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) + for F in $MEDIA_FILES; do + if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then + echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Target platform check + continue-on-error: true + run: | + echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Check updates.xml for targetplatform if it exists + if [ -f "updates.xml" ]; then + if ! grep -q '/dev/null; then + echo "::warning file=updates.xml::No \`\` found — Joomla updater cannot filter by compatible version" + echo "- **Missing** \`\` in updates.xml" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + # Check manifest for minimum PHP/Joomla version hints + if ! grep -qP '|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest" + echo "- **Missing** version constraints (\`\` or \`\`)" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Changelog URL check + continue-on-error: true + run: | + echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` — Joomla updater will not display changelogs" + echo "- **Missing** \`\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + CHANGELOG_URL=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + echo "- \`\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Duplicate file references check + continue-on-error: true + run: | + echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Extract all and references + ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true) + if [ -z "$ALL_REFS" ]; then + echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY + else + DUPES=$(echo "$ALL_REFS" | uniq -d) + if [ -n "$DUPES" ]; then + while IFS= read -r DUP; do + COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP") + echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)" + echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + done <<< "$DUPES" + else + TOTAL=$(echo "$ALL_REFS" | wc -l) + echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY + else + echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Empty language keys check + continue-on-error: true + run: | + echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + TOTAL_FILES=0 + for FILE in $LANG_FILES; do + TOTAL_FILES=$((TOTAL_FILES + 1)) + # Find lines with KEY= but no value (empty or whitespace-only after =) + EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true) + if [ -n "$EMPTY_KEYS" ]; then + COUNT=$(echo "$EMPTY_KEYS" | wc -l) + echo "::warning file=${FILE}::${COUNT} empty language key(s)" + echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY + while IFS= read -r LINE; do + LINE_NUM=$(echo "$LINE" | cut -d: -f1) + KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1) + echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$EMPTY_KEYS" + WARNINGS=$((WARNINGS + COUNT)) + fi + done + + if [ "$WARNINGS" -eq 0 ]; then + echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + release-readiness: name: Release Readiness Check runs-on: ubuntu-latest diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml deleted file mode 100644 index 1af323c..0000000 --- a/.mokogitea/workflows/deploy-manual.yml +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API -# PATH: /templates/workflows/joomla/deploy-manual.yml.template -# VERSION: 04.07.00 -# BRIEF: Manual SFTP deploy to dev server for Joomla repos - -name: "Universal: Deploy to Dev (Manual)" - -on: - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all remote files before uploading' - required: false - default: 'false' - type: boolean - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: read - -jobs: - deploy: - name: SFTP Deploy to Dev - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - run: | - php -v && composer --version - - - name: Setup MokoStandards tools - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api 2>/dev/null || true - if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then - cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Check FTP configuration - id: check - env: - HOST: ${{ vars.DEV_FTP_HOST }} - PATH_VAR: ${{ vars.DEV_FTP_PATH }} - PORT: ${{ vars.DEV_FTP_PORT }} - run: | - if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then - echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "host=$HOST" >> "$GITHUB_OUTPUT" - - REMOTE="${PATH_VAR%/}" - echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" - - [ -z "$PORT" ] && PORT="22" - echo "port=$PORT" >> "$GITHUB_OUTPUT" - - - name: Deploy via SFTP - if: steps.check.outputs.skip != 'true' - env: - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; } - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ - > /tmp/sftp-config.json - - if [ -n "$SFTP_KEY" ]; then - echo "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json - fi - - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) - - PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" - else - php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - fi - - rm -f /tmp/deploy_key /tmp/sftp-config.json - - - name: Summary - if: always() - run: | - if [ "${{ steps.check.outputs.skip }}" = "true" ]; then - echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY - else - echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 11958bd..f094e97 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 01.00.39 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index efb3d1b..9d9f6f9 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -93,20 +93,8 @@ jobs: php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true php ${MOKO_CLI}/manifest_read.php --path . --github-output - - name: Check platform eligibility (Joomla only) - id: eligibility - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then - echo "proceed=true" >> "$GITHUB_OUTPUT" - else - echo "proceed=false" >> "$GITHUB_OUTPUT" - echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" - fi - - name: Resolve metadata and bump version id: meta - if: steps.eligibility.outputs.proceed == 'true' run: | # Auto-detect stability from branch name on push, or use input on dispatch if [ "${{ github.event_name }}" = "push" ]; then @@ -183,7 +171,6 @@ jobs: - name: Create release id: release - if: steps.eligibility.outputs.proceed == 'true' run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" @@ -194,7 +181,6 @@ jobs: --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - name: Update release notes from CHANGELOG.md - if: steps.eligibility.outputs.proceed == 'true' run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" @@ -231,7 +217,6 @@ jobs: - name: Build package and upload id: package - if: steps.eligibility.outputs.proceed == 'true' run: | VERSION="${{ steps.meta.outputs.version }}" TAG="${{ steps.meta.outputs.tag }}" @@ -245,7 +230,6 @@ jobs: # No need to build, commit, or sync updates.xml from workflows - name: "Delete lesser pre-release channels (cascade)" - if: steps.eligibility.outputs.proceed == 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" diff --git a/.mokogitea/workflows/workflow-sync-trigger.yml b/.mokogitea/workflows/workflow-sync-trigger.yml index 34891e8..97b8da2 100644 --- a/.mokogitea/workflows/workflow-sync-trigger.yml +++ b/.mokogitea/workflows/workflow-sync-trigger.yml @@ -47,6 +47,13 @@ jobs: echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" echo "Platform: ${PLATFORM:-all}" + - name: Setup PHP + 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 php-zip php-curl composer >/dev/null 2>&1 + fi + - name: Clone mokocli env: MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdd974..30e1f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,52 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.0] - Unreleased +## [1.2.0] - Unreleased + +### Added +- Multi-category support with parent/child hierarchy (#1) +- Categories admin CRUD — list, edit, color picker, custom marker icon +- Location-category junction table (many-to-many) +- Categories tab on location edit form (multi-select) +- Category filtering on site frontend (`catid` parameter) +- Custom map markers per category — SVG/PNG icon support (#2) +- Map module JOINs category data for marker icons and colors +- `access.xml` with full Joomla ACL permissions (#30) +- SQL update schema with `sql/updates/mysql/` versioned files (#31) +- REST API via Web Services plugin (`plg_webservices_mokosuitestorelocator`) (#29) +- API controller + JSON:API view for locations CRUD at `/api/v1/mokosuitestorelocator/locations` +- `LocationBridgeHelper` — static helper for cross-extension integration (#48) +- `LocationSavedEvent` — fires `onStoreLocatorLocationSaved` for cache invalidation +- Plugin added to package manifest +- Leaflet map on location detail page with marker and popup (#57) +- Leaflet.markercluster for automatic marker grouping at low zoom levels (#61) +- Clustering toggle parameter in map module settings (enabled by default) +- Junction table orphan cleanup on location/category delete (#60) +- License key warning on install/update when no download key is configured +- Download key (dlid) preserved across package upgrades + +### Changed +- Map module dispatcher uses aliased table queries with category JOIN +- ORDER BY clauses in admin and site models now validated against filter_fields allowlist +- Category filter uses EXISTS subquery instead of JOIN to avoid ONLY_FULL_GROUP_BY errors (#59) +- Inline ` +JS, [], ['position' => 'after'], ['leaflet']); +?> diff --git a/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini index fb04db5..3187c04 100644 --- a/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini +++ b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini @@ -2,6 +2,7 @@ ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GNU General Public License version 3 or later; see LICENSE +MOD_MOKOSUITESTORELOCATOR_SEARCH="MokoSuite Store Locator - Search" MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search" MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations." MOD_MOKOJOOMSTORELOCATOR_SEARCH_LABEL="Find a Store" diff --git a/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini index 5400537..27b42b5 100644 --- a/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini +++ b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini @@ -2,5 +2,6 @@ ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GNU General Public License version 3 or later; see LICENSE +MOD_MOKOSUITESTORELOCATOR_SEARCH="MokoSuite Store Locator - Search" MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search" MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations." diff --git a/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml b/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml index 5594d77..27b2c64 100644 --- a/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml +++ b/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml @@ -14,7 +14,7 @@ --> mod_mokosuitestorelocator_search - 1.0.0 + 01.00.39 2026-06-23 Moko Consulting hello@mokoconsulting.tech @@ -26,6 +26,7 @@ Moko\Module\MokoSuiteStoreLocatorSearch + services src tmpl language diff --git a/source/packages/mod_mokosuitestorelocator_search/services/provider.php b/source/packages/mod_mokosuitestorelocator_search/services/provider.php new file mode 100644 index 0000000..bd0e032 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_search/services/provider.php @@ -0,0 +1,25 @@ +registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoSuiteStoreLocatorSearch')); + $container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoSuiteStoreLocatorSearch\\Helper')); + $container->registerServiceProvider(new Module()); + } +}; diff --git a/source/packages/plg_webservices_mokosuitestorelocator/language/en-GB/plg_webservices_mokosuitestorelocator.ini b/source/packages/plg_webservices_mokosuitestorelocator/language/en-GB/plg_webservices_mokosuitestorelocator.ini new file mode 100644 index 0000000..fd78b4b --- /dev/null +++ b/source/packages/plg_webservices_mokosuitestorelocator/language/en-GB/plg_webservices_mokosuitestorelocator.ini @@ -0,0 +1,2 @@ +PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuite Store Locator - Web Services" +PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component." diff --git a/source/packages/plg_webservices_mokosuitestorelocator/language/en-GB/plg_webservices_mokosuitestorelocator.sys.ini b/source/packages/plg_webservices_mokosuitestorelocator/language/en-GB/plg_webservices_mokosuitestorelocator.sys.ini new file mode 100644 index 0000000..5ca667c --- /dev/null +++ b/source/packages/plg_webservices_mokosuitestorelocator/language/en-GB/plg_webservices_mokosuitestorelocator.sys.ini @@ -0,0 +1,6 @@ +; MokoSuiteStoreLocator Web Services Plugin - System language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuite Store Locator - Web Services" +PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component." diff --git a/source/packages/plg_webservices_mokosuitestorelocator/mokosuitestorelocator.xml b/source/packages/plg_webservices_mokosuitestorelocator/mokosuitestorelocator.xml new file mode 100644 index 0000000..9412d95 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitestorelocator/mokosuitestorelocator.xml @@ -0,0 +1,24 @@ + + + + plg_webservices_mokosuitestorelocator + 01.00.39 + 2026-06-24 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC + + Moko\Plugin\WebServices\MokoSuiteStoreLocator + + + services + src + language + + diff --git a/source/packages/plg_webservices_mokosuitestorelocator/services/provider.php b/source/packages/plg_webservices_mokosuitestorelocator/services/provider.php new file mode 100644 index 0000000..85cca5e --- /dev/null +++ b/source/packages/plg_webservices_mokosuitestorelocator/services/provider.php @@ -0,0 +1,37 @@ +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new MokoSuiteStoreLocator( + $dispatcher, + (array) PluginHelper::getPlugin('webservices', 'mokosuitestorelocator') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_webservices_mokosuitestorelocator/src/Extension/MokoSuiteStoreLocator.php b/source/packages/plg_webservices_mokosuitestorelocator/src/Extension/MokoSuiteStoreLocator.php new file mode 100644 index 0000000..710c9f0 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitestorelocator/src/Extension/MokoSuiteStoreLocator.php @@ -0,0 +1,57 @@ + 'onBeforeApiRoute', + ]; + } + + /** + * Register API routes. + * + * @param \Joomla\CMS\Event\ApiRouterEvent $event The event. + * + * @return void + * + * @since 1.2.0 + */ + public function onBeforeApiRoute($event): void + { + $router = $event->getArgument('router') ?? $event->getRouter(); + + $router->createCRUDRoutes( + 'v1/mokosuitestorelocator/locations', + 'locations', + ['component' => 'com_mokosuitestorelocator'] + ); + } +} diff --git a/source/pkg_mokosuitestorelocator.xml b/source/pkg_mokosuitestorelocator.xml index a7d7569..52e6bfb 100644 --- a/source/pkg_mokosuitestorelocator.xml +++ b/source/pkg_mokosuitestorelocator.xml @@ -16,27 +16,28 @@ ========================================================================= --> - pkg_mokosuitestorelocator + MokoSuite Store Locator mokosuitestorelocator - 1.0.0 + 01.00.39 2026-06-23 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GNU General Public License version 3 or later; see LICENSE - PKG_MOKOJOOMSTORELOCATOR_DESC + MokoSuiteStoreLocator — store locator component with interactive map and search modules for Joomla 5/6. + script.php com_mokosuitestorelocator.zip mod_mokosuitestorelocator_map.zip mod_mokosuitestorelocator_search.zip + plg_webservices_mokosuitestorelocator.zip - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/updates.xml + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/updates.xml - true diff --git a/source/script.php b/source/script.php index 0a77084..1f4058e 100644 --- a/source/script.php +++ b/source/script.php @@ -69,6 +69,8 @@ class Pkg_MokosuitestorelocatorInstallerScript implements InstallerScriptInterfa return false; } + $this->saveDownloadKey(); + return true; } @@ -126,6 +128,117 @@ class Pkg_MokosuitestorelocatorInstallerScript implements InstallerScriptInterfa */ public function postflight(string $type, InstallerAdapter $parent): bool { + $this->restoreDownloadKey(); + $this->warnMissingLicenseKey(); + return true; } + + private ?string $savedDownloadKey = null; + + private function saveDownloadKey(): void + { + try + { + $db = \Joomla\CMS\Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('us.extra_query')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') + ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitestorelocator')) + ->setLimit(1) + ); + $key = $db->loadResult(); + + if (!empty($key)) + { + $this->savedDownloadKey = $key; + } + } + catch (\Throwable $e) {} + } + + private function restoreDownloadKey(): void + { + if ($this->savedDownloadKey === null) + { + return; + } + + try + { + $db = \Joomla\CMS\Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('us.update_site_id')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') + ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitestorelocator')) + ->setLimit(1) + ); + $siteId = (int) $db->loadResult(); + + if ($siteId > 0) + { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey)) + ->where($db->quoteName('update_site_id') . ' = ' . $siteId) + )->execute(); + } + } + catch (\Throwable $e) {} + } + + private function warnMissingLicenseKey(): void + { + try + { + $db = \Joomla\CMS\Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) + ->from($db->quoteName('#__update_sites')) + ->where( + '(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteStoreLocator%') + . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteStoreLocator%') . ')' + ) + ->setLimit(1) + ); + $site = $db->loadObject(); + + if ($site) + { + $eq = (string) ($site->extra_query ?? ''); + + if (!empty($eq) && strpos($eq, 'dlid=') !== false) + { + parse_str($eq, $p); + + if (!empty($p['dlid'])) + { + return; + } + } + + $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; + } + else + { + $editUrl = 'index.php?option=com_installer&view=updatesites'; + } + + \Joomla\CMS\Factory::getApplication()->enqueueMessage( + 'Moko Consulting License Key Required — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . 'Enter License Key', + 'warning' + ); + } + catch (\Throwable $e) {} + } }