Fix PHP CodeSniffer dependency conflict, add dev tools, implement platform-aware build system, and prepare dual-repository CI/CD migration #33
12
.github/workflows/php_quality.yml
vendored
@@ -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
|
||||
|
||||
55
.github/workflows/release_pipeline.yml
vendored
@@ -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
@@ -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/
|
||||
|
||||
49
Makefile
@@ -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
@@ -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
@@ -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
@@ -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/
|
||||
|
||||
356
scripts/lib/extension_utils.py
Normal 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()
|
||||
|
|
||||
if root.tag == "extension":
|
||||
return xml_file
|
||||
except Exception:
|
||||
continue
|
||||
|
The error handling catches all exceptions with a bare 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
|
||||
|
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
|
||||
|
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)
|
||||
|
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, 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
|
||||
|
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()
|
||||
98
scripts/release/detect_platform.py
Executable 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())
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 usedefusedxmllibrary 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.