Fix PHP CodeSniffer dependency conflict, add dev tools, implement platform-aware build system, and prepare dual-repository CI/CD migration #33

Merged
Copilot merged 10 commits from copilot/fix-composer-dependency-issue into main 2026-01-05 07:56:30 +00:00
13 changed files with 4655 additions and 50 deletions

View File

@@ -50,8 +50,8 @@ jobs:
- name: Install PHP_CodeSniffer
run: |
composer global require squizlabs/php_codesniffer
composer global require phpcompatibility/php-compatibility
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpcompatibility/php-compatibility:^9.0" --with-all-dependencies
# Register PHPCompatibility standard
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
@@ -104,8 +104,8 @@ jobs:
- name: Install PHPStan
run: |
composer global require phpstan/phpstan
composer global require phpstan/extension-installer
composer global require phpstan/phpstan "^1.0" --with-all-dependencies
composer global require phpstan/extension-installer "^1.0" --with-all-dependencies
- name: Run PHPStan
run: |
@@ -151,8 +151,8 @@ jobs:
- name: Install dependencies
run: |
composer global require squizlabs/php_codesniffer
composer global require phpcompatibility/php-compatibility
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpcompatibility/php-compatibility:^9.0" --with-all-dependencies
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
- name: Check PHP 8.0+ Compatibility

View File

@@ -643,7 +643,7 @@ else:
echo "```"
} >> "${GITHUB_STEP_SUMMARY}"
- name: Build Joomla ZIP (extension type aware, src-only archive)
- name: Build Joomla/Dolibarr ZIP (platform-aware, src-only archive)
id: build
run: |
set -euo pipefail
@@ -657,44 +657,43 @@ else:
DIST_DIR="${GITHUB_WORKSPACE}/dist"
mkdir -p "${DIST_DIR}"
MANIFEST=""
if [ -f "src/templateDetails.xml" ]; then
MANIFEST="src/templateDetails.xml"
elif find src -maxdepth 4 -type f -name 'templateDetails.xml' | head -n 1 | grep -q .; then
MANIFEST="$(find src -maxdepth 4 -type f -name 'templateDetails.xml' | head -n 1)"
elif find src -maxdepth 4 -type f -name 'pkg_*.xml' | head -n 1 | grep -q .; then
MANIFEST="$(find src -maxdepth 4 -type f -name 'pkg_*.xml' | head -n 1)"
elif find src -maxdepth 4 -type f -name 'com_*.xml' | head -n 1 | grep -q .; then
MANIFEST="$(find src -maxdepth 4 -type f -name 'com_*.xml' | head -n 1)"
elif find src -maxdepth 4 -type f -name 'mod_*.xml' | head -n 1 | grep -q .; then
MANIFEST="$(find src -maxdepth 4 -type f -name 'mod_*.xml' | head -n 1)"
elif find src -maxdepth 6 -type f -name 'plg_*.xml' | head -n 1 | grep -q .; then
MANIFEST="$(find src -maxdepth 6 -type f -name 'plg_*.xml' | head -n 1)"
else
MANIFEST="$(grep -Rsl --include='*.xml' '<extension' src | head -n 1 || true)"
fi
if [ -z "${MANIFEST}" ]; then
echo "ERROR: No Joomla manifest XML found under src" >> "${GITHUB_STEP_SUMMARY}"
# Detect platform and extension type using dedicated script
if ! PLATFORM_INFO=$(python3 "${GITHUB_WORKSPACE}/scripts/release/detect_platform.py" "${GITHUB_WORKSPACE}/src"); then
echo "ERROR: Could not detect extension platform and type" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
EXT_TYPE="$(grep -Eo 'type="[^"]+"' "${MANIFEST}" | head -n 1 | cut -d '"' -f2 || true)"
if [ -z "${EXT_TYPE}" ]; then
EXT_TYPE="unknown"
if [ -z "${PLATFORM_INFO}" ]; then
echo "ERROR: Platform detection returned empty result" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
PLATFORM="${PLATFORM_INFO%%|*}"
EXT_TYPE="${PLATFORM_INFO##*|}"
ZIP="${REPO_NAME}-${VERSION}-${CHANNEL}-${EXT_TYPE}.zip"
ZIP="${REPO_NAME}-${VERSION}-${CHANNEL}-${PLATFORM}-${EXT_TYPE}.zip"
# Create ZIP with development artifact exclusions
zip -r -X "${DIST_DIR}/${ZIP}" src \
-x "src/**/.git/**" \
-x "src/**/.github/**" \
-x "src/**/.DS_Store" \
-x "src/**/__MACOSX/**"
-x "src/**/__MACOSX/**" \
-x "src/**/node_modules/**" \
-x "src/**/vendor/**" \
-x "src/**/tests/**" \
-x "src/**/Tests/**" \
-x "src/**/.phpstan.cache/**" \
-x "src/**/.psalm/**" \
-x "src/**/.rector/**" \
-x "src/**/phpmd-cache/**" \
-x "src/**/.php-cs-fixer.cache" \
-x "src/**/.phplint-cache" \
-x "src/**/*.log"
echo "zip_name=${ZIP}" >> "${GITHUB_OUTPUT}"
echo "dist_dir=${DIST_DIR}" >> "${GITHUB_OUTPUT}"
echo "manifest=${MANIFEST}" >> "${GITHUB_OUTPUT}"
echo "platform=${PLATFORM}" >> "${GITHUB_OUTPUT}"
echo "ext_type=${EXT_TYPE}" >> "${GITHUB_OUTPUT}"
ZIP_BYTES="$(stat -c%s "${DIST_DIR}/${ZIP}")"
@@ -702,7 +701,7 @@ else:
{
echo "### Build report"
echo "```json"
echo "{\"repository\":\"${GITHUB_REPOSITORY}\",\"workflow\":\"${GITHUB_WORKFLOW}\",\"job\":\"${GITHUB_JOB}\",\"run/id\":${GITHUB_RUN_ID},\"run/number\":${GITHUB_RUN_NUMBER},\"run/attempt\":${GITHUB_RUN_ATTEMPT},\"run/url\":\"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\",\"actor\":\"${GITHUB_ACTOR}\",\"sha\":\"${GITHUB_SHA}\",\"archive_policy\":\"src_only\",\"manifest\":\"${MANIFEST}\",\"extension_type\":\"${EXT_TYPE}\",\"zip\":\"${DIST_DIR}/${ZIP}\",\"zip_bytes\":${ZIP_BYTES}}"
echo "{\"repository\":\"${GITHUB_REPOSITORY}\",\"workflow\":\"${GITHUB_WORKFLOW}\",\"job\":\"${GITHUB_JOB}\",\"run/id\":${GITHUB_RUN_ID},\"run/number\":${GITHUB_RUN_NUMBER},\"run/attempt\":${GITHUB_RUN_ATTEMPT},\"run/url\":\"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\",\"actor\":\"${GITHUB_ACTOR}\",\"sha\":\"${GITHUB_SHA}\",\"archive_policy\":\"src_only\",\"platform\":\"${PLATFORM}\",\"extension_type\":\"${EXT_TYPE}\",\"zip\":\"${DIST_DIR}/${ZIP}\",\"zip_bytes\":${ZIP_BYTES}}"
echo "```"
} >> "${GITHUB_STEP_SUMMARY}"

12
.gitignore vendored
View File

@@ -793,6 +793,14 @@ package-lock.json
.phpunit.result.cache
codeception.phar
# Development tool artifacts
.phpstan.cache
.psalm/
.rector/
phpmd-cache/
.php-cs-fixer.cache
.phplint-cache
# Python
__pycache__/
*.py[cod]
@@ -804,8 +812,8 @@ develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
/lib/
/lib64/
parts/
sdist/
var/

View File

@@ -24,10 +24,13 @@ help:
install:
@echo "Installing development dependencies..."
@command -v composer >/dev/null 2>&1 || { echo "Error: composer not found. Please install composer first."; exit 1; }
composer global require squizlabs/php_codesniffer
composer global require phpstan/phpstan
composer global require phpcompatibility/php-compatibility
composer global require codeception/codeception
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpstan/phpstan:^1.0" --with-all-dependencies
composer global require "phpcompatibility/php-compatibility:^9.0" --with-all-dependencies
composer global require "codeception/codeception" --with-all-dependencies
composer global require "vimeo/psalm:^5.0" --with-all-dependencies
composer global require "phpmd/phpmd:^2.0" --with-all-dependencies
composer global require "friendsofphp/php-cs-fixer:^3.0" --with-all-dependencies
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
@echo "✓ Dependencies installed"
@@ -93,6 +96,38 @@ phpcompat:
@command -v phpcs >/dev/null 2>&1 || { echo "Error: phpcs not found. Run 'make install' first."; exit 1; }
phpcs --standard=PHPCompatibility --runtime-set testVersion 8.0- src/
## psalm: Run Psalm static analysis
psalm:
@echo "Running Psalm static analysis..."
@command -v psalm >/dev/null 2>&1 || { echo "Error: psalm not found. Run 'make install' first."; exit 1; }
psalm --show-info=false
## phpmd: Run PHP Mess Detector
phpmd:
@echo "Running PHP Mess Detector..."
@command -v phpmd >/dev/null 2>&1 || { echo "Error: phpmd not found. Run 'make install' first."; exit 1; }
phpmd src/ text cleancode,codesize,controversial,design,naming,unusedcode
## php-cs-fixer: Run PHP-CS-Fixer
php-cs-fixer:
@echo "Running PHP-CS-Fixer..."
@command -v php-cs-fixer >/dev/null 2>&1 || { echo "Error: php-cs-fixer not found. Run 'make install' first."; exit 1; }
php-cs-fixer fix --dry-run --diff src/
## php-cs-fixer-fix: Auto-fix with PHP-CS-Fixer
php-cs-fixer-fix:
@echo "Auto-fixing with PHP-CS-Fixer..."
@command -v php-cs-fixer >/dev/null 2>&1 || { echo "Error: php-cs-fixer not found. Run 'make install' first."; exit 1; }
php-cs-fixer fix src/
## quality-extended: Run extended quality checks (includes psalm, phpmd)
quality-extended:
@echo "Running extended code quality checks..."
@$(MAKE) quality
@$(MAKE) psalm
@$(MAKE) phpmd
@echo "✓ All quality checks passed"
## package: Create distribution package
package:
@echo "Creating distribution package..."
@@ -128,6 +163,12 @@ clean:
@rm -rf dist/
@rm -rf tests/_output/
@rm -rf .phpunit.cache/
@rm -rf .phpstan.cache/
@rm -rf .psalm/
@rm -rf .rector/
@rm -rf phpmd-cache/
@find . -type f -name ".php-cs-fixer.cache" -delete
@find . -type f -name ".phplint-cache" -delete
@find . -type f -name "*.log" -delete
@find . -type f -name ".DS_Store" -delete
@echo "✓ Cleaned"

1233
docs/CI_MIGRATION_PLAN.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -128,7 +128,7 @@ The repository is configured with Codeception for acceptance and unit testing.
1. Install Codeception:
```bash
composer global require codeception/codeception
composer global require "codeception/codeception" --with-all-dependencies
```
2. Run tests:
@@ -233,9 +233,9 @@ phpcbf --standard=phpcs.xml
1. Install tools:
```bash
composer global require squizlabs/php_codesniffer
composer global require phpstan/phpstan
composer global require phpcompatibility/php-compatibility
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpstan/phpstan:^1.0" --with-all-dependencies
composer global require "phpcompatibility/php-compatibility:^9.0" --with-all-dependencies
```
2. Configure PHPCompatibility:
@@ -323,7 +323,7 @@ After deployment to Joomla:
**Issue: PHP_CodeSniffer not found**
```bash
composer global require squizlabs/php_codesniffer
composer global require "squizlabs/php_codesniffer:^3.0"
export PATH="$PATH:$HOME/.composer/vendor/bin"
```

1413
docs/MIGRATION_CHECKLIST.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -28,9 +28,9 @@ cd moko-cassiopeia
make dev-setup
# Or manually
composer global require squizlabs/php_codesniffer
composer global require "squizlabs/php_codesniffer:^3.0"
composer global require phpstan/phpstan
composer global require phpcompatibility/php-compatibility
composer global require "phpcompatibility/php-compatibility:^9.0"
composer global require codeception/codeception
```
@@ -243,7 +243,7 @@ chmod +x scripts/**/*.sh
```bash
make install
# Or manually:
composer global require squizlabs/php_codesniffer phpstan/phpstan
composer global require "squizlabs/php_codesniffer:^3.0" phpstan/phpstan
```
### Pre-commit Hook Fails

1452
docs/REUSABLE_WORKFLOWS.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -86,7 +86,8 @@ gh run view <run-id> --log
**How to run locally:**
```bash
# Install tools
composer global require squizlabs/php_codesniffer phpstan/phpstan
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpstan/phpstan:^1.0" --with-all-dependencies
# Run checks
phpcs --standard=phpcs.xml src/

View File

@@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""
Extension utilities for Joomla and Dolibarr.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (./LICENSE.md).
FILE INFORMATION
DEFGROUP: Script.Library
INGROUP: Extension.Utils
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
PATH: /scripts/lib/extension_utils.py
VERSION: 01.00.00
BRIEF: Platform-aware extension utilities for Joomla and Dolibarr
"""
import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Optional, Union
class Platform(Enum):
"""Supported extension platforms."""
JOOMLA = "joomla"
DOLIBARR = "dolibarr"
UNKNOWN = "unknown"
@dataclass
class ExtensionInfo:
"""Extension information."""
platform: Platform
name: str
version: str
extension_type: str
manifest_path: Path
description: Optional[str] = None
author: Optional[str] = None
author_email: Optional[str] = None
license: Optional[str] = None
def detect_joomla_manifest(src_dir: Union[str, Path]) -> Optional[Path]:
"""
Detect Joomla manifest file.
Args:
src_dir: Source directory
Returns:
Path to manifest file or None
"""
src_path = Path(src_dir)
# Common Joomla manifest locations and patterns
manifest_patterns = [
"templateDetails.xml",
"pkg_*.xml",
"com_*.xml",
"mod_*.xml",
"plg_*.xml",
]
# Search in src_dir and subdirectories (max depth 4)
for pattern in manifest_patterns:
# Direct match
matches = list(src_path.glob(pattern))
if matches:
return matches[0]
# Search in subdirectories
matches = list(src_path.glob(f"*/{pattern}"))
if matches:
return matches[0]
matches = list(src_path.glob(f"*/*/{pattern}"))
if matches:
return matches[0]
# Fallback: search for any XML with <extension tag
for xml_file in src_path.rglob("*.xml"):
if xml_file.name.startswith("."):
continue
try:
tree = ET.parse(xml_file)
root = tree.getroot()
copilot-pull-request-reviewer[bot] commented 2026-01-05 08:01:59 +00:00 (Migrated from github.com)
Review

There's a potential security issue with XML parsing. The code uses ET.parse() without protection against XML bombs or XXE attacks. While this is parsing manifest files from the repository (not external user input), it's good practice to use defusedxml library or configure the parser with secure defaults, especially since this code will be used in CI/CD pipelines where supply chain attacks could inject malicious XML.

There's a potential security issue with XML parsing. The code uses `ET.parse()` without protection against XML bombs or XXE attacks. While this is parsing manifest files from the repository (not external user input), it's good practice to use `defusedxml` library or configure the parser with secure defaults, especially since this code will be used in CI/CD pipelines where supply chain attacks could inject malicious XML.
if root.tag == "extension":
return xml_file
except Exception:
continue
copilot-pull-request-reviewer[bot] commented 2026-01-05 08:01:59 +00:00 (Migrated from github.com)
Review

The error handling catches all exceptions with a bare except Exception: without logging what went wrong. This will make debugging difficult when XML parsing fails. Consider logging the exception details to stderr before continuing to the next file.

The error handling catches all exceptions with a bare `except Exception:` without logging what went wrong. This will make debugging difficult when XML parsing fails. Consider logging the exception details to stderr before continuing to the next file.
return None
def detect_dolibarr_manifest(src_dir: Union[str, Path]) -> Optional[Path]:
"""
Detect Dolibarr module descriptor file.
Args:
src_dir: Source directory
Returns:
Path to descriptor file or None
"""
src_path = Path(src_dir)
# Dolibarr module descriptors follow pattern: core/modules/mod*.class.php
descriptor_patterns = [
"core/modules/mod*.class.php",
"*/modules/mod*.class.php",
"mod*.class.php",
]
for pattern in descriptor_patterns:
matches = list(src_path.glob(pattern))
if matches:
# Verify it's actually a Dolibarr module descriptor
# Look for extends DolibarrModules pattern
for match in matches:
try:
content = match.read_text(encoding="utf-8")
# Check for Dolibarr module inheritance pattern
if re.search(r'extends\s+DolibarrModules', content):
return match
except Exception:
continue
copilot-pull-request-reviewer[bot] commented 2026-01-05 08:02:00 +00:00 (Migrated from github.com)
Review

Same issue as line 108-109: bare exception handler without logging. When file reading or regex matching fails, the error is silently swallowed, making it difficult to diagnose issues with Dolibarr module detection.

Same issue as line 108-109: bare exception handler without logging. When file reading or regex matching fails, the error is silently swallowed, making it difficult to diagnose issues with Dolibarr module detection.
return None
def parse_joomla_manifest(manifest_path: Path) -> Optional[ExtensionInfo]:
"""
Parse Joomla manifest XML.
Args:
manifest_path: Path to manifest file
Returns:
ExtensionInfo or None
"""
try:
tree = ET.parse(manifest_path)
root = tree.getroot()
if root.tag != "extension":
return None
# Get extension type
ext_type = root.get("type", "unknown")
# Get name
name_elem = root.find("name")
name = name_elem.text if name_elem is not None else "unknown"
# Get version
version_elem = root.find("version")
version = version_elem.text if version_elem is not None else "0.0.0"
# Get description
desc_elem = root.find("description")
description = desc_elem.text if desc_elem is not None else None
# Get author
author_elem = root.find("author")
author = author_elem.text if author_elem is not None else None
# Get author email
author_email_elem = root.find("authorEmail")
author_email = author_email_elem.text if author_email_elem is not None else None
# Get license
license_elem = root.find("license")
license_text = license_elem.text if license_elem is not None else None
return ExtensionInfo(
platform=Platform.JOOMLA,
name=name,
version=version,
extension_type=ext_type,
manifest_path=manifest_path,
description=description,
author=author,
author_email=author_email,
license=license_text
)
except Exception:
return None
copilot-pull-request-reviewer[bot] commented 2026-01-05 08:02:00 +00:00 (Migrated from github.com)
Review

Same issue with bare exception handler without logging. When parsing Joomla manifests fails (e.g., due to malformed XML), the function silently returns None without any indication of what went wrong.

Same issue with bare exception handler without logging. When parsing Joomla manifests fails (e.g., due to malformed XML), the function silently returns None without any indication of what went wrong.
def parse_dolibarr_descriptor(descriptor_path: Path) -> Optional[ExtensionInfo]:
"""
Parse Dolibarr module descriptor PHP file.
Args:
descriptor_path: Path to descriptor file
Returns:
ExtensionInfo or None
"""
try:
content = descriptor_path.read_text(encoding="utf-8")
# Extract module name from class that extends DolibarrModules
# Pattern: class ModMyModule extends DolibarrModules
class_match = re.search(r'class\s+(\w+)\s+extends\s+DolibarrModules', content)
if not class_match:
# Fallback: try to find any class definition
class_match = re.search(r'class\s+(\w+)', content)
name = class_match.group(1) if class_match else "unknown"
# Extract version
version_match = re.search(r'\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]', content)
version = version_match.group(1) if version_match else "0.0.0"
# Extract description
desc_match = re.search(r'\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]', content)
description = desc_match.group(1) if desc_match else None
# Extract author
author_match = re.search(r'\$this->editor_name\s*=\s*[\'"]([^\'"]+)[\'"]', content)
copilot-pull-request-reviewer[bot] commented 2026-01-05 08:02:00 +00:00 (Migrated from github.com)
Review

The regex patterns for extracting version, description, and author from Dolibarr PHP files may not handle multiline strings or strings with escaped quotes correctly. For example, r'\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]' will fail if the version string contains a quote character. Consider using more robust parsing or handling edge cases.

        version_match = re.search(
            r'\$this->version\s*=\s*[\'"]((?:\\.|[^\'"])*)[\'"]',
            content,
            re.DOTALL,
        )
        version = version_match.group(1) if version_match else "0.0.0"
        
        # Extract description
        desc_match = re.search(
            r'\$this->description\s*=\s*[\'"]((?:\\.|[^\'"])*)[\'"]',
            content,
            re.DOTALL,
        )
        description = desc_match.group(1) if desc_match else None
        
        # Extract author
        author_match = re.search(
            r'\$this->editor_name\s*=\s*[\'"]((?:\\.|[^\'"])*)[\'"]',
            content,
            re.DOTALL,
        )
The regex patterns for extracting version, description, and author from Dolibarr PHP files may not handle multiline strings or strings with escaped quotes correctly. For example, `r'\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]'` will fail if the version string contains a quote character. Consider using more robust parsing or handling edge cases. ```suggestion version_match = re.search( r'\$this->version\s*=\s*[\'"]((?:\\.|[^\'"])*)[\'"]', content, re.DOTALL, ) version = version_match.group(1) if version_match else "0.0.0" # Extract description desc_match = re.search( r'\$this->description\s*=\s*[\'"]((?:\\.|[^\'"])*)[\'"]', content, re.DOTALL, ) description = desc_match.group(1) if desc_match else None # Extract author author_match = re.search( r'\$this->editor_name\s*=\s*[\'"]((?:\\.|[^\'"])*)[\'"]', content, re.DOTALL, ) ```
author = author_match.group(1) if author_match else None
return ExtensionInfo(
platform=Platform.DOLIBARR,
name=name,
version=version,
extension_type="module",
manifest_path=descriptor_path,
description=description,
author=author,
author_email=None,
license=None
)
except Exception:
return None
copilot-pull-request-reviewer[bot] commented 2026-01-05 08:02:00 +00:00 (Migrated from github.com)
Review

Same bare exception handler issue - parsing failures are silently ignored without logging.

Same bare exception handler issue - parsing failures are silently ignored without logging.
def get_extension_info(src_dir: Union[str, Path]) -> Optional[ExtensionInfo]:
"""
Detect and parse extension information from source directory.
Supports both Joomla and Dolibarr platforms.
Args:
src_dir: Source directory containing extension files
Returns:
ExtensionInfo or None if not detected
"""
src_path = Path(src_dir)
if not src_path.is_dir():
return None
# Try Joomla first
joomla_manifest = detect_joomla_manifest(src_path)
if joomla_manifest:
ext_info = parse_joomla_manifest(joomla_manifest)
if ext_info:
return ext_info
# Try Dolibarr
dolibarr_descriptor = detect_dolibarr_manifest(src_path)
if dolibarr_descriptor:
ext_info = parse_dolibarr_descriptor(dolibarr_descriptor)
if ext_info:
return ext_info
return None
def is_joomla_extension(src_dir: Union[str, Path]) -> bool:
"""
Check if directory contains a Joomla extension.
Args:
src_dir: Source directory
Returns:
True if Joomla extension detected
"""
ext_info = get_extension_info(src_dir)
return ext_info is not None and ext_info.platform == Platform.JOOMLA
def is_dolibarr_extension(src_dir: Union[str, Path]) -> bool:
"""
Check if directory contains a Dolibarr module.
Args:
src_dir: Source directory
Returns:
True if Dolibarr module detected
"""
ext_info = get_extension_info(src_dir)
return ext_info is not None and ext_info.platform == Platform.DOLIBARR
def main() -> None:
"""Test the extension utilities."""
import sys
sys.path.insert(0, str(Path(__file__).parent))
import common
common.log_section("Testing Extension Utilities")
# Test with current directory's src
repo_root = common.repo_root()
src_dir = repo_root / "src"
if not src_dir.is_dir():
common.log_warn(f"Source directory not found: {src_dir}")
return
ext_info = get_extension_info(src_dir)
if ext_info:
common.log_success("Extension detected!")
common.log_kv("Platform", ext_info.platform.value.upper())
common.log_kv("Name", ext_info.name)
common.log_kv("Version", ext_info.version)
common.log_kv("Type", ext_info.extension_type)
common.log_kv("Manifest", str(ext_info.manifest_path))
if ext_info.description:
common.log_kv("Description", ext_info.description[:60] + "...")
if ext_info.author:
common.log_kv("Author", ext_info.author)
else:
common.log_error("No extension detected")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Detect extension platform and type.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (./LICENSE.md).
FILE INFORMATION
DEFGROUP: Script.Release
INGROUP: Extension.Detection
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
PATH: /scripts/release/detect_platform.py
VERSION: 01.00.00
BRIEF: Detect extension platform and type for build workflow
USAGE: ./scripts/release/detect_platform.py [src_dir]
"""
import argparse
import sys
from pathlib import Path
# Add lib directory to path
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
try:
import extension_utils
except ImportError:
print("ERROR: Cannot import extension_utils library", file=sys.stderr)
sys.exit(1)
def main() -> int:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Detect extension platform and type",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"src_dir",
nargs="?",
default="src",
help="Source directory (default: src)"
)
parser.add_argument(
"--format",
choices=["pipe", "json"],
default="pipe",
help="Output format (default: pipe)"
)
args = parser.parse_args()
try:
ext_info = extension_utils.get_extension_info(args.src_dir)
if not ext_info:
print(f"ERROR: No extension detected in {args.src_dir}", file=sys.stderr)
return 1
if args.format == "pipe":
# Format: platform|ext_type
print(f"{ext_info.platform.value}|{ext_info.extension_type}")
elif args.format == "json":
import json
data = {
"platform": ext_info.platform.value,
"extension_type": ext_info.extension_type,
"name": ext_info.name,
"version": ext_info.version
}
print(json.dumps(data))
return 0
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -62,6 +62,9 @@ EXCLUDE_PATTERNS = {
# Documentation (optional, can be included)
# Build artifacts
"dist", "build", ".phpunit.cache",
# Development tool caches and artifacts
".phpstan.cache", ".psalm", ".rector",
"phpmd-cache", ".php-cs-fixer.cache", ".phplint-cache",
# OS files
".DS_Store", "Thumbs.db",
# Logs
@@ -78,10 +81,11 @@ EXCLUDE_PATTERNS = {
"composer.json", "composer.lock",
"package.json", "package-lock.json",
"phpunit.xml", "phpstan.neon", "phpcs.xml",
"codeception.yml",
"codeception.yml", "psalm.xml", ".php-cs-fixer.php",
# Others
"README.md", "CHANGELOG.md", "CONTRIBUTING.md",
"CODE_OF_CONDUCT.md", "SECURITY.md", "GOVERNANCE.md",
"Makefile",
}